moonapi

moonapi — a typed web framework for MoonBit (← FastAPI): routing, typed extractors, descriptor-driven validation, multi-version OpenAPI/Swagger (2.0 / 3.0 / 3.1) with security schemes, dependency injection, OAuth2 password-bearer with self-built HS256 JWT and scopes, per-operation security enforcement, multipart/urlencoded form and file extractors, response_model filtering, background tasks, sub-application mounting, a CORS/gzip(real DEFLATE)/exception-handler middleware stack with per-status handlers, Server-Sent Events, and WebSocket routes, on the moonasgi SEAM.

fastapi
web
framework
openapi
swagger
asgi
moonbit
Download zip
Author
Version
0.7.0
License
Apache-2.0
Last updated
13 hours ago
Downloads
477

Dependencies

#moonapi

A typed web framework for MoonBit — ← FastAPI.

Check and Test License mooncakes

moonapi builds an AsgiApp from typed routes and generates its own OpenAPI / Swagger document — the role FastAPI plays for Python. It depends only on moonasgi, so it's backend-agnostic (routing and OpenAPI run in-process on every backend); a server such as mooncat runs the resulting app.

flowchart LR routes["typed routes<br/>App::get / post / …"] --> app["**moonapi** App"] app -->|"App::to_asgi()"| asgi(["moonasgi AsgiApp"]) app -->|"App::openapi()"| spec["OpenAPI 2.0 / 3.0 / 3.1"] asgi --> cat["mooncat serves it"]

#Quickstart

let app = @moonapi.App::new()
app.get("/", _ctx => @moonapi.text(200, "Hello from moonapi!"))
app.get("/users/:id", ctx => @moonapi.text(200, "user " + ctx.param("id").unwrap()),
summary="fetch a user")
app.post("/users", _ctx => @moonapi.text(201, "created"))

// One set of routes → every mainstream spec version:
let v31 = app.openapi_json(version=OpenApi31) // OpenAPI 3.1.0
let v30 = app.openapi_json(version=OpenApi30) // OpenAPI 3.0.3
let v20 = app.openapi_json(version=Swagger20) // Swagger 2.0
let docs_page = @moonapi.swagger_ui() // a Swagger UI page

// Serve it (native, via mooncat):
@mooncat.serve(app.to_asgi(), port=8000)

#What's here (v0)

  • RoutingApp::get/post/put/patch/delete/route, :param path segments extracted into Context::param, correct 404 (no path) vs 405 (path but not method).
  • 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).
  • Multi-version OpenAPIApp::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.
  • Typed body extractorsContext::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.
  • 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.
  • 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.
  • Form & file extractorsContext::form parses both an application/x-www-form-urlencoded body (percent- and +-decoded) and a multipart/form-data body, splitting the boundary stream into FormFields and byte-exact UploadFiles (filename + content-type + raw bytes). Context::oauth2_password_form reads the OAuth2 password form off it.
  • response_modelfilter_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.
  • 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 raises 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.
  • 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).
  • 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.
  • Sub-application mountingApp::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.
  • Server-Sent EventsServerSentEvent frames per the WHATWG event-stream format (id / event / retry / multi-line data / : comments) and sse_response builds the text/event-stream envelope.
  • WebSocket routesApp::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.
  • Security schemesApp::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.
  • Swagger UIswagger_ui() returns a ready-to-serve documentation page.
  • Responsestext and json helpers over moonasgi.Response.

Verified across all backends (wasm, wasm-gc, js, native) in CI, 0 warnings under --deny-warn.

#Design notes

Two places make an explicit, documented trade-off rather than a silent shortcut:

  • GZip produces a valid RFC 1952 gzip stream — correct header, CRC-32, and ISIZE — around a real RFC 1951 DEFLATE payload: LZ77 back-reference matching (a 32 KiB window, hash-chain match finder) coded with the fixed Huffman table (deflate.mbt), so the body actually shrinks. A companion inflate decodes it, so the encoder is round-trip-verified on every backend, and the system gzip reads the output in CI. The one increment left is dynamic Huffman (a per-block code fit to the data) for a tighter ratio; fixed Huffman already delivers a genuine ratio (roughly 19× on a repetitive body).
  • WebSocket handlers are a synchronous core: the same handler runs in a test and under a server. Because the core can't suspend on the async transport (MoonBit runs async only in an async context), the serving shell buffers the client's inbound frames, runs the handler, then emits its frames. Content and order are preserved — exact for echo, broadcast, and request-reply — but it doesn't interleave live per-frame with the client.

#Roadmap (transliterating FastAPI)

The descriptor tree (Endpoint / Param / Schema) drives OpenAPI body schemas and validation; on top sit the typed derive(FromJson) body extractors, the dependency-injection container, the OAuth2 password-bearer security layer with self-built HS256 JWT, the Form / File extractors, and response_model filtering. On the middleware side: CORS, a real DEFLATE gzip, exception and per-status handlers, Server-Sent Events, and WebSocket routes. This release wires per-operation security from the declared schemes (enforced, and emitted into the spec), background tasks that run after the response, and sub-application mounting with a merged OpenAPI document. Next: RS256 / ES256 signing (a self-built RSA/ECDSA bignum stack); dynamic-Huffman DEFLATE for a tighter ratio; static files and templates; and codegen'd request schemas via moonctl.

#License

Apache-2.0.

ApiHandler

type ApiHandler = (Context) ->
Response
raise

A moonapi route handler: request context in, response out. It may raise an HttpException (or any error) instead of returning — the app catches it and maps it to a response through the registered exception handlers, so handlers read like FastAPI's raise HTTPException(...) rather than threading a Result back by hand. A plain non-raising closure is still a valid handler.

BackgroundHandler

type BackgroundHandler = (Context, BackgroundTasks) ->
Response
raise

A background-aware handler: it additionally receives the request's BackgroundTasks queue, so it can schedule work to run after its response is sent (← declaring a BackgroundTasks parameter in FastAPI).

ExceptionHandler

type ExceptionHandler = (Context, Error) ->
Response
?

An exception handler: given the request context and the raised error, return Some(response) to handle it or None to defer to the next handler. The explicit MoonBit form of FastAPI's @app.exception_handler(ExcType) — the None case stands in for "this handler isn't registered for that type".

WsHandler

type WsHandler = (WebSocket) -> Unit

A WebSocket route handler: given the connection, drive the exchange. Usually accept, then a receive loop, then close.

ToSchema

pub(open) trait ToSchema {
fn to_schema(Self) -> Schema
}

The impl-able version of a type's descriptor. A user struct implements it — the value is ignored; it exists so descriptor-carrying code can be generic over "a type that knows its own schema". The primary, mctl-friendly shape is still a plain associated function T::schema() -> Schema (no instance needed, mirroring FastAPI referencing the model class); this trait bridges to it.

HttpException

pub(all) suberror HttpException {
HttpException(status~ : Int, detail~ : Json, headers~ : Array[(String, String)])
}

