You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(moonapi): routers, foreign mounts, the response kinds, and real streaming.
`App::mount` was the only way to compose, and it applied nothing of its own to what it included — no tags, no security, no responses. `Router` and `include_router` do what FastAPI's do, and `mount_handler` takes any moonasgi handler, which is the hook a static-file server would plug into.
Path operations carried 6 of FastAPI's 22 keyword arguments. `description`, `operation_id`, `status_code`, `responses`, `name` and `openapi_extra` are there now, the last merged into the operation object last so a caller can override anything generated. `url_for` renders a named route, mount prefix included.
Responses: redirect, file, streaming and cookies did not exist, and `to_asgi` sent exactly one chunk with `more_body=false` — so SSE, the one case where streaming is the whole point, buffered its event stream into a single body. A streaming route dispatches through moonasgi's stream path now. Plus the `status` constants FastAPI re-exports, `UploadFile`'s size and headers, and part-count and part-size limits on multipart, which was previously bounded only by whatever had already been buffered.
Signed-off-by: Leo Cheng (heke1228) <chengkelfan@qq.com>
Copy file name to clipboardExpand all lines: README.md
+9-5Lines changed: 9 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -41,23 +41,27 @@ let docs_page = @moonapi.swagger_ui() // a Swagger UI page
41
41
42
42
## What's here (`v0`)
43
43
44
-
-**Routing** — `App::get/post/put/patch/delete/route`, `:param` path segments extracted into `Context::param`, correct `404` (no path) vs `405` (path but not method).
44
+
-**Routing** — `App::get/post/put/patch/delete/route`, `:param` path segments extracted into `Context::param`, correct `404` (no path) vs `405` (path but not method). A route takes the operation arguments FastAPI's path operations take — `summary`, `description`, `tags`, `deprecated`, `operation_id`, `status_code`, `responses`, `name`, `include_in_schema`, and an `openapi_extra` fragment merged over the generated operation object — and `App::url_for(name, params)` resolves a named route back to its path (← `url_path_for`), mount prefix included.
45
+
-**Routers** — `Router` collects routes away from any application, and `App::include_router(router, prefix=, tags=, security=, responses=, deprecated=, include_in_schema=)` folds them in as the app's own (← FastAPI's `APIRouter` / `include_router`). The arguments given at the join reach every route in the group: tags and security requirements lead the route's own, group responses are documented under it, and `deprecated` / `include_in_schema` mark or hide the whole set. A router is a value, so the same one can be included twice under different prefixes.
45
46
-**Descriptor tree** — a runtime `Schema` / `Param` / `Endpoint` tree (a one-first-class-value substitute for FastAPI's from-signature reflection) that a typed route carries. Walked **once** to (a) emit **complete** OpenAPI request/response body schemas — objects, arrays, scalars, `required`, with named models hoisted under `components/schemas` and referenced by `$ref` — and (b) drive request validation off the same tree. User structs describe themselves with `derive(ToJson)` + a `T::schema()` associated function + a one-line `ToSchema` bridge (the mctl-friendly shape).
46
47
-**Multi-version OpenAPI** — `App::openapi` / `openapi_json` emit **Swagger 2.0, OpenAPI 3.0.3, and OpenAPI 3.1.0** from the same routes and descriptors (3.x `requestBody` + `components/schemas`; 2.0 body-parameter + `definitions`), because a good FastAPI is not pinned to one spec version.
47
48
-**Typed body extractors** — `Context::body[T]` deserialises the JSON body into a `derive(FromJson)` struct; `Context::body_validated[T]` first checks it against the endpoint descriptor and returns either the built value or a FastAPI-shaped `422` error list — schema emission, validation, and deserialisation all off one descriptor.
48
49
-**Dependency injection** — a `Container` (provider registry + `dependency_overrides`) with request-scoped, one-shot resolution (per-request caching) and `yield`-style teardown run LIFO around the handler — the explicit MoonBit equivalent of FastAPI's `Depends`.
49
50
-**OAuth2 + JWT security** — a `/token` password-grant endpoint issues an HS256 JWT (`create_access_token`), and an `OAuth2PasswordBearer` reads the `Authorization: Bearer` header, verifies the token, and enforces scopes: `401` on a missing or invalid/expired token, `403` when a valid token lacks a required scope. The SHA-256 / HMAC-SHA256 pair is self-built (`crypto.mbt`), checked against the NIST and RFC 4231 vectors; the `alg: "none"` downgrade is refused and signatures compare in constant time.
50
-
-**Form & file extractors** — `Context::form` parses both an `application/x-www-form-urlencoded` body (percent- and `+`-decoded) and a `multipart/form-data` body, splitting the boundary stream into `FormField`s and byte-exact `UploadFile`s (filename + content-type + raw bytes). `Context::oauth2_password_form` reads the OAuth2 password form off it.
51
+
-**Form & file extractors** — `Context::form` parses both an `application/x-www-form-urlencoded` body (percent- and `+`-decoded) and a `multipart/form-data` body, splitting the boundary stream into `FormField`s and byte-exact `UploadFile`s (filename + content-type + `size` + the part's own `headers` + raw bytes). A body is attacker-controlled and already buffered, so `FormLimits` bounds what one may spend — at most `max_parts` parts of `max_part_size` bytes, Starlette's 1000 × 1 MiB by default — and a body over either bound comes back as `None`, refused whole rather than truncated. `Context::oauth2_password_form` reads the OAuth2 password form off it.
51
52
-**response_model** — `filter_response` / `json_model` validate a handler's return value against a declared `Schema` and project it down to exactly the model's fields, so a route can hold a richer object internally than it exposes (an id, a password hash) and still emit only what it promised.
52
53
-**Middleware & exception handlers** — an outer middleware chain (`App::middleware`) with `cors(...)` (preflight + actual-request headers, configurable origins/methods/headers, credentials, exposed headers) and `gzip(...)` (a real DEFLATE compressor, below), plus exception handlers (`App::exception_handler`): a handler `raise`s an `HttpException` and the app maps it to a response, falling through to a built-in `{"detail": ...}` for `HttpException` and a `500` for anything else. `App::add_status_handler(code, ...)` swaps in a custom response for any error status — a routing `404` / `405` or a raised exception's status — so an app can serve its own error pages.
53
54
-**Per-operation security** — a route carries `security=[SecurityRequirement::new(scheme, scopes=[...])]`. `App::secure_oauth2(name, bearer)` both surfaces the scheme in the spec and registers it as a guard, so the app pulls and verifies the bearer token and checks the route's scopes **before** the handler — `401` unauthenticated, `403` on a missing scope — and emits the requirement as the operation's OpenAPI `security` array. A bare `add_security_scheme` documents without enforcing (FastAPI's split between a scheme and a wired dependency).
54
55
-**Background tasks** — a background-aware route (`App::route_bg`) receives a `BackgroundTasks` queue; `add_task` defers work that the app runs, in order, **after** the response is sent (← FastAPI's `BackgroundTasks`), so a slow write never delays the client.
55
-
-**Sub-application mounting** — `App::mount(prefix, subapp)` composes routers: a request under `prefix` is routed by the sub-app with the prefix stripped (its own middleware, security, and background tasks apply), and the sub-app's routes and security schemes fold into the parent's merged OpenAPI document under the prefix. Mounts nest.
56
-
-**Server-Sent Events** — `ServerSentEvent` frames per the WHATWG event-stream format (`id` / `event` / `retry` / multi-line `data` / `:` comments) and `sse_response` builds the `text/event-stream` envelope.
56
+
-**Sub-application mounting** — `App::mount(prefix, subapp)` composes routers: a request under `prefix` is routed by the sub-app with the prefix stripped (its own middleware, security, and background tasks apply), and the sub-app's routes and security schemes fold into the parent's merged OpenAPI document under the prefix. Mounts nest. `App::mount_handler(prefix, handler)` mounts a foreign moonasgi `Handler` the same way — a third-party component, or static files — which the app routes to but does not document.
57
+
-**Streaming responses** — `App::stream(path, handler)` / `App::route_stream(verb, ...)` register a route returning a `moonasgi.StreamingResponse`, whose chunks reach the client as separate body events — a client reads the first long before the last one exists. `App::handle_with_stream` is the chunk-level view a test reads. A middleware is typed buffered-in, buffered-out, so one that rewrites the body (`gzip`) collapses the reply to a single chunk rather than cutting new bytes at boundaries that no longer describe them.
58
+
-**Server-Sent Events** — `ServerSentEvent` frames per the WHATWG event-stream format (`id` / `event` / `retry` / multi-line `data` / `:` comments); `sse_response` is a `text/event-stream` stream of those frames, **one chunk per event**, so each dispatches on arrival. Hand it to `App::stream`. (An event stream delivered as one body is not an event stream — it is a file shaped like one.)
57
59
-**WebSocket routes** — `App::websocket(path, handler)` over the moonasgi WS SEAM. The handler drives a `WebSocket` (accept / receive / send / close); it's a synchronous core, so `drive_websocket` runs it against an in-memory frame queue in a test and `App::to_asgi` serves it over the async transport.
58
60
-**Security schemes** — `App::add_security_scheme` surfaces the OAuth2 / JWT layer in the emitted spec (`components/securitySchemes` in 3.x, `securityDefinitions` in 2.0); `OAuth2PasswordBearer::scheme` builds the password-flow object.
59
61
-**Swagger UI** — `swagger_ui()` returns a ready-to-serve documentation page.
60
-
-**Responses** — `text` and `json` helpers over `moonasgi.Response`.
62
+
-**Responses** — `text`, `html`, `json` and `json_model` helpers over `moonasgi.Response`, plus `redirect(url, status=307)` (← `RedirectResponse`; the URL is encoded over the characters a URI reserves for structure, so an already-encoded URL passes through and a smuggled `CRLF` cannot open a header) and `file_response(content, filename=, media_type=, inline=)` (← `FileResponse`: a media type guessed from the extension, `Content-Length`, and a `Content-Disposition` that falls back to RFC 6266's `filename*` when the name will not survive quoting). It takes bytes rather than a path because moonapi has no filesystem — the same app runs on wasm, js and native.
63
+
-**Cookies** — `set_cookie(resp, name, value, max_age=, expires=, path=, domain=, secure=, http_only=, same_site=)` and `delete_cookie(...)` (← `response.set_cookie` / `delete_cookie`). Each returns a new response carrying one more `Set-Cookie`, which is the correct wire shape: two cookies are two headers, never one folded field. Names, values and attributes are stripped of the octets RFC 6265 forbids, so a value cannot forge an attribute or open a header of its own; `delete_cookie` expires by both `Max-Age=0` and a 1970 date. `Context::cookie` reads them back.
64
+
-**Status constants** — the 63 `HTTP_*` and 15 `WS_*` names FastAPI re-exports from Starlette (`HTTP_404_NOT_FOUND`, `WS_1008_POLICY_VIOLATION`), so a route table shows a deliberate `307` rather than a bare number.
61
65
62
66
Verified across all backends (`wasm`, `wasm-gc`, `js`, `native`) in CI, 0 warnings under `--deny-warn`.
0 commit comments