diff --git a/README.md b/README.md index 500eb46..caf335c 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 61a76a1..0fe83fc 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/generated/client.mbt"] = generate_client_file(ir) files["src/handlers/moon.pkg"] = generate_handlers_pkg(module_name) for operation in ir.operations { @@ -194,10 +198,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(" \"