moonapi

A typed web framework for MoonBit — FastAPI-style routing and multi-version OpenAPI, on the moonasgi SEAM. Backend-agnostic — the async transport lives in the server (mooncat) that runs the app.

CItestsGitHublicense
$moon add moonbitstack/moonapi

The contract at a glance

let app = App::new()
app.get("/users/:id", ctx => text(200, "user " + ctx.param("id").unwrap()))

let spec = app.openapi_json(version=OpenApi31)   // also OpenApi30 / Swagger20
@mooncat.serve(app.to_asgi(), port=8000)         // run it (native)

§Application & routing

The App, its route builders (get / post / ...), :param path matching, the Context, and text / json response helpers - plus App::to_asgi to run anywhere.

enum
enum Method

HTTP methods a moonapi route can bind to.

struct
struct Context

The per-request context handed to a handler: the raw request plus the path parameters extracted from the matched route (:name segments).

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

Look up a path parameter by name.

type
type ApiHandler = (Context) -> @moonasgi.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.

type
type BackgroundHandler = (Context, BackgroundTasks) -> @moonasgi.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).

type
type StreamHandler = (Context) -> @moonasgi.StreamingResponse raise

A streaming route's handler: request context in, a chunked response out (← returning a StreamingResponse from a FastAPI path operation). Every chunk becomes one body event on the wire, so a client reads the first long before the last one exists — the point of streaming, and what a single buffered Response cannot express.

struct
struct App

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.

fn
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.

fn
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.

fn
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.

fn
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.

fn
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.

fn
fn App::middleware(self : App, mw : @moonasgi.Middleware) -> Unit

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.

fn
fn App::exception_handler(self : App, h : ExceptionHandler) -> 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.

fn
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.

fn
fn App::route( self : App, verb : Method, path : String, handler : ApiHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint? = None, security? : Array[SecurityRequirement] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> 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). The rest describe the operation: summary and description are its prose, operation_id the stable handle client generators name their method after, status_code the status its success response is documented under, responses further documented responses, name the handle App::url_for resolves, and openapi_extra a fragment merged over the generated operation object — the same keyword arguments FastAPI's path operations take.

fn
fn App::route_bg( self : App, verb : Method, path : String, handler : BackgroundHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint? = None, security? : Array[SecurityRequirement] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> 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).

