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
Swagger UI (internal development platform) · ReDoc (open platform)
\"\n") + builder.write_string("}\n\n") + builder.write_string("///|\n") + builder.write_string("fn redoc_html() -> String {\n") + builder.write_string(" \"API Docs
ReDoc (open platform) · Swagger UI (internal development platform)
\"\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) }