feat: codegen routes — sanitize resource names, --inject-request/--package, expand form bodies

codegen_routes.generate_routes now sanitizes resource identifiers (fetch-requests -> fetch_requests), optionally prepends 'request: Request' (inject_request) and emits <resource>/routes.py packages with __init__.py (package). multipart/form-data and x-www-form-urlencoded bodies expand to per-property UploadFile/File/Form params instead of an opaque dict; spec paths are normalized to forward slashes to avoid Windows escape issues. Adds CLI flags --inject-request/--package, 7 focused tests, and refreshes lib/wiki/MCP docs to the real CLI.
This commit is contained in:
2026-10-13 03:26:31 +05:30
parent b559323dfe
commit 51ffb4aae9
38 changed files with 1156 additions and 3027 deletions

View File

@@ -1,6 +1,6 @@
# Codegen — Generate Models & Routes From Your Spec
> **TL;DR** — point `openapi-first codegen` at your spec and get `models` + `routes` scaffolds you can bind with one line. The codegen output is deterministic, and it's a *starting point* — not a maintained artifact.
> **TL;DR** — point `openapi-first models` / `openapi-first routes` at your spec and get Pydantic models plus operation-matching route stubs. The output is deterministic and is a *starting point* — not a maintained artifact.
---
@@ -9,48 +9,63 @@
Two pain points kill OpenAPI projects:
1. **The "write the spec, then write the same thing as Pydantic models" step** — exactly where server/client param types drift (your route says `item_id: int`, your client sends a string…).
2. **Writing the docs/spec catalog by hand**, once you have more than a handful of operations.
2. **Typing handler names by hand** when `bind_routes` requires each `operationId` to resolve to a function of the exact same name.
Codegen collapses both. One command, one source file, same generated shapes everywhere.
Codegen collapses both. One command, one source file, the same shapes everywhere.
---
## 🚀 2. Model Generation
```bash
openapi-first codegen models --module my_project.models --input openapi.yaml
openapi-first models openapi.yaml -o app/_gen/models.py
```
The generated model module mirrors the spec's `components.schemas` **by name**:
The generated module mirrors the spec's `components.schemas` **by name**:
| Spec | Generated |
|------|-----------|
| `components.schemas.User` | `class User(BaseModel)` |
| `components.schemas.Item` | `class Item(BaseModel)` |
| every `$ref` (schema) | a `type: ClassVar` alias ✓ |
| `required + type` from schema | Pydantic `Field(...)` / type hints ✓ |
**Never hand-edit those files.** If the spec changes, regenerate — just like you'd re-run `cargo build` after editing `Cargo.toml`.
**Never hand-edit those files.** If the spec changes, regenerate — just like re-running a compiler after editing source.
### Why constructor-time validation still applies
Codegen doesn't change the design: the generated models are plain Pydantic, and the **client/server still validate against the spec** at startup. Codegen is a *convenience accelerator* on top of the fail-fast guarantees in [04 – Design](../04_design.md) and [06 – Error Handling](../06_error_handling.md).
> **Wire model vs. domain model.** Generated models are the **wire contract**. Keep them separate from your persisted/domain model (see the standards *CRUD Template*): give the wire schema an `id: str`, but model the Mongo document so the id is optional.
---
## 🔁 3. Route Generation / Verification
## 🔁 3. Route Generation
```bash
# dry-run verification against your routes module
openapi-first codegen routes --module my_routes --input openapi.yaml --check
# scaffold an operation skeleton (generates the handler with a TODO)
openapi-first codegen routes --module my_routes --input openapi.yaml
openapi-first routes openapi.yaml -o app/interfaces \
--use-models --models-module app._gen.models \
--inject-request --package
```
Why bother? Because `bind_routes` (in [02 – Components](../02_components.md)) needs an `operationId` → handler **exactly** matching the spec. Codegen guarantees you never type a handler name wrong — it wears the same `operationId` as the spec says.
One file per resource (derived from the first path segment). Flags:
> **Note:** `codegen` is build-time tooling. It does **not** run at runtime, and templates (the Bake-your-stuff section of [02 – Templates](02_templates.md)) make scaffolding a server out-of-the-box even simpler for greenfield projects.
| Flag | Effect |
|------|--------|
| `--use-models` | Import request-body `$ref` models from `--models-module` |
| `--models-module MOD` | Dotted module for those imports (default `models`) |
| `--inject-request` | Prepend `request: Request` to every handler (DI convention) |
| `--package` | Emit `<resource>/routes.py` packages (with `__init__.py`) instead of flat `<resource>.py` |
Why bother? Because `bind_routes` (in [02 – Components](../02_components.md)) needs an `operationId` → handler **exactly** matching the spec. Codegen guarantees you never type a handler name wrong — it wears the same `operationId` the spec declares.
### Resource names and extra media types
- A path such as `/fetch-requests` yields a **sanitized** module name `fetch_requests.py` (a raw hyphen could never be imported).
- `multipart/form-data` and `application/x-www-form-urlencoded` bodies expand to one parameter per schema property: `UploadFile = File(...)` for `format: binary`, `Form(...)` otherwise — not an opaque `payload: dict`. Fields are required only when listed in the schema's `required`.
The stubs `raise NotImplementedError`; fill each body in (typically by resolving a service from `request.app.state`).
> **Note:** `models` / `routes` are build-time tooling. They do **not** run at runtime, and templates (the Bake-your-stuff section of [02 – Templates](02_templates.md)) scaffold a working server out-of-the-box for greenfield projects.
---