fn
fn App::route_stream( self : App, verb : Method, path : String, handler : StreamHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint? = None, security? : Array[SecurityRequirement] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

Register a streaming route (← a FastAPI path operation returning a StreamingResponse). The handler's chunks reach the client as separate body events, so a long or open-ended reply starts arriving before it is finished. A stream is only streamed as far as the app can honestly keep it one: moonasgi types a middleware as buffered response in, buffered response out, so a middleware that rewrites the body — gzip does — collapses the reply to a single chunk. Everything else about the route is ordinary; it documents, validates and enforces security exactly as route does.

fn
fn App::stream( self : App, path : String, handler : StreamHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint? = None, security? : Array[SecurityRequirement] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

Register a streaming GET route — the verb a stream is nearly always read over, and what an SSE endpoint is. A shorthand for route_stream(Get, ...).

fn
fn App::get( self : App, path : String, handler : ApiHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint? = None, security? : Array[SecurityRequirement] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

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

fn
fn App::enable_docs( self : App, openapi_url? : String? = Some("/openapi.json"), docs_url? : String? = Some("/docs"), redoc_url? : String? = Some("/redoc"), version? : OpenApiVersion = OpenApi31) -> 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.

fn
fn App::post( self : App, path : String, handler : ApiHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint? = None, security? : Array[SecurityRequirement] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> 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.

fn
fn App::put( self : App, path : String, handler : ApiHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint? = None, security? : Array[SecurityRequirement] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

Register a PUT route: replace the addressed resource wholesale.

fn
fn App::patch( self : App, path : String, handler : ApiHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint? = None, security? : Array[SecurityRequirement] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

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

fn
fn App::delete( self : App, path : String, handler : ApiHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint? = None, security? : Array[SecurityRequirement] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

Register a DELETE route.

fn
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.

fn
fn App::add_status_handler( self : App, status : Int, handler : (Context) -> @moonasgi.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.

fn
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.

fn
fn App::mount_handler( self : App, prefix : String, handler : @moonasgi.Handler) -> Unit

Mount a foreign moonasgi handler under prefix (← FastAPI mounting a plain ASGI app, app.mount("/static", StaticFiles(...))). A request under prefix is handed to handler with the prefix stripped, exactly as for a sub-app. A handler is not a moonapi application, so it contributes nothing to the OpenAPI document and has no lifespan of its own to run — the app only routes to it. Mounts are tried in registration order, whichever kind they are.

fn
fn App::url_for( self : App, name : String, params? : Map[String, String] = Map([])) -> String?

The path of the route registered under name, with its :name segments filled from params (← FastAPI's url_path_for). Mounted sub-applications are searched too, so what comes back already carries the mount prefix — the path a client would call, not the one the sub-app knows itself by. None when nothing is registered under that name, or when params is missing one the path needs. Values are substituted as given: a value with a / in it lands as extra path segments, so encode before calling if that matters.

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

A plain-text response.

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

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

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

A JSON response serialised from a Json value.

fn
fn App::handle_with_stream( self : App, request : @moonasgi.Request) -> (@moonasgi.StreamingResponse, BackgroundTasks)

Route a request through the middleware chain and return the reply in its streamed form, plus the background queue the handler filled. to_asgi sends each chunk as its own body event; a test reads chunks to see where the boundaries fell. A route that is not a streaming one comes back as a single chunk, so this answers every request, not only the streamed ones. A middleware is typed buffered-in, buffered-out, so the chain is run over the joined body and the chunks are kept only when what came back is what went in. A middleware that rewrote the body — gzip — has produced something the old boundaries no longer describe, and cutting the new bytes at them would send a corrupt stream.

fn
fn App::handle_with_background( self : App, request : @moonasgi.Request) -> (@moonasgi.Response, BackgroundTasks)

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.

fn
fn App::handle( self : App, request : @moonasgi.Request) -> @moonasgi.Response

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.

fn
fn App::lifespan_handler(self : App) -> @moonasgi.LifespanHandler

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.

fn
fn App::to_asgi(self : App) -> @moonasgi.AsgiApp

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.

§Routers & composition

APIRouter and include_router - routes collected away from any application and folded into one later, under a prefix and with the tags, security and responses the whole group shares. Plus mounting a foreign moonasgi handler.

struct
struct Router

A collection of routes built away from any application and folded into one with App::include_router (← FastAPI's APIRouter). It carries the registration surface of an App and nothing else: middleware, mounts, security schemes, documentation and lifespan belong to the application that includes it.

fn
fn Router::new() -> Router

An empty router. The prefix and the attributes its routes share are given at App::include_router rather than here, so one router can be included twice — under a second prefix, or on another app with different tags.

fn
fn Router::route( self : Router, verb : Method, path : String, handler : ApiHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint? = None, security? : Array[SecurityRequirement] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

Register a route on the router for an explicit method. Takes what App::route takes and means the same by it; the route reaches an application when the router is included.

fn
fn Router::route_bg( self : Router, verb : Method, path : String, handler : BackgroundHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint? = None, security? : Array[SecurityRequirement] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

Register a background-aware route on the router — App::route_bg, deferred to whichever application includes it.

fn
fn Router::stream( self : Router, path : String, handler : StreamHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint? = None, security? : Array[SecurityRequirement] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

Register a streaming GET route on the router — App::stream, deferred to whichever application includes it.

fn
fn Router::get( self : Router, path : String, handler : ApiHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint? = None, security? : Array[SecurityRequirement] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

Register a GET route on the router.

fn
fn Router::post( self : Router, path : String, handler : ApiHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint? = None, security? : Array[SecurityRequirement] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

Register a POST route on the router.

fn
fn Router::put( self : Router, path : String, handler : ApiHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint? = None, security? : Array[SecurityRequirement] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

Register a PUT route on the router.

fn
fn Router::patch( self : Router, path : String, handler : ApiHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint? = None, security? : Array[SecurityRequirement] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

Register a PATCH route on the router.

fn
fn Router::delete( self : Router, path : String, handler : ApiHandler, summary? : String = "", description? : String = "", tags? : Array[String] = [], deprecated? : Bool = false, operation_id? : String = "", status_code? : Int, responses? : Array[ResponseSpec] = [], name? : String = "", endpoint? : Endpoint? = None, security? : Array[SecurityRequirement] = [], include_in_schema? : Bool = true, validate? : Bool = true, openapi_extra? : Json) -> Unit

Register a DELETE route on the router.

fn
fn Router::websocket( self : Router, path : String, handler : WsHandler) -> Unit

Register a WebSocket route on the router (← APIRouter.websocket). It takes the including prefix like any other route; the operation attributes do not apply, since a WebSocket route is not an OpenAPI operation.

fn
fn App::include_router( self : App, router : Router, prefix? : String = "", tags? : Array[String] = [], security? : Array[SecurityRequirement] = [], responses? : Array[ResponseSpec] = [], deprecated? : Bool = false, include_in_schema? : Bool = true) -> Unit

Fold router's routes into this app (FastAPI's include_router, and the same name — include is a reserved word). Each is registered under prefix and becomes one of the app's own routes, and the arguments given here reach every one of them: - tags and security are placed before the route's own, so a group's tag leads and a group-wide requirement cannot be dropped by a route that adds one of its own; - responses are documented under the route's, which therefore wins any status both name; - deprecated marks the whole group, and include_in_schema=false hides it, neither of which a route can undo. The one include_router argument with no counterpart is dependencies: moonapi has no route-level dependency list, since injection is explicit through a Container that the handler reads.

§Descriptor tree

The runtime Schema descriptor - the first-class value that stands in for FastAPI's from-signature reflection. Walked once to emit complete OpenAPI body schemas (objects / arrays / scalars, required, $ref'd components) and to drive validation. Includes the ToSchema trait and its builders.

enum
enum Schema

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.

struct
struct ObjectSchema

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.

struct
struct Field

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

fn
fn Field::new( name : String, schema : Schema, required? : Bool = true, 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.

fn
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.

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

An array schema whose elements all conform to item.

item
pub(open) trait ToSchema

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.

item
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.

§Endpoint descriptor

Param / ResponseSpec / Endpoint - the descriptor a typed route carries, walked once into an OpenAPI operation (parameters + requestBody + responses, with named schemas hoisted into components).

enum
enum ParamLoc

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

struct
struct Param

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.

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

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

struct
struct ResponseSpec

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

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

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

struct
struct Endpoint

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.

fn
fn Endpoint::new( params? : Array[Param] = [], request_body? : Schema? = None, request_required? : Bool = true, 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.

§OpenAPI & Swagger

Multi-version OpenAPI / Swagger generation (2.0 / 3.0 / 3.1) from the same routes and descriptors, and a ready-to-serve Swagger UI page.

enum
enum OpenApiVersion

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.

struct
struct Contact

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.

struct
struct License

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

struct
struct Server

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

struct
struct ApiInfo

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.

fn
fn ApiInfo::new() -> ApiInfo

The defaults an app starts with.

fn
fn App::openapi( self : App, version? : OpenApiVersion = OpenApi31, 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.

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

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

fn
fn redoc_ui( spec_url? : String = "/openapi.json", title? : String = "moonapi") -> 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.

fn
fn swagger_ui( spec_url? : String = "/openapi.json", title? : String = "moonapi") -> String

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

§Extraction & validation

Typed extraction off the Context - query / cookie params, JSON body, single JSON fields - plus descriptor-driven validation and FastAPI-shaped ValidationError values and 422 responses.

fn
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.

fn
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.

fn
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.

fn
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.

fn
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.

struct
struct ValidationError

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).

fn
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"}.

fn
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.

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

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

fn
fn unprocessable(errors : Array[ValidationError]) -> @moonasgi.Response

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

fn
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.

fn
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.

§Typed body extractors

Context::body deserialises the JSON body into a derive(FromJson) struct, and Context::body_validated checks it against the endpoint descriptor first - yielding a FastAPI-shaped 422 error list on failure and the built value on success, all off one source of truth.

item
fn[T : @json.FromJson] Context::body(self : Context) -> T?

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.

item
fn[T : @json.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).

§Dependency injection

A container (provider registry + dependency_overrides), request-scoped one-shot resolution with per-request caching, and yield-style teardown run LIFO around the handler - the explicit MoonBit equivalent of FastAPI's Depends.

struct
struct Provider[V]

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).

item
fn[V] Provider::new( factory : () -> V, teardown? : (V) -> Unit = _v => ()) -> 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.

item
fn[V] Provider::scoped( factory : (Scope[V]) -> V, teardown? : (V) -> Unit = _v => ()) -> 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).

struct
struct Container[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.

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

An empty container.

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

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

item
fn[V] Container::provide_using( self : Container[V], key : String, factory : (Scope[V]) -> V, teardown? : (V) -> Unit = _v => ()) -> 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.

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

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

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

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

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

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

struct
struct Scope[V]

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).

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

Open a fresh request scope over this container.

item
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.

item
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.

item
fn[V] Container::run( self : Container[V], handler : (Scope[V]) -> @moonasgi.Response) -> @moonasgi.Response

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.

§Form & file extractors

Context::form parses an urlencoded body (percent- and plus-decoded) and a multipart/form-data body, splitting the boundary stream into FormFields and byte-exact UploadFiles - FastAPI's Form(...) and File(...) parameters.

struct
struct FormField

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

struct
struct UploadFile

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), the raw content bytes exactly as received, and the part's own headers with their names lowercased, in body order. content_type is kept as its own field because nearly every caller wants it and nothing else; headers is there for the rest — a Content-Transfer-Encoding a handler must honour, a checksum a client attached — which otherwise had nowhere to be read from.

fn
fn UploadFile::size(self : UploadFile) -> Int

The uploaded size in bytes (← FastAPI's UploadFile.size). Derived rather than stored: a field could be set to disagree with content, and a size that lies about the bytes beside it is worse than no size at all.

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

Look up one of the part's own headers by name, None if it carried no such header. Names are matched lowercased, the same convention as @moonasgi.Request::header.

struct
struct FormData

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.

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

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

fn
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.

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

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

struct
struct FormLimits

What a submitted form may cost before it is refused: how many parts it may carry, and how many bytes any one part may hold. A request body is attacker-controlled and, on this SEAM, fully buffered before a route ever sees it. Unbounded, a body that is nothing but boundaries becomes as many parts as it has bytes — every one of them an allocation the app made on the sender's say-so, on top of the body it already holds.

fn
fn FormLimits::new( max_parts? : Int = 1000, max_part_size? : Int = 1024 * 1024) -> FormLimits

Limits with Starlette's defaults — 1000 parts of at most 1 MiB — or either bound overridden.

fn
fn Context::form( self : Context, limits? : FormLimits = FormLimits::new()) -> 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. None when the body breaks limits — more parts than max_parts, or a part longer than max_part_size. The form is refused whole rather than truncated: a handler given the first thousand parts of a larger form would answer a request nobody sent. An empty Some is the other answer, and means the request carried no form at all.

§OAuth2 & bearer security

OAuth2 password-bearer: create_access_token issues a scoped HS256 JWT, and OAuth2PasswordBearer reads the Authorization header, verifies the token, and enforces scopes - 401 on a missing / invalid / expired token, 403 on an insufficient scope. The MoonBit equivalent of FastAPI's Security(...).

struct
struct OAuth2PasswordRequestForm

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.

fn
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 — and equally when the body is too large to be one, since a grant carries six fields and nothing that big is a login.

struct
struct AuthenticatedUser

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).

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

Whether this user was granted scope.

struct
struct OAuth2PasswordBearer

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.

fn
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.

fn
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.

fn
fn OAuth2PasswordBearer::authenticate( self : OAuth2PasswordBearer, ctx : Context, now_secs : Int64, scopes? : Array[String] = []) -> Result[AuthenticatedUser, @moonasgi.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.

fn
fn create_access_token( subject : String, secret : String, now_secs : Int64, scopes? : Array[String] = [], expires_in_secs? : Int64 = 3600, extra? : Map[String, Json] = Map([])) -> 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.

fn
fn token_response(access_token : String) -> @moonasgi.Response

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

§OpenAPI security schemes

SecurityScheme objects emitted into the generated spec - OAuth2 password flow, HTTP bearer (JWT), API keys, and Basic - under components/securitySchemes in 3.x and securityDefinitions in Swagger 2.0, so the OAuth2 / JWT layer is described to clients.

enum
enum SecurityScheme

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.

fn
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.

§Per-operation security

A SecurityRequirement attaches a declared scheme (and its scopes) to a route: emitted as the operation's OpenAPI security array, and - when the scheme was registered with an enforcer via App::secure_oauth2 - enforced before the handler (401 unauthenticated, 403 on a missing scope).

struct
struct SecurityRequirement

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.

fn
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"]).

§Background tasks

BackgroundTasks queues thunks a background-aware route schedules; the app runs them, in order, after the response is sent - FastAPI's BackgroundTasks.

struct
struct BackgroundTasks

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.

fn
fn BackgroundTasks::new() -> BackgroundTasks

An empty task queue.

fn
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).

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

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

fn
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.

§Middleware & exception handlers

The outer middleware chain: cors (preflight + actual-request headers, configurable origins / methods / headers / credentials), gzip (a real DEFLATE compressor, below), per-status handlers for custom error pages, and exception handlers that map a raised HttpException to a response, with a built-in 500 fallback.

item
suberror HttpException

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.

fn
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").

type
type ExceptionHandler = (Context, Error) -> @moonasgi.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".

struct
struct CorsConfig

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.

fn
fn cors( allow_origins? : Array[String] = [], allow_all_origins? : Bool = false, allow_methods? : Array[String] = [ "GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", ], allow_headers? : Array[String] = [], allow_all_headers? : Bool = false, allow_credentials? : Bool = false, expose_headers? : Array[String] = [], max_age? : Int = 600) -> @moonasgi.Middleware

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.

fn
fn gzip(min_size? : Int = 500) -> @moonasgi.Middleware

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.

§DEFLATE / gzip compression

A real RFC 1951 DEFLATE compressor - LZ77 back-reference matching over a 32 KiB window, coded with the fixed Huffman table - and a companion inflate that decodes stored and fixed-Huffman blocks, so gzip output round-trips and any conforming inflater (gzip, zlib) reads it.

fn
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.

§Server-Sent Events

ServerSentEvent frames per the WHATWG event-stream format - id / event / retry / multi-line data / comment keep-alives - and sse_response builds the text/event-stream envelope.

struct
struct ServerSentEvent

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).

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

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

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

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

fn
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.

fn
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.

fn
fn sse_response( events : Array[ServerSentEvent], status? : Int = 200, headers? : Array[(String, String)] = []) -> @moonasgi.StreamingResponse

A text/event-stream response carrying events, one frame per chunk — hand it to App::stream and each reaches the client on its own. Sets Cache-Control: no-cache and Connection: keep-alive, the headers an SSE endpoint sends so intermediaries don't buffer or close the stream.

§WebSocket routes

App::websocket over the moonasgi WS SEAM. The handler drives a WebSocket (accept / receive / send / close) as a synchronous core, so drive_websocket runs it against an in-memory frame queue in a test and the serving shell runs it over the async transport.

enum
enum WsMessage

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

struct
struct WebSocket

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.

type
type WsHandler = (WebSocket) -> Unit

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

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

Look up a matched path parameter by name.

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

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

fn
fn WebSocket::accept( self : WebSocket, subprotocol? : String? = None, 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.

fn
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.

fn
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()).

fn
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.

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

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

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

Send a binary frame to the client.

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

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

fn
fn WebSocket::sent(self : WebSocket) -> Array[@moonasgi.Event]

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

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

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

fn
fn drive_websocket( handler : WsHandler, inbound : Array[WsMessage], params? : Map[String, String] = Map([]), subprotocols? : Array[String] = []) -> Array[@moonasgi.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.

§JWT (HS256)

Sign and verify compact HS256 JSON Web Tokens over the self-built HMAC, with base64url segments, exp / nbf checks, constant-time signature comparison, and refusal of the alg:none downgrade.

fn
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.

fn
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.

item
suberror JwtError

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.

fn
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.

fn
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.

fn
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.

fn
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.

fn
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.

fn
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.

fn
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.

fn
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.

§SHA-256 / HMAC

The self-built signing primitives behind JWT: SHA-256 (FIPS 180-4), HMAC-SHA256 (RFC 2104), and a constant-time byte comparison - core ships no crypto, so these are implemented here and checked against the NIST / RFC 4231 vectors.

fn
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.

fn
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.

fn
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.

§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 exposes only what it promised - FastAPI's response_model.

fn
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.

fn
fn json_model( status : Int, model : Schema, value : Json) -> @moonasgi.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.

§Redirects & file downloads

The response kinds that are an envelope rather than a body: redirect (the status and a percent-encoded Location) and file_response (a media type from the filename, Content-Length, and an RFC 6266 Content-Disposition).

fn
fn redirect(url : String, status? : Int = 307) -> @moonasgi.Response

A redirect to url (← FastAPI's RedirectResponse). The body is empty and Location carries the target. 307 is the default because it is the redirect that keeps the request's method and body — the one a POST can safely follow, which 302 historically is not. Use 303 to send a client to a GET after a write, and 301 / 308 for a move that is permanent. The URL is percent-encoded over the characters a URI reserves for structure, so an already-encoded URL passes through unchanged and a \r\n smuggled into one cannot open a header of its own.

fn
fn file_response( content : Bytes, filename? : String = "", media_type? : String = "", status? : Int = 200, inline? : Bool = false) -> @moonasgi.Response

A file download built from bytes already in hand (← FastAPI's FileResponse). It takes the content rather than a path because moonapi has no filesystem of its own — the same app runs on wasm, js and native, and only the server knows how to read a file on any of them. What this adds is the envelope: a media type guessed from filename's extension unless media_type names one, Content-Length, and a Content-Disposition that tells the browser to save the file (or, with inline, to display it) under that name. An empty filename leaves the disposition off entirely.

§Status codes

The 63 HTTP_* and 15 WS_* constants FastAPI re-exports from Starlette, so a route reads HTTP_404_NOT_FOUND rather than a bare number.

item
const HTTP_100_CONTINUE : Int = 100 ///| /// Switching Protocols: the server is changing to the protocol `Upgrade` asked for. pub const HTTP_101_SWITCHING_PROTOCOLS : Int = 101 ///| /// Processing: WebDAV, the request is under way and the reply will follow. pub const HTTP_102_PROCESSING : Int = 102 ///| /// Early Hints: preload links sent ahead of the real response. pub const HTTP_103_EARLY_HINTS : Int = 103 ///| /// OK. pub const HTTP_200_OK : Int = 200 ///| /// Created: the request made a new resource, named in `Location`. pub const HTTP_201_CREATED : Int = 201 ///| /// Accepted: taken for processing, outcome not yet known. pub const HTTP_202_ACCEPTED : Int = 202 ///| /// Non-Authoritative Information: an intermediary altered the origin's payload. pub const HTTP_203_NON_AUTHORITATIVE_INFORMATION : Int = 203 ///| /// No Content: succeeded, and there is nothing to send back. pub const HTTP_204_NO_CONTENT : Int = 204 ///| /// Reset Content: succeeded; the client should clear the form that sent it. pub const HTTP_205_RESET_CONTENT : Int = 205 ///| /// Partial Content: the byte ranges `Range` asked for. pub const HTTP_206_PARTIAL_CONTENT : Int = 206 ///| /// Multi-Status: WebDAV, one status per member of a collection. pub const HTTP_207_MULTI_STATUS : Int = 207 ///| /// Already Reported: WebDAV, this member was enumerated earlier in the reply. pub const HTTP_208_ALREADY_REPORTED : Int = 208 ///| /// IM Used: the body is the result of applying delta encodings. pub const HTTP_226_IM_USED : Int = 226 ///| /// Multiple Choices: several representations, pick one. pub const HTTP_300_MULTIPLE_CHOICES : Int = 300 ///| /// Moved Permanently: use `Location` from now on. pub const HTTP_301_MOVED_PERMANENTLY : Int = 301 ///| /// Found: a temporary move. Clients rewrite `POST` to `GET` here, which is why /// `307` exists. pub const HTTP_302_FOUND : Int = 302 ///| /// See Other: fetch the outcome with a `GET` at `Location` — the redirect after /// a form post. pub const HTTP_303_SEE_OTHER : Int = 303 ///| /// Not Modified: the client's cached copy is still current. pub const HTTP_304_NOT_MODIFIED : Int = 304 ///| /// Use Proxy: deprecated by RFC 9110. pub const HTTP_305_USE_PROXY : Int = 305 ///| /// Reserved: never standardised, and reserved so nothing else claims it. pub const HTTP_306_RESERVED : Int = 306 ///| /// Temporary Redirect: like `302`, but the method and body must be kept. pub const HTTP_307_TEMPORARY_REDIRECT : Int = 307 ///| /// Permanent Redirect: like `301`, but the method and body must be kept. pub const HTTP_308_PERMANENT_REDIRECT : Int = 308 ///| /// Bad Request: malformed enough that the server will not act on it. pub const HTTP_400_BAD_REQUEST : Int = 400 ///| /// Unauthorized: not authenticated. The reply must carry `WWW-Authenticate`. pub const HTTP_401_UNAUTHORIZED : Int = 401 ///| /// Payment Required: reserved. pub const HTTP_402_PAYMENT_REQUIRED : Int = 402 ///| /// Forbidden: authenticated, and still not allowed. pub const HTTP_403_FORBIDDEN : Int = 403 ///| /// Not Found. pub const HTTP_404_NOT_FOUND : Int = 404 ///| /// Method Not Allowed: the path exists under another verb, listed in `Allow`. pub const HTTP_405_METHOD_NOT_ALLOWED : Int = 405 ///| /// Not Acceptable: nothing on offer matches the request's `Accept`. pub const HTTP_406_NOT_ACCEPTABLE : Int = 406 ///| /// Proxy Authentication Required. pub const HTTP_407_PROXY_AUTHENTICATION_REQUIRED : Int = 407 ///| /// Request Timeout: the client took too long to send it. pub const HTTP_408_REQUEST_TIMEOUT : Int = 408 ///| /// Conflict: it clashes with the resource's current state. pub const HTTP_409_CONFLICT : Int = 409 ///| /// Gone: it existed and was removed on purpose. pub const HTTP_410_GONE : Int = 410 ///| /// Length Required: `Content-Length` is missing and this server insists on it. pub const HTTP_411_LENGTH_REQUIRED : Int = 411 ///| /// Precondition Failed: an `If-*` header did not hold. pub const HTTP_412_PRECONDITION_FAILED : Int = 412 ///| /// Content Too LargeStarlette's name keeps RFC 7231's "Request Entity Too Large". pub const HTTP_413_REQUEST_ENTITY_TOO_LARGE : Int = 413 ///| /// URI Too LongStarlette's name keeps RFC 7231's "Request-URI Too Long". pub const HTTP_414_REQUEST_URI_TOO_LONG : Int = 414 ///| /// Unsupported Media Type: the body's `Content-Type` is one this route cannot read. pub const HTTP_415_UNSUPPORTED_MEDIA_TYPE : Int = 415 ///| /// Range Not Satisfiable: no part of the requested range exists. pub const HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE : Int = 416 ///| /// Expectation Failed: the `Expect` header cannot be met. pub const HTTP_417_EXPECTATION_FAILED : Int = 417 ///| /// I'm a Teapot: RFC 2324's joke, kept because clients test for it. pub const HTTP_418_IM_A_TEAPOT : Int = 418 ///| /// Misdirected Request: this connection is not authoritative for that authority. pub const HTTP_421_MISDIRECTED_REQUEST : Int = 421 ///| /// Unprocessable Content: well-formed, and it fails validation — what `unprocessable` /// returns. pub const HTTP_422_UNPROCESSABLE_ENTITY : Int = 422 ///| /// Locked: WebDAV, the resource is locked. pub const HTTP_423_LOCKED : Int = 423 ///| /// Failed Dependency: WebDAV, a request this one depended on failed. pub const HTTP_424_FAILED_DEPENDENCY : Int = 424 ///| /// Too Early: replaying this early-data request could be a replay attack. pub const HTTP_425_TOO_EARLY : Int = 425 ///| /// Upgrade Required: resend over the protocol named in `Upgrade`. pub const HTTP_426_UPGRADE_REQUIRED : Int = 426 ///| /// Precondition Required: the server refuses to act on an unconditional write. pub const HTTP_428_PRECONDITION_REQUIRED : Int = 428 ///| /// Too Many Requests: rate limited; `Retry-After` says when to come back. pub const HTTP_429_TOO_MANY_REQUESTS : Int = 429 ///| /// Request Header Fields Too Large. pub const HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE : Int = 431 ///| /// Unavailable For Legal Reasons. pub const HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS : Int = 451 ///| /// Internal Server Error: the fallback for an error the app did not map. pub const HTTP_500_INTERNAL_SERVER_ERROR : Int = 500 ///| /// Not Implemented: the server does not support the method at all. pub const HTTP_501_NOT_IMPLEMENTED : Int = 501 ///| /// Bad Gateway: an upstream sent something invalid. pub const HTTP_502_BAD_GATEWAY : Int = 502 ///| /// Service Unavailable: down or overloaded, and expected back. pub const HTTP_503_SERVICE_UNAVAILABLE : Int = 503 ///| /// Gateway Timeout: an upstream did not answer in time. pub const HTTP_504_GATEWAY_TIMEOUT : Int = 504 ///| /// HTTP Version Not Supported. pub const HTTP_505_HTTP_VERSION_NOT_SUPPORTED : Int = 505 ///| /// Variant Also Negotiates: the negotiation is configured in a circle. pub const HTTP_506_VARIANT_ALSO_NEGOTIATES : Int = 506 ///| /// Insufficient Storage: WebDAV, no room to store the representation. pub const HTTP_507_INSUFFICIENT_STORAGE : Int = 507 ///| /// Loop Detected: WebDAV, the traversal is cyclic. pub const HTTP_508_LOOP_DETECTED : Int = 508 ///| /// Not Extended. pub const HTTP_510_NOT_EXTENDED : Int = 510 ///| /// Network Authentication Required: a captive portal wants a login first. pub const HTTP_511_NETWORK_AUTHENTICATION_REQUIRED : Int = 511 // The WebSocket close codes of RFC 6455 §7.4.1, the second half of what // `status` re-exports. `1005`, `1006` and `1015` never travel on the wire — an // endpoint reports them to its own application, so sending one is a protocol // error rather than a close reason. ///| /// Normal Closure: the purpose the connection was opened for is fulfilled. pub const WS_1000_NORMAL_CLOSURE : Int = 1000 ///| /// Going Away: the peer is shutting down or navigating off the page. pub const WS_1001_GOING_AWAY : Int = 1001 ///| /// Protocol Error. pub const WS_1002_PROTOCOL_ERROR : Int = 1002 ///| /// Unsupported Data: a frame of a type this endpoint cannot accept. pub const WS_1003_UNSUPPORTED_DATA : Int = 1003 ///| /// No Status Received: reported locally when a close frame carried no code. pub const WS_1005_NO_STATUS_RCVD : Int = 1005 ///| /// Abnormal Closure: reported locally when the connection died without a close frame. pub const WS_1006_ABNORMAL_CLOSURE : Int = 1006 ///| /// Invalid Frame Payload Data: a text frame that is not valid UTF-8, say. pub const WS_1007_INVALID_FRAME_PAYLOAD_DATA : Int = 1007 ///| /// Policy Violation: the generic refusal when no other code fits. pub const WS_1008_POLICY_VIOLATION : Int = 1008 ///| /// Message Too Big. pub const WS_1009_MESSAGE_TOO_BIG : Int = 1009 ///| /// Mandatory Extension: the client required an extension the server did not negotiate. pub const WS_1010_MANDATORY_EXT : Int = 1010 ///| /// Internal Error: the server hit an unexpected condition. pub const WS_1011_INTERNAL_ERROR : Int = 1011 ///| /// Service Restart. pub const WS_1012_SERVICE_RESTART : Int = 1012 ///| /// Try Again Later: overloaded; come back. pub const WS_1013_TRY_AGAIN_LATER : Int = 1013 ///| /// Bad Gateway: the gateway got an invalid response upstream. pub const WS_1014_BAD_GATEWAY : Int = 1014 ///| /// TLS Handshake: reported locally when the handshake failed. pub const WS_1015_TLS_HANDSHAKE : Int = 1015

Continue: the headers are acceptable, send the body.

§Worked example

User structs that describe their own schema (derive(ToJson) + a T::schema() associated function + a ToSchema bridge) and a demo app whose request and response bodies surface fully-typed in openapi.json - the mctl-friendly shape.

struct
struct Address

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

fn
fn Address::schema() -> Schema

The Address descriptor.

item
impl ToSchema for Address with fn to_schema(_self)

Address's schema, so a route declaring it as a body or response documents itself.

struct
struct NewUser

The demo request model — the body of POST /users. age is optional.

fn
fn NewUser::schema() -> Schema

The NewUser descriptor.

item
impl ToSchema for NewUser with fn to_schema(_self)

NewUser's schema, so a route declaring it as a body or response documents itself.

struct
struct User

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.

fn
fn User::schema() -> Schema

The User descriptor.

item
impl ToSchema for User with fn to_schema(_self)

User's schema, so a route declaring it as a body or response documents itself.

fn
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.

§OAuth2 worked example

oauth2_app wires the security layer end to end: a /token endpoint that issues a scoped JWT and two protected routes, one requiring the items scope - FastAPI's security tutorial in explicit MoonBit form.

fn
fn oauth2_app(now : () -> Int64, secret? : String = "demo-secret") -> 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.

§Parameter constraints

The declared bounds a parameter carries — min/max, length, pattern, enum — checked on the way in and emitted into the schema on the way out, so the document and the enforcement cannot drift apart.

enum
enum Constraint

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

fn
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.

fn
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.

§Security extractors

Pulling the credential out of a request for each scheme: the Authorization header, an API key in a header, query or cookie, and HTTP basic.

struct
struct HttpBasicCredentials

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

fn
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.

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

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

fn
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.

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

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

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

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

§Signature primitives

The RSA PKCS#1 v1.5, ECDSA P-256 and Ed25519 sign/verify primitives JWT rests on, with the SHA-512 they need — written here so token verification needs no native binding and runs on every backend.

struct
struct 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.

struct
struct 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.

fn
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.

fn
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.

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

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

fn
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.

fn
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.

struct
struct EcdsaPublicKey

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

fn
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.

fn
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).

struct
struct EcdsaPrivateKey

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

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

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

fn
fn EcdsaPrivateKey::public_key(self : EcdsaPrivateKey) -> EcdsaPublicKey

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

fn
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.

fn
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.

fn
fn ed25519_public_from_seed(seed : Bytes) -> Bytes

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

fn
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.

struct
struct Ed25519PublicKey

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

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

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

struct
struct Ed25519PrivateKey

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

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

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

fn
fn Ed25519PrivateKey::public_key( self : Ed25519PrivateKey) -> Ed25519PublicKey

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

fn
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.

§Dependency injection worked example

A worked wiring of the container: providers, scopes and overrides, kept in the package so it is compiled and tested rather than only described.

struct
struct GreetReq

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.

fn
fn GreetReq::schema() -> Schema

The GreetReq descriptor — one required string field.

enum
enum Dep

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.

fn
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.