An HTTP error a handler can raise to short-circuit with a status and body (← FastAPI's HTTPException). detail is any JSON (a string is the common case); headers are added to the response (e.g. a WWW-Authenticate challenge). Caught by the app and mapped to a response.

JwtError

pub suberror JwtError {
MalformedToken(String)
UnsupportedAlg(String)
BadSignature
Expired
NotYetValid
}

A JWT verification failure. Each way a token can be rejected is reported distinctly so the Security layer can map it to the right status and a caller can log precisely which check failed.

Address

pub(all) struct Address {
city : String
zip : String
} derive(Eq, ToJson)

A nested value type, to show a $ref chain: User.address references this under components/schemas.
impl ToSchema for Address

Address::schema

fn Address::schema() -> Schema

The Address descriptor.

ApiInfo

pub(all) struct ApiInfo {
title : String
api_version : String
description : String
terms_of_service : String
contact : Contact?
license : License?
servers : Array[Server]
}

What the document says about the API itself — FastAPI's info block plus servers. An app carries one (App::describe sets it) so that every emission of the document, including the route enable_docs registers, agrees.

ApiInfo::new

fn ApiInfo::new() -> ApiInfo

The defaults an app starts with.

App

pub struct App {
routes : Array[Route]
ws_routes : Array[WsRoute]
middlewares : Array[((
Request
) ->
Response
) -> ((
Request
) ->
Response
)]
exception_handlers : Array[(Context, Error) ->
Response
?]
security_schemes : Array[(String, SecurityScheme)]
enforcers : Map[String, (Context, Array[String], Int64) -> Result[AuthenticatedUser,
Response
]]
status_handlers : Map[Int, (Context) ->
Response
]
mounts : Array[(String, App)]
startup_hooks : Array[() -> Unit raise]
shutdown_hooks : Array[() -> Unit raise]
info : ApiInfo
clock : () -> Int64
}

A moonapi application: routes, WebSocket routes, an outer middleware chain, exception handlers, mounted sub-applications, the security schemes surfaced in the OpenAPI document (with optional runtime enforcers), per-status exception handlers, and the verification clock. Compiles to a moonasgi AsgiApp any server (mooncat) can run.

App::add_security_scheme

fn App::add_security_scheme(self : App, name : String, scheme : SecurityScheme) -> Unit

Declare a security scheme under name, surfaced in the emitted OpenAPI document (components/securitySchemes in 3.x, securityDefinitions in Swagger 2.0) so the generated spec describes how to authenticate.

App::add_status_handler

fn App::add_status_handler(self : App, status : Int, handler : (Context) ->
Response
) -> Unit

Register a per-status exception handler (← FastAPI's add_exception_handler keyed by an HTTP status code). Whenever an error path yields status — a routing 404 / 405, or a raised HttpException (or the fallback 500) — the handler's response replaces the default, so an app can serve a custom error page. Successful handler returns are never rewritten.

App::delete

fn App::delete(self : App, path : String, handler : (Context) ->
Response
raise, summary? : String, tags? : Array[String], deprecated? : Bool, endpoint? : Endpoint?, security? : Array[SecurityRequirement], include_in_schema? : Bool, validate? : Bool) -> Unit

Register a DELETE route.

App::describe

fn App::describe(self : App, title? : String, api_version? : String, description? : String, terms_of_service? : String, contact? : Contact?, license? : License?, servers? : Array[Server]) -> Unit

Set what the OpenAPI document says about this API (← FastAPI's FastAPI(title=…,description=…, contact=…, license_info=…, servers=…)). App::openapi and the /openapi.json route enable_docs registers both read it, so the document a client fetches and the one a test builds cannot describe different APIs.

App::enable_docs

fn App::enable_docs(self : App, openapi_url? : String?, docs_url? : String?, redoc_url? : String?, version? : OpenApiVersion) -> Unit

Serve the app's own documentation, the way FastAPI does out of the box: the OpenAPI document at openapi_url, Swagger UI at docs_url, and ReDoc at redoc_url. Pass None for any of the three to leave it off. The three routes are kept out of the document they serve, so enabling docs does not change the spec a client reads.

It is a call rather than a default because registering routes behind the app's back would surprise anyone mounting this app under a prefix.

App::exception_handler

fn App::exception_handler(self : App, h : (Context, Error) ->
Response
?) -> Unit

Register an exception handler. On a raised error the handlers are tried in registration order; the first to return Some(response) wins. An unhandled error falls through to the built-in mapping — an HttpException becomes its own status / detail, anything else a 500.

App::get

fn App::get(self : App, path : String, handler : (Context) ->
Response
raise, summary? : String, tags? : Array[String], deprecated? : Bool, endpoint? : Endpoint?, security? : Array[SecurityRequirement], include_in_schema? : Bool, validate? : Bool) -> Unit

Register a GET route. A shorthand for route(Get, ...) carrying the same documentation and security arguments.

App::handle

Route a request to its handler through the middleware chain, returning 404 when no path matches and 405 when a path matches but no method does. Any error a handler raises is mapped to a response by the exception handlers. Background tasks a handler scheduled are dropped on this path; use handle_with_background (as to_asgi does) to run them.

App::handle_with_background

Route a request through the middleware chain and return both the response and the background queue the handler filled — the caller (to_asgi, or a test) runs the queue after the response is sent. handle is the plain-response wrapper over this.

App::lifespan_handler

This app's hooks as a moonasgi LifespanHandler — the synchronous core, which is what makes boot and teardown testable on every backend without a server. Mounted apps are included: a mount is part of the composition being started, and nothing else would ever drive its hooks.

App::middleware

Add an outer middleware. Middlewares wrap the router as an onion; the first registered is the outermost (it sees the request first and the response last). cors(...) and gzip(...) are middlewares.

App::mount

fn App::mount(self : App, prefix : String, subapp : App) -> Unit

Mount a sub-application under prefix (← FastAPI's app.mount(prefix, sub)). A request whose path lies under prefix is routed by subapp with the prefix stripped (its own middleware, security, and background tasks apply), and the sub-app's routes appear under prefix in the merged OpenAPI document with its security schemes folded into the parent's.

App::new

fn App::new() -> App

Create an empty application. The verification clock defaults to 0 (Unix epoch); a server sets a real one with App::set_clock, and tests inject a fixed time so token expiry is deterministic.

App::on_shutdown

fn App::on_shutdown(self : App, hook : () -> Unit raise) -> Unit

Run hook when the server shuts down, after the last request has been served (← FastAPI's on_event("shutdown")). Hooks run in reverse registration order, so a resource is released before whatever it was opened from. A hook that raises does not stop the others — shutdown reports the first failure once the rest have run.

App::on_startup

fn App::on_startup(self : App, hook : () -> Unit raise) -> Unit

Run hook when the server starts, before it accepts the first request (← FastAPI's on_event("startup")). Hooks run in the order they were registered; one that raises aborts startup, and the server is told why.

App::openapi

fn App::openapi(self : App, version? : OpenApiVersion, title? : String, api_version? : String, description? : String, terms_of_service? : String, contact? : Contact?, license? : License?, servers? : Array[Server]) -> Json

Build the app's OpenAPI document as a Json value, walking the registered routes — this app's and every mounted sub-app's, each under its prefix — into paths, methods and operations. version picks the dialect: Swagger 2.0, OpenAPI 3.0.3 or 3.1.0 off the identical routes. Routes registered with include_in_schema=false are left out.

App::openapi_json

fn App::openapi_json(self : App, version? : OpenApiVersion) -> String

The app's OpenAPI / Swagger document for version, stringified.

App::patch

fn App::patch(self : App, path : String, handler : (Context) ->
Response
raise, summary? : String, tags? : Array[String], deprecated? : Bool, endpoint? : Endpoint?, security? : Array[SecurityRequirement], include_in_schema? : Bool, validate? : Bool) -> Unit

Register a PATCH route: change part of the addressed resource.

App::post

fn App::post(self : App, path : String, handler : (Context) ->
Response
raise, summary? : String, tags? : Array[String], deprecated? : Bool, endpoint? : Endpoint?, security? : Array[SecurityRequirement], include_in_schema? : Bool, validate? : Bool) -> Unit

Register a POST route — the verb that carries a request body, so this is the one most often given an endpoint describing that body.

App::put

fn App::put(self : App, path : String, handler : (Context) ->
Response
raise, summary? : String, tags? : Array[String], deprecated? : Bool, endpoint? : Endpoint?, security? : Array[SecurityRequirement], include_in_schema? : Bool, validate? : Bool) -> Unit

Register a PUT route: replace the addressed resource wholesale.

App::route

fn App::route(self : App, verb : Method, path : String, handler : (Context) ->
Response
raise, summary? : String, tags? : Array[String], deprecated? : Bool, endpoint? : Endpoint?, security? : Array[SecurityRequirement], include_in_schema? : Bool, validate? : Bool) -> Unit

Register a route for an explicit method. An optional endpoint descriptor makes the route fully typed (its parameters, request body, and responses surface in the OpenAPI document and drive validation); security attaches per-operation requirements (emitted as OpenAPI security and enforced before the handler when their scheme has an enforcer).

App::route_bg

fn App::route_bg(self : App, verb : Method, path : String, handler : (Context, BackgroundTasks) ->
Response
raise, summary? : String, tags? : Array[String], deprecated? : Bool, endpoint? : Endpoint?, security? : Array[SecurityRequirement], include_in_schema? : Bool, validate? : Bool) -> Unit

Register a background-aware route: the handler additionally receives the request's BackgroundTasks queue, whose thunks the app runs after the response is sent (← declaring a BackgroundTasks parameter in FastAPI).

App::secure_oauth2

fn App::secure_oauth2(self : App, name : String, bearer : OAuth2PasswordBearer, scopes? : Array[(String, String)]) -> Unit

Declare an OAuth2 password-bearer scheme under name and wire it as a runtime enforcer. Like add_security_scheme it surfaces the scheme in the OpenAPI document (with the advertised scopes), and additionally registers the guard a route names in its security: the app pulls the bearer token, verifies it against bearer's secret at the app clock's time, and checks the route's required scopes — returning 401/403 before the handler runs.

App::set_clock

fn App::set_clock(self : App, clock : () -> Int64) -> Unit

Set the clock the app reads to verify token expiry when enforcing route security (Unix seconds). A native server passes the wall clock; a test passes a fixed function so expiry is deterministic.

App::to_asgi

fn App::to_asgi(self : App) -> (async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit)

Compile the app to a moonasgi AsgiApp a server can run: drain the request body, route it, and stream the response back over the SEAM.

App::websocket

fn App::websocket(self : App, path : String, handler : (WebSocket) -> Unit) -> Unit

Register a WebSocket route (← FastAPI's @app.websocket(path)). The path matches with the same :name segment rules as HTTP routes.

AuthenticatedUser

pub(all) struct AuthenticatedUser {
subject : String
scopes : Array[String]
claims : Map[String, Json]
}

The identity a verified token carries, injected into a protected handler (← the object FastAPI's get_current_user returns). subject is the sub claim, scopes the granted scopes, and claims the whole verified payload for anything else the handler needs (exp, custom claims).

AuthenticatedUser::has_scope

fn AuthenticatedUser::has_scope(self : AuthenticatedUser, scope : String) -> Bool

Whether this user was granted scope.

BackgroundTasks

pub struct BackgroundTasks {
tasks : Array[() -> Unit]
}

A queue of deferred thunks (← FastAPI's BackgroundTasks). A background-aware route receives one per request, calls add_task to enqueue work, and the app runs the queue — in enqueue order — after the response has been sent.

BackgroundTasks::add_task

fn BackgroundTasks::add_task(self : BackgroundTasks, task : () -> Unit) -> Unit

Enqueue a thunk to run after the response is sent. Tasks run in the order they were added, each after the previous returns (← BackgroundTasks.add_task).

BackgroundTasks::len

fn BackgroundTasks::len(self : BackgroundTasks) -> Int

How many tasks are queued — the app checks this to skip the drain when a route scheduled nothing.

BackgroundTasks::new

An empty task queue.

BackgroundTasks::run

fn BackgroundTasks::run(self : BackgroundTasks) -> Unit

Run every queued task in order, then clear the queue. Called by the app once the response has been handed to the transport, so a task's latency never delays the client. Idempotent: a second call runs nothing.

Constraint

pub(all) enum Constraint {
Minimum(Double)
Maximum(Double)
ExclusiveMinimum(Double)
ExclusiveMaximum(Double)
MultipleOf(Double)
MinLength(Int)
MaxLength(Int)
Pattern(String)
MinItems(Int)
MaxItems(Int)
} derive(Eq)

A value constraint on a field (JSON-Schema keyword ↔ Pydantic argument).

Contact

pub(all) struct Contact {
name : String
url : String
email : String
}

Build the OpenAPI / Swagger document for the app as a Json value, walking the registered routes once into paths → methods → operations. OpenAPI contact info (← FastAPI's contact): every field optional, emitted only when non-empty.

Container

pub(all) struct Container[V] {
providers : Map[String, Provider[V]]
overrides : Map[String, Provider[V]]
}

The provider registry: key -> Provider, plus a separate overrides map that shadows it. Overrides are FastAPI's app.dependency_overrides — a test swaps a real dependency (a live DB session) for a fake without touching the routes. A registered override always wins over the base provider.

Container::clear_override

fn[V] Container::clear_override(self : Container[V], key : String) -> Unit

Drop the override for key (no-op if none), restoring the base provider.

Container::clear_overrides

fn[V] Container::clear_overrides(self : Container[V]) -> Unit

Drop every override — the usual test teardown that returns the container to its production wiring.

Container::new

fn[V] Container::new() -> Container[V]

An empty container.

Container::open_scope

fn[V] Container::open_scope(self : Container[V]) -> Scope[V]

Open a fresh request scope over this container.

Container::override_

fn[V] Container::override_(self : Container[V], key : String, factory : () -> V, teardown? : (V) -> Unit) -> Container[V]

Register a dependency override for key — FastAPI's app.dependency_overrides[dep] = fake. Takes precedence over the base provider until cleared.

Container::provide

fn[V] Container::provide(self : Container[V], key : String, factory : () -> V, teardown? : (V) -> Unit) -> Container[V]

Register a base provider under key (last registration wins), returning the container so registrations can chain.

Container::provide_using

fn[V] Container::provide_using(self : Container[V], key : String, factory : (Scope[V]) -> V, teardown? : (V) -> Unit) -> Container[V]

Register a base provider whose factory resolves sub-dependencies through the request scope it is handed (FastAPI's nested Depends). Otherwise like provide.

Container::run

Run handler inside a fresh request scope, then tear the scope down — the setup/teardown pair wrapped around a handler, exactly as a FastAPI yield dependency brackets the request. The handler resolves whatever it needs through the scope; every dependency built during the call is released (LIFO) once it returns, then the response is handed back.

Context

pub(all) struct Context {
request :
Request

params : Map[String, String]
}

The per-request context handed to a handler: the raw request plus the path parameters extracted from the matched route (:name segments).
fn Context::api_key_cookie(self : Context, name : String) -> String?

The API key carried in the cookie name (← FastAPI's APIKeyCookie), or None.

Context::api_key_header

fn Context::api_key_header(self : Context, name : String) -> String?

The API key carried in the request header name (← FastAPI's APIKeyHeader), or None. Header names are matched against the request's lower-cased headers.

Context::api_key_query

fn Context::api_key_query(self : Context, name : String) -> String?

The API key carried in the query parameter name (← FastAPI's APIKeyQuery), or None.

Context::bearer_token

fn Context::bearer_token(self : Context) -> String?

Pull the bearer token out of the Authorization: Bearer <token> header, None if the header is absent or isn't a bearer credential. The scheme name is matched case-insensitively, as RFC 6750 requires.

Context::body

Deserialise the JSON request body into a user type T, None when the body is absent, is not valid JSON, or does not shape-match T. This is the unchecked, best-effort extractor — the total counterpart of writing item: Item on a FastAPI handler when you don't want the framework's 422. T describes itself with derive(@json.FromJson); the deserialisation is core's, so it stays faithful to the JSON shape without any reflection.

Context::body_json

fn Context::body_json(self : Context) -> Json?

Parse the request body as JSON, returning None for an empty body or one that does not parse — the total counterpart of FastAPI reading a JSON body.

Context::body_validated

fn[T :
FromJson
] Context::body_validated(self : Context, schema : Schema) -> Result[T, Array[ValidationError]]

The validated typed-body extractor — the faithful equivalent of FastAPI declaring a pydantic model parameter: the body is checked against the endpoint's descriptor schema (the same tree that emits the OpenAPI body schema), and only if it conforms is it deserialised into T. On failure it yields the FastAPI-shaped ValidationError list (located under ["body",...]), ready for unprocessable; on success it yields the built T.

Reusing validate_schema here is the point of the descriptor tree: schema emission, request validation, and typed deserialisation are all driven off one source of truth, exactly as pydantic derives all three from one model. Because validation runs first, @json.from_json is reached only for a shape-conforming value; the final catch keeps the extractor total for the residual cases a scalar schema cannot express (e.g. an out-of-range integer).

Context::cookie

fn Context::cookie(self : Context, name : String) -> String?

Look up a cookie by name from the request Cookie header, which is a ; -separated list of key=value pairs. Surrounding spaces are trimmed; None if there is no Cookie header or the name is absent.

Context::form

fn Context::form(self : Context) -> FormData

The parsed request form — FastAPI's Form(...) / File(...) parameters. Dispatches on the Content-Type: a multipart/form-data body is split on its boundary into fields and files, an application/x-www-form-urlencoded body is decoded into fields. Any other (or absent) content type yields an empty form rather than raising, so the extractor stays total.

Context::http_basic

fn Context::http_basic(self : Context) -> HttpBasicCredentials?

The HTTP Basic credentials on this request (← FastAPI's HTTPBasic dependency), or None.

Context::json_field

fn Context::json_field(self : Context, name : String) -> Json?

Pull a single field out of a JSON object body by name, None if the body is absent, not an object, or lacks the field.

Context::oauth2_password_form

fn Context::oauth2_password_form(self : Context) -> OAuth2PasswordRequestForm?

Read an OAuth2PasswordRequestForm off the request's urlencoded (or multipart) body. None when neither username nor password is present — the body isn't a password-grant form at all.

Context::param

fn Context::param(self : Context, name : String) -> String?

Look up a path parameter by name.

Context::query

fn Context::query(self : Context, name : String) -> String?

Look up a query-string parameter by name, e.g. ?limit=10&q=cat%20dog. Keys and values are percent/plus-decoded, so q above reads back as cat dog. When a key repeats, the first occurrence wins; use query_all to read every one.

Context::query_all

fn Context::query_all(self : Context, name : String) -> Array[String]

Every value given for name, in the order they appear — ?tag=a&tag=b reads back as ["a", "b"]. Empty when the key is absent.

CorsConfig

pub(all) struct CorsConfig {
allow_origins : Array[String]
allow_all_origins : Bool
allow_methods : Array[String]
allow_headers : Array[String]
allow_all_headers : Bool
allow_credentials : Bool
expose_headers : Array[String]
max_age : Int
}

CORS policy (← Starlette's CORSMiddleware). Origins, methods, and headers are allow-lists; the *_all flags open a dimension wholesale. Per the Fetch standard, allow_credentials forbids the * wildcard in the reflected Access-Control-Allow-Origin, so with credentials the request origin is echoed back instead.

Dep

pub(all) enum Dep {
Greeting(String)
} derive(Eq)

The dependency value type of the greet app. A sum type wrapping every dependency this app injects — the explicit, exhaustive stand-in for FastAPI resolving heterogeneous Depends values dynamically.

EcdsaPrivateKey

pub(all) struct EcdsaPrivateKey {
d :
BigInt

}

An ECDSA P-256 private key: the scalar d. The ES256 signing key.

EcdsaPrivateKey::from_hex

fn EcdsaPrivateKey::from_hex(d_hex : String) -> EcdsaPrivateKey

Build a P-256 private key from its hex-encoded scalar.

EcdsaPrivateKey::public_key

The public key Q = d·G for this private key.

EcdsaPublicKey

An ECDSA P-256 public key: the curve point (x, y). The ES256 verification key.

EcdsaPublicKey::from_hex

fn EcdsaPublicKey::from_hex(x_hex : String, y_hex : String) -> EcdsaPublicKey

Build a P-256 public key from the hex-encoded affine coordinates — e.g. the two halves of openssl's uncompressed pub point after its 04 prefix.

Ed25519PrivateKey

pub(all) struct Ed25519PrivateKey {
seed : Bytes
}

An Ed25519 (EdDSA) private key: the 32-byte secret seed. The signing key for the JWT EdDSA algorithm (RFC 8037).

Ed25519PrivateKey::from_hex

fn Ed25519PrivateKey::from_hex(hex : String) -> Ed25519PrivateKey

Build an Ed25519 private key from its 32-byte seed in hex.

Ed25519PrivateKey::public_key

The public key matching this private key: A = [s]B.

Ed25519PublicKey

pub(all) struct Ed25519PublicKey {
key : Bytes
}

An Ed25519 (EdDSA) public key: the 32-byte compressed point. The verification key for the JWT EdDSA algorithm (RFC 8037).

Ed25519PublicKey::from_hex

fn Ed25519PublicKey::from_hex(hex : String) -> Ed25519PublicKey

Build an Ed25519 public key from its 32-byte hex encoding.

Endpoint

pub(all) struct Endpoint {
params : Array[Param]
request_body : Schema?
request_required : Bool
responses : Array[ResponseSpec]
} derive(Eq)

The runtime endpoint descriptor a route can carry — the one first-class value that replaces FastAPI reading a handler's signature. Walked once for the OpenAPI operation (parameters + requestBody + responses, with every named object hoisted into components/schemas) and for request validation.

Endpoint::new

fn Endpoint::new(params? : Array[Param], request_body? : Schema?, request_required? : Bool, responses? : Array[ResponseSpec]) -> Endpoint

Build an endpoint descriptor. Everything is optional: a bare Endpoint::new() describes an endpoint with no parameters, no body, and (on emit) a default 200 OK response.

Endpoint::validate

fn Endpoint::validate(self : Endpoint, ctx : Context) -> Array[ValidationError]

Validate an inbound request ctx against this endpoint descriptor: every declared parameter (path / query / header / cookie) plus the JSON request body, all off the same descriptor tree that emits the OpenAPI operation. Returns the accumulated errors — an empty array means the request conforms, otherwise pass them to unprocessable for a FastAPI-shaped 422.

Field

pub(all) struct Field {
name : String
schema : Schema
required : Bool
description : String
constraints : Array[Constraint]
} derive(Eq)

One field of an object schema: its name, its schema, whether it is required, and an optional description.

Field::new

fn Field::new(name : String, schema : Schema, required? : Bool, description? : String, constraints? : Array[Constraint]) -> Field

Build a field. required defaults to true (FastAPI treats a field without a default as required). constraints are the Pydantic-style value constraints (ge/le/min_length/pattern/…) that are both emitted into the field's OpenAPI schema and enforced when validating an inbound value.

FormData

pub(all) struct FormData {
fields : Array[FormField]
files : Array[UploadFile]
} derive(Eq)

A parsed form: its plain fields and its uploaded files, in body order. A multipart part is a file when its Content-Disposition carries a filename; otherwise it's a field. An urlencoded body only ever yields fields.

FormData::field

fn FormData::field(self : FormData, name : String) -> String?

The value of the first field named name, None if absent — the common Form(...) lookup.

FormData::field_all

fn FormData::field_all(self : FormData, name : String) -> Array[String]

Every value submitted under name, in order — an HTML form can repeat a field (checkbox groups, multi-selects), and FastAPI surfaces those as a list.

FormData::file

fn FormData::file(self : FormData, name : String) -> UploadFile?

The first uploaded file under name, None if absent — the File(...) lookup.

FormField

pub(all) struct FormField {
name : String
value : String
} derive(Eq)

A plain form field: a name and its decoded text value.

GreetReq

pub(all) struct GreetReq {
name : String
} derive(Eq, ToJson,
FromJson
)

The request body of POST /greet. derive(@json.FromJson) lets Context::body_validated build it after the descriptor accepts the payload; its schema and struct fields agree (both require name), so a schema-valid body always deserialises.

GreetReq::schema

fn GreetReq::schema() -> Schema

The GreetReq descriptor — one required string field.

HttpBasicCredentials

pub(all) struct HttpBasicCredentials {
username : String
password : String
}

The credentials carried in an HTTP Basic Authorization header (← FastAPI's HTTPBasicCredentials): the username and password from base64(username:password).

License

pub(all) struct License {
name : String
url : String
}

OpenAPI license info (← FastAPI's license_info): name required, url optional.

Method

pub(all) enum Method {
Get
Post
Put
Patch
Delete
Head
Options
} derive(Eq)

HTTP methods a moonapi route can bind to.

NewUser

pub(all) struct NewUser {
name : String
email : String
age : Int
} derive(Eq, ToJson)

The demo request model — the body of POST /users. age is optional.
impl ToSchema for NewUser

NewUser::schema

fn NewUser::schema() -> Schema

The NewUser descriptor.

OAuth2PasswordBearer

pub(all) struct OAuth2PasswordBearer {
token_url : String
secret : String
}

An OAuth2 password-bearer security scheme (← FastAPI's OAuth2PasswordBearer). Holds the token_url (the endpoint that issues tokens, surfaced to API docs) and the shared HS256 secret used to verify presented tokens.

OAuth2PasswordBearer::authenticate

fn OAuth2PasswordBearer::authenticate(self : OAuth2PasswordBearer, ctx : Context, now_secs : Int64, scopes? : Array[String]) -> Result[AuthenticatedUser,
Response
]

Authenticate a request against this scheme and enforce scopes (← FastAPI's Security(get_current_user, scopes=[...])). On success returns the AuthenticatedUser; otherwise the response to return:

  • no bearer token -> 401 {"detail":"Not authenticated"}
  • malformed / bad-signature / expired / not-yet-valid token -> 401 {"detail":"Could not validate credentials"}
  • valid token missing a required scope -> 403 {"detail":"Not enough permissions"}

now_secs is the verification time (Unix seconds), passed in so the check stays pure and testable on every backend.

OAuth2PasswordBearer::new

fn OAuth2PasswordBearer::new(token_url : String, secret : String) -> OAuth2PasswordBearer

Build a password-bearer scheme pointing at the token endpoint at token_url, verifying tokens with secret.

OAuth2PasswordBearer::scheme

fn OAuth2PasswordBearer::scheme(self : OAuth2PasswordBearer, scopes? : Array[(String, String)]) -> SecurityScheme

Build the security scheme that describes this password-bearer flow (← the object FastAPI derives from OAuth2PasswordBearer). scopes are the (name, description) pairs advertised in the OpenAPI document.

OAuth2PasswordRequestForm

pub(all) struct OAuth2PasswordRequestForm {
grant_type : String
username : String
password : String
scopes : Array[String]
client_id : String?
client_secret : String?
} derive(Eq)

The parsed OAuth2 password-grant form (← FastAPI's OAuth2PasswordRequestForm). The token endpoint reads username / password to authenticate and scopes (the space-delimited scope field, split into a list) to stamp into the issued token. grant_type is "password" for this flow; client_id / client_secret are optional confidential-client credentials.

ObjectSchema

pub(all) struct ObjectSchema {
name : String
fields : Array[Field]
description : String
} derive(Eq)

The body of an object schema: its component name (empty = an anonymous inline object; non-empty = hoisted to components/schemas and referenced), its ordered fields, and an optional description.

OpenApiVersion

pub(all) enum OpenApiVersion {
Swagger20
OpenApi30
OpenApi31
} derive(Eq)

Target OpenAPI / Swagger document version. moonapi emits every mainstream version from the same route descriptors — a "good FastAPI" is not pinned to one spec version.

Param

pub(all) struct Param {
name : String
loc : ParamLoc
schema : Schema
required : Bool
description : String
} derive(Eq)

A single request parameter descriptor: its name, location, scalar schema, whether it is required, and an optional description. Path parameters are always required (OpenAPI requires it); the constructor keeps the caller's value but validation treats path params as mandatory.

Param::new

fn Param::new(name : String, loc : ParamLoc, schema? : Schema, required? : Bool, description? : String) -> Param

Build a parameter descriptor. schema defaults to a string and required to true.

ParamLoc

pub(all) enum ParamLoc {
InPath
InQuery
InHeader
InCookie
} derive(Eq)

Where a parameter is carried, the OpenAPI in locations: the path, the query string, a header, or a cookie.

Provider

pub(all) struct Provider[V] {
factory : (Scope[V]) -> V
teardown : (V) -> Unit
}

A provider: a keyed factory that builds a request-scoped dependency value, with an optional teardown run after the handler (FastAPI's yield dependencies, whose post-yield body is cleanup). The factory runs at most once per request scope; the teardown receives the produced value. The factory is handed the Scope so it can resolve sub-dependencies through it — FastAPI's Depends(a) where a itself declares Depends(b).

Provider::new

fn[V] Provider::new(factory : () -> V, teardown? : (V) -> Unit) -> Provider[V]

Build a leaf provider whose factory needs nothing else. teardown defaults to a no-op — the common "plain value, nothing to release" case.

Provider::scoped

fn[V] Provider::scoped(factory : (Scope[V]) -> V, teardown? : (V) -> Unit) -> Provider[V]

Build a provider whose factory resolves other dependencies through the request Scope it is handed — the sub-dependency case (FastAPI's nested Depends).

ResponseSpec

pub(all) struct ResponseSpec {
status : Int
description : String
body : Schema?
} derive(Eq)

A single response descriptor: the HTTP status, a human description, and an optional body schema (None for an empty body, e.g. 204).

ResponseSpec::new

fn ResponseSpec::new(status : Int, description? : String, body? : Schema?) -> ResponseSpec

Build a response descriptor. description defaults to "OK" and there is no body unless one is given.

Route

type Route

A route stores one background-aware handler; a plain ApiHandler is wrapped to ignore the queue. security is the per-operation requirements (emitted as OpenAPI security and enforced before the handler when their scheme has an enforcer).

RsaPrivateKey

An RSA private key: modulus n, public exponent e, and private exponent d (the CRT parameters are not needed for the plain m^d mod n path). The RS256 signing key.

RsaPrivateKey::from_hex

fn RsaPrivateKey::from_hex(n_hex : String, e_hex : String, d_hex : String) -> RsaPrivateKey

Build a private key from hex-encoded modulus, public exponent, and private exponent.

RsaPrivateKey::public_key

fn RsaPrivateKey::public_key(self : RsaPrivateKey) -> RsaPublicKey

The public half of a private key — for verifying what it signs.

RsaPublicKey

An RSA public key: modulus n and public exponent e (RFC 8017). The RS256 verification key. Core's BigInt carries the modular arithmetic, so the signature scheme is a straight transcription of PKCS#1 v1.5 with no vendored C.

RsaPublicKey::from_hex

fn RsaPublicKey::from_hex(n_hex : String, e_hex : String) -> RsaPublicKey

Build a public key from hex-encoded modulus and exponent — e.g. openssl's rsa -modulus output and 10001.

Schema

pub(all) enum Schema {
SStr
SInt
SFloat
SBool
SNull
SArray(Schema)
SObject(ObjectSchema)
SEnum(Schema, Array[Json])
} derive(Eq)

A JSON-Schema type descriptor — the runtime, first-class value that stands in for FastAPI's from-signature reflection. One Schema tree is walked once to (a) emit a complete OpenAPI request/response body schema — objects, arrays and scalars, with required, and named objects hoisted under components/schemas and referenced by $ref — and (b) drive validation of an inbound JSON value. This is the explicit, MoonBit-idiomatic equivalent of pydantic's type-driven magic (cf. Rust serde + macros, Go struct tags + codegen). A named object carries its fields inline, so the same tree is fully self-describing for validation; the name is used only to deduplicate it into components on emit.

Schema::array

fn Schema::array(item : Schema) -> Schema

An array schema whose elements all conform to item.

Schema::object

fn Schema::object(name : String, fields : Array[Field]) -> Schema

A named object schema: hoisted to components/schemas under name and referenced by $ref wherever it is used.

Scope

pub struct Scope[V] {
container : Container[V]
cache : Map[String, V]
building : Map[String, Bool]
teardowns : Array[() -> Unit]
}

A request-scoped resolution scope. Each dependency is built at most once and its value cached for the life of the scope (FastAPI's per-request dependency cache), and each built value's teardown is recorded to run — in reverse registration order (LIFO) — when the scope closes. Open one per request, resolve dependencies through it, then close it (or use Container::run).

Scope::close

fn[V] Scope::close(self : Scope[V]) -> Unit

Run every recorded teardown in LIFO order and clear them, so a closed scope is inert. Mirrors FastAPI unwinding yield dependencies in reverse — the last opened is torn down first.

Scope::get

fn[V] Scope::get(self : Scope[V], key : String) -> V?

Resolve key within this scope: return the already-built instance if the dependency was resolved earlier in the same request; otherwise run its factory once, cache the value, register its teardown, and return it. None when no provider (or override) is registered for key.

SecurityRequirement

pub(all) struct SecurityRequirement {
scheme : String
scopes : Array[String]
} derive(Eq)

A security requirement on a route: the scheme name (which must match a name declared with App::add_security_scheme / App::secure_oauth2) and the scopes the caller must hold. Emitted as one {scheme: [scopes]} entry of the operation's OpenAPI security array.

SecurityRequirement::new

fn SecurityRequirement::new(scheme : String, scopes? : Array[String]) -> SecurityRequirement

Require scheme with the given scopes (default: none — authentication with no scope check). App::get(..., security=[SecurityRequirement::new("OAuth2",scopes=["items"])]) reads like FastAPI's Security(oauth2, scopes=["items"]).

SecurityScheme

pub(all) enum SecurityScheme {
OAuth2Password(token_url~ : String, scopes~ : Array[(String, String)])
HttpBearer(bearer_format~ : String)
ApiKeyHeader(name~ : String)
ApiKeyQuery(name~ : String)
ApiKeyCookie(name~ : String)
HttpBasic
}

A security scheme describing how a client authenticates. Mirrors the OpenAPI scheme types: an OAuth2 password flow (its token URL and named scopes), an HTTP bearer scheme (with a bearerFormat such as JWT), an API key in a header / query / cookie, and HTTP Basic.

Server

pub(all) struct Server {
url : String
description : String
}

A servers entry (← FastAPI's servers): a base URL and an optional description.

ServerSentEvent

pub(all) struct ServerSentEvent {
data : String
event : String?
id : String?
retry : Int?
comment : String?
}

One Server-Sent Event. data is the payload (multi-line data is split into several data: lines per the spec); event names the type an EventSource.addEventListener binds to; id sets lastEventId for reconnection; retry is the client's reconnection delay in milliseconds; comment emits :-prefixed lines (a keep-alive ping carries only this).

ServerSentEvent::data

fn ServerSentEvent::data(data : String) -> ServerSentEvent

A data-only event — the common case (data: ...\n\n).

ServerSentEvent::encode

fn ServerSentEvent::encode(self : ServerSentEvent) -> String

Encode this event as its wire frame: optional comment / id / event / retry fields, then one data: line per line of data, terminated by the blank line that dispatches the event.

ServerSentEvent::keep_alive

fn ServerSentEvent::keep_alive(comment? : String) -> ServerSentEvent

A comment-only keep-alive frame (: <text>\n\n) — no event is dispatched, but the bytes keep the connection warm through proxies.

ServerSentEvent::new

fn ServerSentEvent::new(data : String, event? : String?, id? : String?, retry? : Int?, comment? : String?) -> ServerSentEvent

A fully-specified event. Any field left None is omitted from the frame.

UploadFile

pub(all) struct UploadFile {
name : String
filename : String
content_type : String
content : Bytes
} derive(Eq)

An uploaded file from a multipart part: the form-field name it came under, the client's filename, its declared content_type (empty when the part carried no Content-Type), and the raw content bytes exactly as received.

User

pub(all) struct User {
id : Int
name : String
address : Address
tags : Array[String]
} derive(Eq, ToJson)

The demo response model — returned by both user routes. Nests Address and carries an array of tags, so its emitted schema exercises objects, $refs, and arrays together.
impl ToSchema for User

User::schema

fn User::schema() -> Schema

The User descriptor.

ValidationError

pub(all) struct ValidationError {
loc : Array[String]
msg : String
kind : String
}

One entry in a 422 response's detail array, mirroring FastAPI / pydantic v2: where the error is (loc, e.g. ["query", "q"]), a human msg, and a machine kind (serialised as the JSON key type).

ValidationError::missing

fn ValidationError::missing(loc : Array[String]) -> ValidationError

The canonical "a required parameter was not supplied" error located at loc, matching FastAPI's {"type": "missing", "msg": "Field required"}.

ValidationError::type_error

fn ValidationError::type_error(loc : Array[String], kind : String, msg : String) -> ValidationError

A type/parse error located at loc, e.g. kind = "int_parsing" with the matching pydantic message — the shape FastAPI reports for a value of the wrong type.

WebSocket

pub struct WebSocket {
inbox : Array[WsMessage]
cursor : Int
params : Map[String, String]
subprotocols : Array[String]
accepted : Bool
closed : Bool
outbox : Array[
Event
]
}

The handler's view of a WebSocket connection. It reads client frames off an inbound queue and records its own actions (accept / send / close) into an outbound event log the transport replays. params are the matched :name path segments, as with an HTTP Context.

WebSocket::accept

fn WebSocket::accept(self : WebSocket, subprotocol? : String?, headers? : Array[(String, String)]) -> Unit

Accept the handshake (← await websocket.accept()), optionally selecting a subprotocol and adding response headers. Idempotent: a second call is a no-op, so accept-once handlers stay simple.

WebSocket::close

fn WebSocket::close(self : WebSocket, code? : Int, reason? : String) -> Unit

Close the connection with a status code (default 1000, normal closure) and reason. Idempotent.

WebSocket::offered_subprotocols

fn WebSocket::offered_subprotocols(self : WebSocket) -> Array[String]

The subprotocols the client offered (the Sec-WebSocket-Protocol list).

WebSocket::param

fn WebSocket::param(self : WebSocket, name : String) -> String?

Look up a matched path parameter by name.

WebSocket::receive

fn WebSocket::receive(self : WebSocket) -> WsMessage?

Pull the next client frame, None once the client has sent them all (the disconnect). The receive a handler loops on.

WebSocket::receive_bytes

fn WebSocket::receive_bytes(self : WebSocket) -> Bytes?

The next client frame as bytes: Some(b) for a binary frame, None on a text frame or the disconnect.

WebSocket::receive_text

fn WebSocket::receive_text(self : WebSocket) -> String?

The next client frame as text: Some(s) for a text frame, None on a binary frame or the disconnect (← await websocket.receive_text()).

WebSocket::send_bytes

fn WebSocket::send_bytes(self : WebSocket, bytes : Bytes) -> Unit

Send a binary frame to the client.

WebSocket::send_text

fn WebSocket::send_text(self : WebSocket, text : String) -> Unit

Send a text frame to the client (← await websocket.send_text(...)).

WebSocket::sent

The events the handler emitted, in order — the transcript a test asserts on.

WsMessage

pub(all) enum WsMessage {
WsText(String)
WsBinary(Bytes)
} derive(Eq)

One inbound WebSocket message: a text frame or a binary frame.

WsRoute

type WsRoute

base64url_decode

fn base64url_decode(s : String) -> Bytes

Decode a base64url string (padding optional) back to bytes, remapping -/_ to +// first. Lenient about missing padding, the way JWT segments are written.

base64url_encode

fn base64url_encode(data : Bytes) -> String

base64url encoding (RFC 4648 §5, no padding): standard base64 with +// remapped to -/_ and trailing = dropped — the alphabet JWT uses for its header, payload, and signature segments.

check_constraints

fn check_constraints(value : Json, constraints : Array[Constraint], loc : Array[String], errs : Array[ValidationError]) -> Unit

Enforce the constraints on an inbound value, appending a Pydantic-shaped ValidationError (located at loc) for each violation. A constraint that does not apply to the value's kind (a length bound on a number, say) is simply skipped, as pydantic does.

constant_time_eq

fn constant_time_eq(a : Bytes, b : Bytes) -> Bool

A constant-time byte-string equality: it inspects every byte of both inputs regardless of where they first differ, so an attacker can't recover a valid signature byte-by-byte from response timing. Unequal lengths return false at once (length isn't secret). Used to compare JWT signatures.

cors

fn cors(allow_origins? : Array[String], allow_all_origins? : Bool, allow_methods? : Array[String], allow_headers? : Array[String], allow_all_headers? : Bool, allow_credentials? : Bool, expose_headers? : Array[String], max_age? : Int) -> (((
Request
) ->
Response
) -> ((
Request
) ->
Response
))

A CORS middleware for the given policy. It answers preflight OPTIONS requests (those carrying Access-Control-Request-Method) directly with a 204 and the negotiated Access-Control-* headers, and decorates every other cross-origin response with Access-Control-Allow-Origin (plus Vary: Origin, exposed headers, and the credentials flag). A request with no Origin, or one from a disallowed origin, passes through untouched.

create_access_token

fn create_access_token(subject : String, secret : String, now_secs : Int64, scopes? : Array[String], expires_in_secs? : Int64, extra? : Map[String, Json]) -> String

Mint an HS256 access token for subject (← FastAPI's create_access_token). Stamps sub, iat (= now_secs), exp (= now_secs + expires_in_secs), and, when non-empty, scopes; extra merges in any further claims. Times are Unix seconds. secret is the shared HS256 key.

demo_app

fn demo_app() -> App

A demo application exercising the descriptor tree end to end: POST /users takes a typed NewUser body and returns a User; GET /users/:id takes a typed integer path param and returns a User. Both validate off their descriptor and both surface fully-typed bodies (with components/schemas $refs) in openapi.json.

drive_websocket

fn drive_websocket(handler : (WebSocket) -> Unit, inbound : Array[WsMessage], params? : Map[String, String], subprotocols? : Array[String]) -> Array[
Event
]

Run a WebSocket handler against an in-memory frame queue and return the events it emitted — the synchronous test driver (the WS half of a TestClient). Feed the client's frames as inbound; get back the handler's accept / send / close sequence.

ecdsa_p256_sha256_sign

fn ecdsa_p256_sha256_sign(msg : Bytes, key : EcdsaPrivateKey) -> Bytes

ECDSA P-256 sign with SHA-256 — the ES256 signing primitive (FIPS 186-4 §6.4.1) with the deterministic nonce of RFC 6979. Returns the raw r || s (two 32-byte big-endian integers), the encoding JWT uses. Deterministic, so the same message and key always produce the same signature.

ecdsa_p256_sha256_verify

fn ecdsa_p256_sha256_verify(msg : Bytes, sig : Bytes, key : EcdsaPublicKey) -> Bool

ECDSA P-256 verify with SHA-256 — the ES256 primitive (FIPS 186-4 §6.4.2). sig is the raw r || s (two 32-byte big-endian integers), the encoding JWT uses (not ASN.1 DER). Returns whether the signature is valid for msg under key: r, s in range, then u1·G + u2·Q has x-coordinate ≡ r (mod n).

ed25519_public_from_seed

fn ed25519_public_from_seed(seed : Bytes) -> Bytes

Derive the 32-byte compressed public key A = [s]B from a 32-byte Ed25519 seed.

ed25519_sign

fn ed25519_sign(seed : Bytes, msg : Bytes) -> Bytes

Ed25519 signing (RFC 8032 §5.1.6), deterministic. seed is the 32-byte secret key. Returns the 64-byte R || S signature: r = SHA-512(prefix || M)mod l, R = [r]B, k = SHA-512(R || A || M) mod l, S = (r + k·s) mod l.

ed25519_verify

fn ed25519_verify(pub_key : Bytes, msg : Bytes, sig : Bytes) -> Bool

Ed25519 signature verification (RFC 8032 §5.1.7). sig is the 64-byte R || S, pub_key the 32-byte compressed public point. Checks [S]B = R +[k]A with k = SHA-512(R || A || M) mod l.

filter_response

fn filter_response(model : Schema, value : Json) -> Result[Json, Array[ValidationError]]

Validate value against model and, if it conforms, return it filtered down to the model's declared fields (extras dropped, nested objects and arrays projected too). On a mismatch return the located ValidationErrors under ["response"] — the same shape request validation produces. This is FastAPI's response_model: the outgoing shape is the model, not whatever the handler happened to build.

greet_app

fn greet_app(container : Container[Dep]) -> App

Build the greet application over a caller-supplied dependency container, so a test can register dependency_overrides on the same container before or between requests. POST /greet resolves the "greeting" dependency, reads a validated GreetReq body, and answers {"message": "<greeting>, <name>"}; a malformed body gets a FastAPI-shaped 422. The dependency scope brackets each request, so any yield teardown runs once the handler returns.

gzip

A GZip middleware: responses at least min_size bytes are re-encoded as gzip when the client sent Accept-Encoding: gzip and the response isn't already content-encoded. Sets Content-Encoding: gzip, updates Content-Length, and adds Vary: Accept-Encoding.

The gzip stream is a complete RFC 1952 container — correct header, CRC-32, and ISIZE — around a real RFC 1951 DEFLATE payload: LZ77 back-references coded with the fixed Huffman table (deflate.mbt), so the body actually shrinks. Dynamic Huffman would tighten the ratio further and is the documented next step.

hmac_sha256

fn hmac_sha256(key : Bytes, msg : Bytes) -> Bytes

HMAC-SHA256 (RFC 2104): a keyed MAC over sha256. A key longer than the 64-byte block is hashed first; a shorter key is zero-padded. The message is authenticated as H((K ⊕ opad) ∥ H((K ⊕ ipad) ∥ msg)). Checked against RFC 4231 test case 2. This is the signature function behind JWT HS256.

html

fn html(status : Int, body : String) ->
Response

An HTML response — what the documentation pages are served as.

http_error

fn http_error(status : Int, detail : String, headers? : Array[(String, String)]) -> HttpException

Build an HttpException with a string detail — the common case — and optional extra headers. raise http_error(404, "Item not found") reads like FastAPI's raise HTTPException(404, "Item not found").

inflate

fn inflate(data : Bytes) -> Bytes

Decode a raw DEFLATE stream (stored and fixed-Huffman blocks). Used to round-trip deflate_encode in the tests; back-references copy byte-by-byte so overlapping (run-length) matches inflate correctly.

json

fn json(status : Int, value : Json) ->
Response

A JSON response serialised from a Json value.

json_model

fn json_model(status : Int, model : Schema, value : Json) ->
Response

A JSON response whose body is value filtered through model. On success a status response carrying only the model's declared fields; on a model mismatch a 500 whose body lists the response-validation errors — the return value didn't match what the route promised, which is a server-side fault.

jwt_sign

fn jwt_sign(claims : Map[String, Json], secret : String) -> String

Sign a claims set as a compact JWT using HS256. The header is fixed to {"alg":"HS256","typ":"JWT"}; claims is serialised as the JSON payload (exp / iat / nbf / sub / scopes go in as ordinary entries); secret is the shared HS256 key. Returns header.payload.signature, each segment base64url-encoded.

jwt_sign_eddsa

fn jwt_sign_eddsa(claims : Map[String, Json], key : Ed25519PrivateKey) -> String

Sign a claims set as a compact JWT using EdDSA (Ed25519, RFC 8037). The header is fixed to {"alg":"EdDSA","typ":"JWT"}; key is the Ed25519 private key. Returns header.payload.signature, each segment base64url-encoded.

jwt_sign_es256

fn jwt_sign_es256(claims : Map[String, Json], key : EcdsaPrivateKey) -> String

Sign a claims set as a compact JWT using ES256 (ECDSA P-256 / SHA-256 with the deterministic RFC 6979 nonce). The header is fixed to {"alg":"ES256","typ":"JWT"}; key is the P-256 private key. Returns header.payload.signature, each segment base64url-encoded.

jwt_sign_rs256

fn jwt_sign_rs256(claims : Map[String, Json], key : RsaPrivateKey) -> String

Sign a claims set as a compact JWT using RS256 (RSASSA-PKCS1-v1_5 + SHA-256). The header is fixed to {"alg":"RS256","typ":"JWT"}; key is the RSA private key. Returns header.payload.signature, each segment base64url-encoded.

jwt_verify

fn jwt_verify(token : String, secret : String, now_secs : Int64) -> Map[String, Json] raise JwtError

Verify a compact HS256 JWT and return its claims. Checks, in order: three segments; header alg is HS256; the HMAC-SHA256 signature matches (compared in constant time); exp (if present) is strictly after now_secs; nbf (if present) is at or before now_secs. now_secs is the verification time as a Unix timestamp in seconds (JWT NumericDate). Raises the matching JwtError on any failure; a tampered payload or signature fails at BadSignature.

jwt_verify_eddsa

fn jwt_verify_eddsa(token : String, key : Ed25519PublicKey, now_secs : Int64) -> Map[String, Json] raise JwtError

Verify a compact EdDSA (Ed25519) JWT and return its claims (RFC 8037). Checks three segments; the header alg is EdDSA; the Ed25519 signature verifies against key; and the exp / nbf time claims. Raises the matching JwtError; a tampered payload or signature fails at BadSignature.

jwt_verify_es256

fn jwt_verify_es256(token : String, key : EcdsaPublicKey, now_secs : Int64) -> Map[String, Json] raise JwtError

Verify a compact ES256 JWT and return its claims. Checks three segments; the header alg is ES256; the ECDSA-P256 / SHA-256 signature (raw r || s, the JWS encoding) verifies against key; and the exp / nbf time claims. Raises the matching JwtError; a tampered payload or signature fails at BadSignature.

jwt_verify_rs256

fn jwt_verify_rs256(token : String, key : RsaPublicKey, now_secs : Int64) -> Map[String, Json] raise JwtError

Verify a compact RS256 JWT and return its claims. Checks three segments; the header alg is RS256; the RSASSA-PKCS1-v1_5 signature verifies against key; and the exp / nbf time claims. Raises the matching JwtError; a tampered payload or signature fails at BadSignature.

oauth2_app

fn oauth2_app(now : () -> Int64, secret? : String) -> App

Build the OAuth2 demo application. secret is the shared HS256 key; now supplies the current Unix time (seconds) for both issuing and verifying, so a caller controls time in tests. Tokens live for one hour.

parse_basic_auth

fn parse_basic_auth(header : String) -> HttpBasicCredentials?

Parse an HTTP Basic Authorization header value into credentials (← FastAPI's HTTPBasic), or None. Matches the Basic scheme case-insensitively (RFC 7617), base64-decodes the rest, and splits on the first colon so a password may itself contain colons.

redoc_ui

fn redoc_ui(spec_url? : String, title? : String) -> String

A self-contained ReDoc page rendering the document served at spec_url — the second reading of the same spec FastAPI serves at /redoc, three-panel and built for reading rather than for trying calls out.

rsa_pkcs1_sha256_sign

fn rsa_pkcs1_sha256_sign(msg : Bytes, key : RsaPrivateKey) -> Bytes

RSASSA-PKCS1-v1_5 sign with SHA-256 — the RS256 signing primitive (RFC 8017 §8.2.1): s = EM^d mod n, returned as a fixed-width big-endian octet string.

rsa_pkcs1_sha256_verify

fn rsa_pkcs1_sha256_verify(msg : Bytes, sig : Bytes, key : RsaPublicKey) -> Bool

RSASSA-PKCS1-v1_5 verify with SHA-256 — the RS256 verification primitive (RFC 8017 §8.2.2): recover m = s^e mod n and compare it, in constant time, to the expected EMSA-PKCS1-v1_5 encoding of msg. Rejects a signature that is not the modulus width or is >= n.

schema_of

fn[T : ToSchema] schema_of(x : T) -> Schema

The schema of any ToSchema type, without needing a value of it materialised at the call site beyond the one handed in — the generic entry point.

sha256

fn sha256(msg : Bytes) -> Bytes

SHA-256 (FIPS 180-4): hash an arbitrary byte string to a 32-byte digest. The full message schedule and 64-round compression over 512-bit blocks with the standard length-padding, checked against the NIST vectors for "" and "abc". The building block for hmac_sha256, and through it JWT HS256.

sha512

fn sha512(msg : Bytes) -> Bytes

SHA-512 (FIPS 180-4). Core ships no such hash, so it is hand-written like sha256 but over 64-bit words with 80 rounds. It is the digest Ed25519 signs over. Messages here are far under 2^64 bits, so the 128-bit length field's high half is always zero.

sse_response

fn sse_response(events : Array[ServerSentEvent], status? : Int, headers? : Array[(String, String)]) ->
Response

A text/event-stream response whose body is the framed events. Sets Cache-Control: no-cache and Connection: keep-alive, the headers an SSE endpoint sends so intermediaries don't buffer or close the stream.

swagger_ui

fn swagger_ui(spec_url? : String, title? : String) -> String

A self-contained Swagger UI page rendering the document served at spec_url.

text

fn text(status : Int, body : String) ->
Response

A plain-text response.

token_response

fn token_response(access_token : String) ->
Response

The 200 token response body {"access_token": ..., "token_type": "bearer"} — the OAuth2 password-grant reply FastAPI's token endpoint returns.

unprocessable

A 422 Unprocessable Entity response whose application/json body lists the validation errors, exactly as FastAPI reports a failed request.

validate_schema

fn validate_schema(schema : Schema, value : Json, loc : Array[String], errs : Array[ValidationError]) -> Unit

Validate a JSON value against the descriptor schema, appending FastAPI-shaped errors to errs (located at loc). This is what "the descriptor drives validation" means: the very tree that emits the OpenAPI body schema also decides whether an inbound body conforms — one source of truth, exactly as pydantic derives both from one model. A named object is validated against its inline fields, so no $ref resolution is needed here.

validation_error_body

fn validation_error_body(errors : Array[ValidationError]) -> Json

The {"detail": [ ... ]} body FastAPI returns when request validation fails, built from a list of ValidationErrors.

with_constraints

fn with_constraints(j : Json, constraints : Array[Constraint], version : OpenApiVersion) -> Json

Merge the constraints into a scalar/array schema object for OpenAPI emission. exclusiveMinimum / exclusiveMaximum are numeric under OpenAPI 3.1 (JSON-Schema 2020-12) but a boolean flag alongside minimum / maximum under Swagger 2.0 and OpenAPI 3.0, so the form is version-aware.