From 8d0d3a2b3a6d02579f1bac520acfa6999af2fc9d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 16 Apr 2026 19:00:21 +0000
Subject: [PATCH 1/6] Add generated /docs Swagger UI and ReDoc routes
Agent-Logs-Url: https://github.com/cogna-dev/mapi/sessions/02524dc6-8ed4-4160-bb9b-ee50d44c6748
Co-authored-by: yufeiminds <9150374+yufeiminds@users.noreply.github.com>
---
README.md | 1 +
main/generator.mbt | 87 ++++++++++++++++++++++++++++++++++++++++++++--
main/main.mbt | 2 +-
3 files changed, 86 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 29c8fd9..0e14f50 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,7 @@ Whether you're scaffolding a greenfield service from an OpenAPI contract, evolvi
- **Host-adapter architecture** — the runtime core (`lib/`) is completely decoupled from I/O; any execution environment (Bun, Node, Deno, native Wasm) can be plugged in by implementing a thin host adapter
- **Type-safe HTTP primitives** — `HttpMethod`, `RequestEnvelope`, `ResponseEnvelope`, and a precise typed error hierarchy (`AppError`, `DecodeError`) mean routing mistakes are caught at compile time, not at runtime
- **In-memory test host** — `InMemoryHost` lets you drive your full routing and handler stack in unit tests without spinning up a real server or making network calls
+- **Built-in API docs routes** — generated projects automatically expose Swagger UI at `/docs`, ReDoc at `/docs/redoc`, and the raw OpenAPI document at `/docs/openapi`
- **Five focused CLI commands** — `init`, `generate`, `check`, `diff`, `doctor` — each doing exactly one thing, composable in scripts and CI pipelines
---
diff --git a/main/generator.mbt b/main/generator.mbt
index 29eea47..88dd3ba 100644
--- a/main/generator.mbt
+++ b/main/generator.mbt
@@ -5,14 +5,18 @@ let generated_header = "// Code generated by mapi. DO NOT EDIT.\n"
let user_header = "// User-owned file. Safe from regeneration.\n"
///|
-fn generate_project(ir : ApiIR, module_name : String) -> Map[String, String] {
+fn generate_project(
+ ir : ApiIR,
+ module_name : String,
+ openapi_spec_raw : String,
+) -> Map[String, String] {
let files = {}
files["moon.mod.json"] = generate_moon_mod(module_name)
files["src/generated/moon.pkg"] = generate_generated_pkg(module_name)
files["src/generated/schemas.mbt"] = generate_schemas_file(ir)
files["src/generated/operations.mbt"] = generate_operations_file(ir)
files["src/generated/contract.mbt"] = generate_contract_file(ir)
- files["src/generated/router.mbt"] = generate_router_file(ir)
+ files["src/generated/router.mbt"] = generate_router_file(ir, openapi_spec_raw)
files["src/handlers/moon.pkg"] = generate_handlers_pkg(module_name)
for operation in ir.operations {
files["src/handlers/" + operation.snake_id + ".mbt"] = generate_handler_file(
@@ -192,10 +196,40 @@ fn generate_contract_file(ir : ApiIR) -> String {
}
///|
-fn generate_router_file(ir : ApiIR) -> String {
+fn generate_router_file(ir : ApiIR, openapi_spec_raw : String) -> String {
let builder = StringBuilder::new()
+ let escaped_openapi_spec = escape_moon_string(openapi_spec_raw)
+ let openapi_content_type = infer_openapi_content_type(openapi_spec_raw)
builder.write_string(generated_header)
builder.write_string("///|\n")
+ builder.write_string("fn docs_html_response(html : String) -> @lib.ResponseEnvelope {\n")
+ builder.write_string(" {\n")
+ builder.write_string(" status: 200,\n")
+ builder.write_string(" headers: { \"content-type\": \"text/html; charset=utf-8\" },\n")
+ builder.write_string(" body: Some(@utf8.encode(html)),\n")
+ builder.write_string(" }\n")
+ builder.write_string("}\n\n")
+ builder.write_string("///|\n")
+ builder.write_string("fn openapi_docs_response() -> @lib.ResponseEnvelope {\n")
+ builder.write_string(" {\n")
+ builder.write_string(" status: 200,\n")
+ builder.write_string(" headers: { \"content-type\": \"")
+ builder.write_string(openapi_content_type)
+ builder.write_string("\" },\n")
+ builder.write_string(" body: Some(@utf8.encode(\"")
+ builder.write_string(escaped_openapi_spec)
+ builder.write_string("\")),\n")
+ builder.write_string(" }\n")
+ builder.write_string("}\n\n")
+ builder.write_string("///|\n")
+ builder.write_string("fn swagger_ui_html() -> String {\n")
+ builder.write_string(" \"
API Docs\"\n")
+ builder.write_string("}\n\n")
+ builder.write_string("///|\n")
+ builder.write_string("fn redoc_html() -> String {\n")
+ builder.write_string(" \"API Docs\"\n")
+ builder.write_string("}\n\n")
+ builder.write_string("///|\n")
builder.write_string(
"pub fn routes(_handlers : ApiHandlers) -> Array[@lib.Route] {\n",
)
@@ -209,6 +243,27 @@ fn generate_router_file(ir : ApiIR) -> String {
"\", handler: (_ctx, _req, _params) => { status: 501, headers: Map::new(), body: None } },\n",
)
}
+ builder.write_string(" {\n")
+ builder.write_string(" http_method: @lib.HttpMethod::Get,\n")
+ builder.write_string(" pattern: \"/docs\",\n")
+ builder.write_string(
+ " handler: (_ctx, _req, _params) => docs_html_response(swagger_ui_html()),\n",
+ )
+ builder.write_string(" },\n")
+ builder.write_string(" {\n")
+ builder.write_string(" http_method: @lib.HttpMethod::Get,\n")
+ builder.write_string(" pattern: \"/docs/redoc\",\n")
+ builder.write_string(
+ " handler: (_ctx, _req, _params) => docs_html_response(redoc_html()),\n",
+ )
+ builder.write_string(" },\n")
+ builder.write_string(" {\n")
+ builder.write_string(" http_method: @lib.HttpMethod::Get,\n")
+ builder.write_string(" pattern: \"/docs/openapi\",\n")
+ builder.write_string(
+ " handler: (_ctx, _req, _params) => openapi_docs_response(),\n",
+ )
+ builder.write_string(" },\n")
builder.write_string(" ]\n")
builder.write_string("}\n\n")
builder.write_string("///|\n")
@@ -224,6 +279,32 @@ fn generate_router_file(ir : ApiIR) -> String {
builder.to_string()
}
+///|
+fn infer_openapi_content_type(openapi_spec_raw : String) -> String {
+ let trimmed = openapi_spec_raw.trim()
+ if trimmed.has_prefix("{") || trimmed.has_prefix("[") {
+ "application/json"
+ } else {
+ "application/yaml"
+ }
+}
+
+///|
+fn escape_moon_string(raw : String) -> String {
+ let builder = StringBuilder::new()
+ for ch in raw.to_array() {
+ match ch {
+ '\\' => builder.write_string("\\\\")
+ '"' => builder.write_string("\\\"")
+ '\n' => builder.write_string("\\n")
+ '\r' => builder.write_string("\\r")
+ '\t' => builder.write_string("\\t")
+ _ => builder.write_char(ch)
+ }
+ }
+ builder.to_string()
+}
+
///|
fn generate_handler_file(operation : OperationIR) -> String {
let builder = StringBuilder::new()
diff --git a/main/main.mbt b/main/main.mbt
index 0c9809a..dc4910a 100644
--- a/main/main.mbt
+++ b/main/main.mbt
@@ -44,7 +44,7 @@ fn run_command(args : Array[String], mode : CommandMode) -> Unit {
}
}
let module_name = infer_module_name(config.output_dir)
- let files = generate_project(ir, module_name)
+ let files = generate_project(ir, module_name, spec)
write_project(files, config.output_dir, mode == Generate) catch {
err => fail_with_message("Failed to write project: ", debug_string(err))
}
From f7f27c4eaeca567a24de1cfa3809cbf0c6f1885c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 16 Apr 2026 19:01:12 +0000
Subject: [PATCH 2/6] Harden generated OpenAPI content-type inference
Agent-Logs-Url: https://github.com/cogna-dev/mapi/sessions/02524dc6-8ed4-4160-bb9b-ee50d44c6748
Co-authored-by: yufeiminds <9150374+yufeiminds@users.noreply.github.com>
---
main/generator.mbt | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/main/generator.mbt b/main/generator.mbt
index 88dd3ba..c4e5b7e 100644
--- a/main/generator.mbt
+++ b/main/generator.mbt
@@ -284,8 +284,15 @@ fn infer_openapi_content_type(openapi_spec_raw : String) -> String {
let trimmed = openapi_spec_raw.trim()
if trimmed.has_prefix("{") || trimmed.has_prefix("[") {
"application/json"
- } else {
+ } else if trimmed.has_prefix("openapi:") ||
+ trimmed.has_prefix("---") ||
+ trimmed.has_prefix("#") ||
+ trimmed.contains("\nopenapi:") {
"application/yaml"
+ } else if trimmed.is_empty() {
+ "text/plain; charset=utf-8"
+ } else {
+ "text/plain; charset=utf-8"
}
}
From f7ad8c89d44e38acaf23bec16a5497791b3a34eb Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 16 Apr 2026 19:01:37 +0000
Subject: [PATCH 3/6] Simplify OpenAPI content-type fallback branch
Agent-Logs-Url: https://github.com/cogna-dev/mapi/sessions/02524dc6-8ed4-4160-bb9b-ee50d44c6748
Co-authored-by: yufeiminds <9150374+yufeiminds@users.noreply.github.com>
---
main/generator.mbt | 2 --
1 file changed, 2 deletions(-)
diff --git a/main/generator.mbt b/main/generator.mbt
index c4e5b7e..f800583 100644
--- a/main/generator.mbt
+++ b/main/generator.mbt
@@ -289,8 +289,6 @@ fn infer_openapi_content_type(openapi_spec_raw : String) -> String {
trimmed.has_prefix("#") ||
trimmed.contains("\nopenapi:") {
"application/yaml"
- } else if trimmed.is_empty() {
- "text/plain; charset=utf-8"
} else {
"text/plain; charset=utf-8"
}
From 2c0cd5783bda7bfe6318d012e06d10579d8689cf Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 16 Apr 2026 19:02:20 +0000
Subject: [PATCH 4/6] Harden docs content-type detection and string escaping
Agent-Logs-Url: https://github.com/cogna-dev/mapi/sessions/02524dc6-8ed4-4160-bb9b-ee50d44c6748
Co-authored-by: yufeiminds <9150374+yufeiminds@users.noreply.github.com>
---
main/generator.mbt | 32 +++++++++++++++++++++++++++-----
1 file changed, 27 insertions(+), 5 deletions(-)
diff --git a/main/generator.mbt b/main/generator.mbt
index f800583..4da6a77 100644
--- a/main/generator.mbt
+++ b/main/generator.mbt
@@ -284,10 +284,7 @@ fn infer_openapi_content_type(openapi_spec_raw : String) -> String {
let trimmed = openapi_spec_raw.trim()
if trimmed.has_prefix("{") || trimmed.has_prefix("[") {
"application/json"
- } else if trimmed.has_prefix("openapi:") ||
- trimmed.has_prefix("---") ||
- trimmed.has_prefix("#") ||
- trimmed.contains("\nopenapi:") {
+ } else if trimmed.contains("openapi:") {
"application/yaml"
} else {
"text/plain; charset=utf-8"
@@ -304,12 +301,37 @@ fn escape_moon_string(raw : String) -> String {
'\n' => builder.write_string("\\n")
'\r' => builder.write_string("\\r")
'\t' => builder.write_string("\\t")
- _ => builder.write_char(ch)
+ _ => {
+ let code = ch.to_int()
+ if code >= 0 && code < 32 {
+ builder.write_string("\\u00")
+ builder.write_string(to_hex_byte(code))
+ } else {
+ builder.write_char(ch)
+ }
+ }
}
}
builder.to_string()
}
+///|
+fn to_hex_byte(value : Int) -> String {
+ let builder = StringBuilder::new()
+ builder.write_char(to_hex_digit(value / 16))
+ builder.write_char(to_hex_digit(value % 16))
+ builder.to_string()
+}
+
+///|
+fn to_hex_digit(value : Int) -> Char {
+ if value < 10 {
+ (value + '0'.to_int()).unsafe_to_char()
+ } else {
+ (value - 10 + 'a'.to_int()).unsafe_to_char()
+ }
+}
+
///|
fn generate_handler_file(operation : OperationIR) -> String {
let builder = StringBuilder::new()
From 5525567c1ffdac945f1503cd58a1220ef8d07f05 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 16 Apr 2026 19:03:02 +0000
Subject: [PATCH 5/6] Refine YAML key detection and simplify control-char check
Agent-Logs-Url: https://github.com/cogna-dev/mapi/sessions/02524dc6-8ed4-4160-bb9b-ee50d44c6748
Co-authored-by: yufeiminds <9150374+yufeiminds@users.noreply.github.com>
---
main/generator.mbt | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/main/generator.mbt b/main/generator.mbt
index 4da6a77..5815bfd 100644
--- a/main/generator.mbt
+++ b/main/generator.mbt
@@ -284,13 +284,23 @@ fn infer_openapi_content_type(openapi_spec_raw : String) -> String {
let trimmed = openapi_spec_raw.trim()
if trimmed.has_prefix("{") || trimmed.has_prefix("[") {
"application/json"
- } else if trimmed.contains("openapi:") {
+ } else if has_openapi_yaml_key(openapi_spec_raw) {
"application/yaml"
} else {
"text/plain; charset=utf-8"
}
}
+///|
+fn has_openapi_yaml_key(raw : String) -> Bool {
+ for line in raw.split("\n") {
+ if line.to_string().trim_start().has_prefix("openapi:") {
+ return true
+ }
+ }
+ false
+}
+
///|
fn escape_moon_string(raw : String) -> String {
let builder = StringBuilder::new()
@@ -303,7 +313,7 @@ fn escape_moon_string(raw : String) -> String {
'\t' => builder.write_string("\\t")
_ => {
let code = ch.to_int()
- if code >= 0 && code < 32 {
+ if code < 32 {
builder.write_string("\\u00")
builder.write_string(to_hex_byte(code))
} else {
From 7708c3b5793bd249f5bc8fbf3b0ea578efabc7e8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 16 Apr 2026 19:03:56 +0000
Subject: [PATCH 6/6] Fix generated Unicode escape syntax for control chars
Agent-Logs-Url: https://github.com/cogna-dev/mapi/sessions/02524dc6-8ed4-4160-bb9b-ee50d44c6748
Co-authored-by: yufeiminds <9150374+yufeiminds@users.noreply.github.com>
---
main/generator.mbt | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/main/generator.mbt b/main/generator.mbt
index 5815bfd..abe63b3 100644
--- a/main/generator.mbt
+++ b/main/generator.mbt
@@ -313,9 +313,10 @@ fn escape_moon_string(raw : String) -> String {
'\t' => builder.write_string("\\t")
_ => {
let code = ch.to_int()
- if code < 32 {
- builder.write_string("\\u00")
+ if code < 32 || code == 127 {
+ builder.write_string("\\u{")
builder.write_string(to_hex_byte(code))
+ builder.write_string("}")
} else {
builder.write_char(ch)
}