// Registering what an application serves: routes and streaming routes, WebSocket routes,
// included routers, mounts, the documentation routes, and the reverse lookup `url_for`.
///|
/// Serve the app's own documentation: the OpenAPI document at `openapi_url`, an
/// interactive reference at `docs_url`, and ReDoc at `redoc_url`. Pass `None` for
/// any of them to leave it off. The routes are kept out of the document they
/// serve, so enabling docs does not change the spec a client reads.
///
/// `ui` picks the reader at `docs_url`. With `Swagger`, `oauth2_redirect_url` is
/// where an OAuth2 flow started from the page returns to, and `init_oauth`
/// prefills it (← FastAPI's `swagger_ui_init_oauth`); `ui_extra` is merged over
/// the reader's own settings either way.
///
/// Behind a proxy that mounts the app under a prefix, the server reports that
/// prefix as the ASGI `root_path`. The pages then fetch the document from under
/// it, and with `root_path_in_servers` the document names it as its first
/// server, so a call tried out from the page goes where the app actually is.
///
/// 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,
ui~ : @openapi.Ui = Scalar,
oauth2_redirect_url~ : String? = Some("/docs/oauth2-redirect"),
init_oauth? : Json,
ui_extra? : Map[String, Json],
root_path_in_servers~ : Bool = true,
) -> Unit {
if openapi_url is Some(url) {
self.get(
url,
ctx => {
let root = ctx.scope.root_path
let servers = self.info.servers
let servers = if root_path_in_servers &&
root != "" &&
!servers.iter().any(s => s.url == root) {
[@openapi.Server::new(root), ..servers]
} else {
servers
}
@status.json(200, self.openapi(version~, servers~))
},
include_in_schema=false,
)
}
let spec_path = match openapi_url {
Some(url) => url
None => "/openapi.json"
}
if docs_url is Some(url) {
self.get(
url,
ctx => {
let root = ctx.scope.root_path
let spec_url = root + spec_path
let title = self.info.title
let page = match ui {
Scalar => @openapi.scalar_ui(spec_url~, title~, extra?=ui_extra)
Swagger =>
@openapi.swagger_ui(
spec_url~,
title~,
oauth2_redirect_url?=oauth2_redirect_url.map(r => root + r),
init_oauth?,
extra?=ui_extra,
)
}
@status.html(200, page)
},
include_in_schema=false,
)
if ui is Swagger && oauth2_redirect_url is Some(r) {
self.get(
r,
_ctx => @status.html(200, @openapi.swagger_redirect()),
include_in_schema=false,
)
}
}
if redoc_url is Some(url) {
self.get(
url,
ctx => {
let spec_url = ctx.scope.root_path + spec_path
@status.html(200, @openapi.redoc_ui(spec_url~, title=self.info.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)))
}
///|
/// The path of the route registered under `name`, with its parameters
/// 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 or holds one its convertor refuses; only a `path`
/// parameter takes a `/`.
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
}
self.named(name, "").bind(path => @route.Pattern::parse(path).fill(params))
}
///|
/// The full path of the route or WebSocket route called `name`, searching this app, then its
/// mounted sub-applications in mount order.
fn App::named(self : App, name : String, prefix : String) -> String? {
for r in self.router.routes {
if r.name == name {
return Some(@route.join_prefix(prefix, r.path))
}
}
for w in self.router.ws_routes {
if w.name == name {
return Some(@route.join_prefix(prefix, w.path))
}
}
for entry in self.mounts {
if entry.1 is Sub(sub) &&
sub.named(name, @route.join_prefix(prefix, entry.0)) is Some(path) {
return Some(path)
}
}
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, with the arguments given here
/// reaching every one of them as `Router::include_router` describes.
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,
hoops~ : Array[@route.Hoop] = [],
) -> Unit {
self.router.include_router(
router,
prefix~,
tags~,
security~,
dependencies~,
responses~,
deprecated~,
include_in_schema~,
hoops~,
)
}
///|
/// Register the methods `view` answers at `path`; see `Router::view`.
pub fn App::view(
self : App,
path : String,
view : &@route.View,
name~ : String = "",
tags~ : Array[String] = [],
security~ : Array[@security.SecurityRequirement] = [],
dependencies~ : Array[String] = [],
deprecated~ : Bool = false,
include_in_schema~ : Bool = true,
hoops~ : Array[@route.Hoop] = [],
) -> Unit {
self.router.view(
path,
view,
name~,
tags~,
security~,
dependencies~,
deprecated~,
include_in_schema~,
hoops~,
)
}
///|
/// Register a route table; see `Router::urls`.
pub fn App::urls(self : App, urls : Array[@route.Url]) -> Unit {
self.router.urls(urls)
}
///|
/// Register a WebSocket route (← FastAPI's `@app.websocket(path)`). The path is a template with
/// the same parameters and convertors as an HTTP route's.
///
/// `security` is enforced and `dependencies` resolved before the handler is called. A refusal
/// declines the handshake with the response an HTTP route would have answered — through ASGI's
/// `websocket.http.response` extension where the server offers it, and with close code 1008
/// otherwise. The dependencies are torn down when the handler returns. `name` is the handle
/// `App::url_for` resolves.
pub fn App::websocket(
self : App,
path : String,
handler : @socket.WsHandler,
name~ : String = "",
security~ : Array[@security.SecurityRequirement] = [],
dependencies~ : Array[String] = [],
) -> Unit {
self.router.websocket(path, handler, name~, security~, dependencies~)
}