// Registering what an application serves: routes and streaming routes, WebSocket routes,
// included routers, mounts, the documentation routes, and the reverse lookup `url_for`.
///|
/// 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); `dependencies` names provider
/// keys resolved through the app's container (`App::depends`) before the handler
/// and torn down after it, whatever the handler did.
///
/// 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.
pub fn App::route(
self : App,
verb : @route.Method,
path : String,
handler : @route.ApiHandler,
summary~ : String = "",
description~ : String = "",
tags~ : Array[String] = [],
deprecated~ : Bool = false,
operation_id~ : String = "",
status_code? : Int32,
responses~ : Array[@endpoint.ResponseSpec] = [],
name~ : String = "",
endpoint? : @endpoint.Endpoint,
security~ : Array[@security.SecurityRequirement] = [],
dependencies~ : Array[String] = [],
include_in_schema~ : Bool = true,
validate~ : Bool = true,
openapi_extra? : Json,
) -> Unit {
self.router.routes.push({
verb,
path,
run: ctx => @route.Buffered(handler(ctx)),
summary,
description,
tags,
deprecated,
operation_id,
status_code,
responses,
name,
openapi_extra,
endpoint,
security,
dependencies,
include_in_schema,
validate,
})
}
///|
/// 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.
pub fn App::route_stream(
self : App,
verb : @route.Method,
path : String,
handler : @route.StreamHandler,
summary~ : String = "",
description~ : String = "",
tags~ : Array[String] = [],
deprecated~ : Bool = false,
operation_id~ : String = "",
status_code? : Int32,
responses~ : Array[@endpoint.ResponseSpec] = [],
name~ : String = "",
endpoint? : @endpoint.Endpoint,
security~ : Array[@security.SecurityRequirement] = [],
dependencies~ : Array[String] = [],
include_in_schema~ : Bool = true,
validate~ : Bool = true,
openapi_extra? : Json,
) -> Unit {
self.router.routes.push({
verb,
path,
run: ctx => @route.Streamed(handler(ctx)),
summary,
description,
tags,
deprecated,
operation_id,
status_code,
responses,
name,
openapi_extra,
endpoint,
security,
dependencies,
include_in_schema,
validate,
})
}
///|
/// 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, ...)`.
pub fn App::stream(
self : App,
path : String,
handler : @route.StreamHandler,
summary~ : String = "",
description~ : String = "",
tags~ : Array[String] = [],
deprecated~ : Bool = false,
operation_id~ : String = "",
status_code? : Int32,
responses~ : Array[@endpoint.ResponseSpec] = [],
name~ : String = "",
endpoint? : @endpoint.Endpoint,
security~ : Array[@security.SecurityRequirement] = [],
dependencies~ : Array[String] = [],
include_in_schema~ : Bool = true,
validate~ : Bool = true,
openapi_extra? : Json,
) -> Unit {
self.route_stream(
Get,
path,
handler,
summary~,
description~,
tags~,
deprecated~,
operation_id~,
status_code?,
responses~,
name~,
endpoint?,
security~,
dependencies~,
include_in_schema~,
validate~,
openapi_extra?,
)
}
///|
/// Register a `GET` route. A shorthand for `route(Get, ...)` carrying the same
/// documentation and security arguments.
pub fn App::get(
self : App,
path : String,
handler : @route.ApiHandler,
summary~ : String = "",
description~ : String = "",
tags~ : Array[String] = [],
deprecated~ : Bool = false,
operation_id~ : String = "",
status_code? : Int32,
responses~ : Array[@endpoint.ResponseSpec] = [],
name~ : String = "",
endpoint? : @endpoint.Endpoint,
security~ : Array[@security.SecurityRequirement] = [],
dependencies~ : Array[String] = [],
include_in_schema~ : Bool = true,
validate~ : Bool = true,
openapi_extra? : Json,
) -> Unit {
self.route(
Get,
path,
handler,
summary~,
description~,
tags~,
deprecated~,
operation_id~,
status_code?,
responses~,
name~,
endpoint?,
security~,
dependencies~,
include_in_schema~,
validate~,
openapi_extra?,
)
}
///|
/// Register a `POST` route — the verb that carries a request body, so this is the
/// one most often given an `endpoint` describing that body.
pub fn App::post(
self : App,
path : String,
handler : @route.ApiHandler,
summary~ : String = "",
description~ : String = "",
tags~ : Array[String] = [],
deprecated~ : Bool = false,
operation_id~ : String = "",
status_code? : Int32,
responses~ : Array[@endpoint.ResponseSpec] = [],
name~ : String = "",
endpoint? : @endpoint.Endpoint,
security~ : Array[@security.SecurityRequirement] = [],
dependencies~ : Array[String] = [],
include_in_schema~ : Bool = true,
validate~ : Bool = true,
openapi_extra? : Json,
) -> Unit {
self.route(
Post,
path,
handler,
summary~,
description~,
tags~,
deprecated~,
operation_id~,
status_code?,
responses~,
name~,
endpoint?,
security~,
dependencies~,
include_in_schema~,
validate~,
openapi_extra?,
)
}
///|
/// Register a `PUT` route: replace the addressed resource wholesale.
pub fn App::put(
self : App,
path : String,
handler : @route.ApiHandler,
summary~ : String = "",
description~ : String = "",
tags~ : Array[String] = [],
deprecated~ : Bool = false,
operation_id~ : String = "",
status_code? : Int32,
responses~ : Array[@endpoint.ResponseSpec] = [],
name~ : String = "",
endpoint? : @endpoint.Endpoint,
security~ : Array[@security.SecurityRequirement] = [],
dependencies~ : Array[String] = [],
include_in_schema~ : Bool = true,
validate~ : Bool = true,
openapi_extra? : Json,
) -> Unit {
self.route(
Put,
path,
handler,
summary~,
description~,
tags~,
deprecated~,
operation_id~,
status_code?,
responses~,
name~,
endpoint?,
security~,
dependencies~,
include_in_schema~,
validate~,
openapi_extra?,
)
}
///|
/// Register a `PATCH` route: change part of the addressed resource.
pub fn App::patch(
self : App,
path : String,
handler : @route.ApiHandler,
summary~ : String = "",
description~ : String = "",
tags~ : Array[String] = [],
deprecated~ : Bool = false,
operation_id~ : String = "",
status_code? : Int32,
responses~ : Array[@endpoint.ResponseSpec] = [],
name~ : String = "",
endpoint? : @endpoint.Endpoint,
security~ : Array[@security.SecurityRequirement] = [],
dependencies~ : Array[String] = [],
include_in_schema~ : Bool = true,
validate~ : Bool = true,
openapi_extra? : Json,
) -> Unit {
self.route(
Patch,
path,
handler,
summary~,
description~,
tags~,
deprecated~,
operation_id~,
status_code?,
responses~,
name~,
endpoint?,
security~,
dependencies~,
include_in_schema~,
validate~,
openapi_extra?,
)
}
///|
/// Register a `DELETE` route.
pub fn App::delete(
self : App,
path : String,
handler : @route.ApiHandler,
summary~ : String = "",
description~ : String = "",
tags~ : Array[String] = [],
deprecated~ : Bool = false,
operation_id~ : String = "",
status_code? : Int32,
responses~ : Array[@endpoint.ResponseSpec] = [],
name~ : String = "",
endpoint? : @endpoint.Endpoint,
security~ : Array[@security.SecurityRequirement] = [],
dependencies~ : Array[String] = [],
include_in_schema~ : Bool = true,
validate~ : Bool = true,
openapi_extra? : Json,
) -> Unit {
self.route(
Delete,
path,
handler,
summary~,
description~,
tags~,
deprecated~,
operation_id~,
status_code?,
responses~,
name~,
endpoint?,
security~,
dependencies~,
include_in_schema~,
validate~,
openapi_extra?,
)
}
///|
/// 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.
pub fn App::enable_docs(
self : App,
openapi_url~ : String? = Some("/openapi.json"),
docs_url~ : String? = Some("/docs"),
redoc_url~ : String? = Some("/redoc"),
version~ : @schema.OpenApiVersion = OpenApi31,
) -> Unit {
let title = self.info.title
if openapi_url is Some(url) {
self.get(
url,
_ctx => @status.json(200, self.openapi(version~)),
include_in_schema=false,
)
}
let spec_url = match openapi_url {
Some(url) => url
None => "/openapi.json"
}
if docs_url is Some(url) {
self.get(
url,
_ctx => @status.html(200, @openapi.swagger_ui(spec_url~, title~)),
include_in_schema=false,
)
}
if redoc_url is Some(url) {
self.get(
url,
_ctx => @status.html(200, @openapi.redoc_ui(spec_url~, title~)),
include_in_schema=false,
)
}
}
///|
/// Mount a sub-application under `prefix` (← FastAPI's `app.mount(prefix, sub)`).
/// A request whose path lies under `prefix` is served by `subapp`, with the prefix moved from
/// `path` to `root_path` (its own layers, 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.
pub fn App::mount(self : App, prefix : String, subapp : App) -> Unit {
self.mounts.push((prefix, Sub(subapp)))
}
///|
/// Mount any ASGI application under `prefix` (← FastAPI mounting a plain ASGI app,
/// `app.mount("/static", StaticFiles(...))`). A request under `prefix` reaches `app` with the
/// prefix moved from `path` to `root_path`, exactly as for a sub-app.
///
/// It 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.
pub fn App::mount_asgi(
self : App,
prefix : String,
app : @moonasgi.AsgiApp,
) -> Unit {
self.mounts.push((prefix, Asgi(app)))
}
///|
/// Fill a route pattern's `:name` segments from `params`, the inverse of
/// `match_path`. `None` when the pattern needs a parameter `params` does not
/// carry — half a URL is worse than none.
fn render_path(pattern : String, params : Map[String, String]) -> String? {
let sb = StringBuilder()
for seg in @route.segments(pattern) {
sb.write_string("/")
if seg.length() > 0 && seg[0] == ':' {
match params.get(seg[1:].to_owned()) {
Some(v) => sb.write_string(v)
None => return None
}
} else {
sb.write_string(seg)
}
}
let rendered = sb.to_string()
Some(if rendered == "" { "/" } else { rendered })
}
///|
/// 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.
pub fn App::url_for(
self : App,
name : String,
params~ : Map[String, String] = Map([]),
) -> String? {
// Every route starts out unnamed, so an empty name would match the first one.
if name == "" {
return None
}
let all : Array[(String, @route.Route)] = []
self.collect_routes("", all)
for entry in all {
if entry.1.name == name {
return render_path(entry.0, params)
}
}
None
}
///|
/// 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`, `security` and `dependencies` are placed before the route's own, so
/// a group's tag leads and a group-wide requirement or dependency 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.
pub fn App::include_router(
self : App,
router : @route.Router,
prefix~ : String = "",
tags~ : Array[String] = [],
security~ : Array[@security.SecurityRequirement] = [],
dependencies~ : Array[String] = [],
responses~ : Array[@endpoint.ResponseSpec] = [],
deprecated~ : Bool = false,
include_in_schema~ : Bool = true,
) -> Unit {
for r in router.routes {
self.router.routes.push({
..r,
path: join_prefix(prefix, r.path),
tags: [..tags, ..r.tags],
security: [..security, ..r.security],
dependencies: [..dependencies, ..r.dependencies],
responses: [..responses, ..r.responses],
deprecated: deprecated || r.deprecated,
include_in_schema: include_in_schema && r.include_in_schema,
})
}
for w in router.ws_routes {
self.router.ws_routes.push({ ..w, path: join_prefix(prefix, w.path), })
}
}
///|
/// Register a WebSocket route (← FastAPI's `@app.websocket(path)`). The path matches with the
/// same `:name` segment rules as HTTP routes.
pub fn App::websocket(
self : App,
path : String,
handler : @socket.WsHandler,
) -> Unit {
self.router.ws_routes.push({ path, handler, })
}