///|
/// HTTP methods a moonapi route can bind to.
pub(all) enum Method {
Get
Post
Put
Patch
Delete
Head
Options
} derive(Eq)
///|
fn parse_method(s : String) -> Method? {
match s {
"GET" => Some(Get)
"POST" => Some(Post)
"PUT" => Some(Put)
"PATCH" => Some(Patch)
"DELETE" => Some(Delete)
"HEAD" => Some(Head)
"OPTIONS" => Some(Options)
// TRACE, CONNECT, or something invented. Answering it as a GET would run a
// handler the caller never asked for.
_ => None
}
}
///|
/// A method's name as it appears in an `Allow` header.
fn method_name(m : Method) -> String {
match m {
Get => "GET"
Post => "POST"
Put => "PUT"
Patch => "PATCH"
Delete => "DELETE"
Head => "HEAD"
Options => "OPTIONS"
}
}
///|
fn method_lower(m : Method) -> String {
match m {
Get => "get"
Post => "post"
Put => "put"
Patch => "patch"
Delete => "delete"
Head => "head"
Options => "options"
}
}
///|
/// The per-request context handed to a handler: the raw request plus the path
/// parameters extracted from the matched route (`:name` segments).
pub(all) struct Context {
request : @moonasgi.Request
params : Map[String, String]
}
///|
/// Look up a path parameter by name.
pub fn Context::param(self : Context, name : String) -> String? {
self.params.get(name)
}
///|
/// 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.
pub type ApiHandler = (Context) -> @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).
pub type BackgroundHandler = (Context, BackgroundTasks) -> @moonasgi.Response raise
///|
/// A route stores one background-aware handler; a plain `ApiHandler` is wrapped
/// to ignore the queue. `security` is the per-operation requirements (emitted as
/// OpenAPI `security` and enforced before the handler when their scheme has an
/// enforcer).
struct Route {
verb : Method
path : String
run : BackgroundHandler
summary : String
tags : Array[String]
deprecated : Bool
endpoint : Endpoint?
security : Array[SecurityRequirement]
// FastAPI's `include_in_schema`: a route that serves the documentation itself has
// no business appearing in the document it serves.
include_in_schema : Bool
// Whether the declared `endpoint` is checked before the handler runs. On by
// default: a route that advertises constraints in its documentation and does not
// apply them is worse than one that declares nothing.
validate : Bool
}
///|
/// 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.
pub struct App {
routes : Array[Route]
ws_routes : Array[WsRoute]
middlewares : Array[@moonasgi.Middleware]
exception_handlers : Array[ExceptionHandler]
security_schemes : Array[(String, SecurityScheme)]
enforcers : Map[String, SecurityEnforcer]
status_handlers : Map[Int, (Context) -> @moonasgi.Response]
mounts : Array[(String, App)]
// Run in registration order when the server starts, and in reverse when it stops,
// so a resource is torn down before whatever it was built on.
startup_hooks : Array[() -> Unit raise]
shutdown_hooks : Array[() -> Unit raise]
// What the emitted document says about itself. `enable_docs` serves the same
// values, so the page a reader opens is not a different API from the one
// `App::openapi` describes.
mut info : ApiInfo
mut clock : () -> Int64
}
///|
/// 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.
pub fn App::new() -> App {
{
routes: [],
ws_routes: [],
middlewares: [],
exception_handlers: [],
security_schemes: [],
enforcers: Map([]),
status_handlers: Map([]),
mounts: [],
startup_hooks: [],
shutdown_hooks: [],
info: ApiInfo::new(),
clock: () => 0L,
}
}
///|
/// 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.
pub fn App::describe(
self : App,
title? : String,
api_version? : String,
description? : String,
terms_of_service? : String,
contact? : Contact?,
license? : License?,
servers? : Array[Server],
) -> Unit {
let cur = self.info
self.info = {
title: title.unwrap_or(cur.title),
api_version: api_version.unwrap_or(cur.api_version),
description: description.unwrap_or(cur.description),
terms_of_service: terms_of_service.unwrap_or(cur.terms_of_service),
contact: contact.unwrap_or(cur.contact),
license: license.unwrap_or(cur.license),
servers: servers.unwrap_or(cur.servers),
}
}
///|
/// 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.
pub fn App::on_startup(self : App, hook : () -> Unit raise) -> Unit {
self.startup_hooks.push(hook)
}
///|
/// 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.
pub fn App::on_shutdown(self : App, hook : () -> Unit raise) -> Unit {
self.shutdown_hooks.push(hook)
}
///|
/// 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.
pub fn App::set_clock(self : App, clock : () -> Int64) -> Unit {
self.clock = clock
}
///|
/// 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.
pub fn App::middleware(self : App, mw : @moonasgi.Middleware) -> Unit {
self.middlewares.push(mw)
}
///|
/// 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`.
pub fn App::exception_handler(self : App, h : ExceptionHandler) -> Unit {
self.exception_handlers.push(h)
}
///|
/// 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.
pub fn App::add_security_scheme(
self : App,
name : String,
scheme : SecurityScheme,
) -> Unit {
self.security_schemes.push((name, scheme))
}
///|
/// 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).
pub fn App::route(
self : App,
verb : Method,
path : String,
handler : ApiHandler,
summary? : String = "",
tags? : Array[String] = [],
deprecated? : Bool = false,
endpoint? : Endpoint? = None,
security? : Array[SecurityRequirement] = [],
include_in_schema? : Bool = true,
validate? : Bool = true,
) -> Unit {
self.routes.push({
verb,
path,
run: (ctx, _bg) => handler(ctx),
summary,
tags,
deprecated,
endpoint,
security,
include_in_schema,
validate,
})
}
///|
/// 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).
pub fn App::route_bg(
self : App,
verb : Method,
path : String,
handler : BackgroundHandler,
summary? : String = "",
tags? : Array[String] = [],
deprecated? : Bool = false,
endpoint? : Endpoint? = None,
security? : Array[SecurityRequirement] = [],
include_in_schema? : Bool = true,
validate? : Bool = true,
) -> Unit {
self.routes.push({
verb,
path,
run: handler,
summary,
tags,
deprecated,
endpoint,
security,
include_in_schema,
validate,
})
}
///|
/// 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 : ApiHandler,
summary? : String = "",
tags? : Array[String] = [],
deprecated? : Bool = false,
endpoint? : Endpoint? = None,
security? : Array[SecurityRequirement] = [],
include_in_schema? : Bool = true,
validate? : Bool = true,
) -> Unit {
self.route(
Get,
path,
handler,
summary~,
tags~,
deprecated~,
endpoint~,
security~,
include_in_schema~,
validate~,
)
}
///|
/// 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? : OpenApiVersion = OpenApi31,
) -> Unit {
let title = self.info.title
if openapi_url is Some(url) {
self.get(
url,
_ctx => 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 => html(200, swagger_ui(spec_url~, title~)),
include_in_schema=false,
)
}
if redoc_url is Some(url) {
self.get(
url,
_ctx => html(200, redoc_ui(spec_url~, title~)),
include_in_schema=false,
)
}
}
///|
/// 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 : ApiHandler,
summary? : String = "",
tags? : Array[String] = [],
deprecated? : Bool = false,
endpoint? : Endpoint? = None,
security? : Array[SecurityRequirement] = [],
include_in_schema? : Bool = true,
validate? : Bool = true,
) -> Unit {
self.route(
Post,
path,
handler,
summary~,
tags~,
deprecated~,
endpoint~,
security~,
include_in_schema~,
validate~,
)
}
///|
/// Register a `PUT` route: replace the addressed resource wholesale.
pub fn App::put(
self : App,
path : String,
handler : ApiHandler,
summary? : String = "",
tags? : Array[String] = [],
deprecated? : Bool = false,
endpoint? : Endpoint? = None,
security? : Array[SecurityRequirement] = [],
include_in_schema? : Bool = true,
validate? : Bool = true,
) -> Unit {
self.route(
Put,
path,
handler,
summary~,
tags~,
deprecated~,
endpoint~,
security~,
include_in_schema~,
validate~,
)
}
///|
/// Register a `PATCH` route: change part of the addressed resource.
pub fn App::patch(
self : App,
path : String,
handler : ApiHandler,
summary? : String = "",
tags? : Array[String] = [],
deprecated? : Bool = false,
endpoint? : Endpoint? = None,
security? : Array[SecurityRequirement] = [],
include_in_schema? : Bool = true,
validate? : Bool = true,
) -> Unit {
self.route(
Patch,
path,
handler,
summary~,
tags~,
deprecated~,
endpoint~,
security~,
include_in_schema~,
validate~,
)
}
///|
/// Register a `DELETE` route.
pub fn App::delete(
self : App,
path : String,
handler : ApiHandler,
summary? : String = "",
tags? : Array[String] = [],
deprecated? : Bool = false,
endpoint? : Endpoint? = None,
security? : Array[SecurityRequirement] = [],
include_in_schema? : Bool = true,
validate? : Bool = true,
) -> Unit {
self.route(
Delete,
path,
handler,
summary~,
tags~,
deprecated~,
endpoint~,
security~,
include_in_schema~,
validate~,
)
}
///|
/// 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.
pub fn App::secure_oauth2(
self : App,
name : String,
bearer : OAuth2PasswordBearer,
scopes? : Array[(String, String)] = [],
) -> Unit {
self.security_schemes.push((name, bearer.scheme(scopes~)))
self.enforcers[name] = (ctx, required, now) => {
bearer.authenticate(ctx, now, scopes=required)
}
}
///|
/// 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.
pub fn App::add_status_handler(
self : App,
status : Int,
handler : (Context) -> @moonasgi.Response,
) -> Unit {
self.status_handlers[status] = handler
}
///|
/// 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.
pub fn App::mount(self : App, prefix : String, subapp : App) -> Unit {
self.mounts.push((prefix, subapp))
}
///|
/// Split a path into its non-empty segments.
fn segments(path : String) -> Array[String] {
let out : Array[String] = []
let n = path.length()
let mut start = 0
for i = 0; i <= n; i = i + 1 {
if i == n || path[i] == '/' {
if i > start {
out.push(path[start:i].to_owned())
}
start = i + 1
}
}
out
}
///|
/// Match a route pattern against a request path, extracting `:name` params.
fn match_path(pattern : String, path : String) -> Map[String, String]? {
let pp = segments(pattern)
let ph = segments(path)
if pp.length() != ph.length() {
return None
}
let params : Map[String, String] = Map([])
for i = 0; i < pp.length(); i = i + 1 {
let seg = pp[i]
if seg.length() > 0 && seg[0] == ':' {
params[seg[1:].to_owned()] = ph[i]
} else if seg != ph[i] {
return None
}
}
Some(params)
}
///|
/// A plain-text response.
pub fn text(status : Int, body : String) -> @moonasgi.Response {
@moonasgi.Response::new(
status,
[("content-type", "text/plain; charset=utf-8")],
@utf8.encode(body),
)
}
///|
/// An HTML response — what the documentation pages are served as.
pub fn html(status : Int, body : String) -> @moonasgi.Response {
@moonasgi.Response::new(
status,
[("content-type", "text/html; charset=utf-8")],
@utf8.encode(body),
)
}
///|
/// A JSON response serialised from a `Json` value.
pub fn json(status : Int, value : Json) -> @moonasgi.Response {
@moonasgi.Response::new(
status,
[("content-type", "application/json")],
@utf8.encode(value.stringify()),
)
}
///|
/// Rewrite an error-path response through a registered per-status handler, if one
/// is registered for its status; otherwise pass it through unchanged.
fn App::apply_status(
self : App,
ctx : Context,
resp : @moonasgi.Response,
) -> @moonasgi.Response {
match self.status_handlers.get(resp.status) {
Some(h) => h(ctx)
None => resp
}
}
///|
/// Try each registered exception handler in order; the first to claim the error
/// wins outright. Otherwise fall through to the built-in mapping, which a
/// per-status handler may still rewrite (a custom `500` page, say).
fn App::dispatch_exception(
self : App,
ctx : Context,
err : Error,
) -> @moonasgi.Response {
for h in self.exception_handlers {
match h(ctx, err) {
Some(resp) => return resp
None => ()
}
}
self.apply_status(ctx, default_exception_response(err))
}
///|
/// Enforce a route's security requirements. `None` means allow (nothing to
/// enforce, or a requirement passed); `Some(resp)` is the 401/403 to return.
/// Requirements are alternatives (OpenAPI OR-semantics): the first enforceable
/// one that passes admits the request; if every enforceable requirement fails,
/// the last failure response is returned. A requirement naming a
/// documentation-only scheme (no enforcer) grants nothing by itself.
fn App::enforce_security(
self : App,
ctx : Context,
reqs : Array[SecurityRequirement],
) -> @moonasgi.Response? {
if reqs.is_empty() {
return None
}
let now = (self.clock)()
let mut enforceable = false
let mut last : @moonasgi.Response? = None
for r in reqs {
match self.enforcers.get(r.scheme) {
Some(enf) => {
enforceable = true
match enf(ctx, r.scopes, now) {
Ok(_) => return None
Err(resp) => last = Some(resp)
}
}
None => ()
}
}
if enforceable {
last
} else {
None
}
}
///|
/// The routing + exception core, inside the middleware chain: match a route,
/// enforce its security, run its (possibly raising) handler, and turn any raised
/// error into a response. Falls through to mounted sub-apps, then 405 (a path
/// matched but no method) or 404 (no path). `bg` is the request's background
/// queue, handed to the matched handler and drained by the caller after the
/// response is sent.
fn App::route_and_dispatch(
self : App,
request : @moonasgi.Request,
bg : BackgroundTasks,
) -> @moonasgi.Response {
let verb = parse_method(request.http_method)
let allowed : Array[String] = []
for route in self.routes {
match match_path(route.path, request.path) {
Some(params) =>
// A GET route answers HEAD too, with the body dropped — the same rule
// Starlette applies, and what a client probing a resource expects.
if verb is Some(v) &&
(route.verb == v || (v is Head && route.verb is Get)) {
let ctx : Context = { request, params, }
match self.enforce_security(ctx, route.security) {
Some(denied) => return self.apply_status(ctx, denied)
None => ()
}
if route.validate && route.endpoint is Some(ep) {
let errs = ep.validate(ctx)
if errs.length() > 0 {
return self.apply_status(ctx, unprocessable(errs))
}
}
let resp = (route.run)(ctx, bg) catch {
e => self.dispatch_exception(ctx, e)
}
return if verb is Some(Head) && route.verb is Get {
@moonasgi.Response::new(resp.status, resp.headers, b"")
} else {
resp
}
} else {
allowed.push(method_name(route.verb))
if route.verb is Get {
allowed.push("HEAD")
}
}
None => ()
}
}
for entry in self.mounts {
let (prefix, sub) = entry
match strip_prefix(prefix, request.path) {
Some(rest) => {
let subreq : @moonasgi.Request = {
http_method: request.http_method,
path: rest,
query_string: request.query_string,
headers: request.headers,
body: request.body,
}
let (resp, subbg) = sub.handle_with_background(subreq)
bg.absorb(subbg)
return resp
}
None => ()
}
}
let ctx : Context = { request, params: Map([]), }
if allowed.length() > 0 {
// RFC 9110 §15.5.6 requires the Allow header on a 405; a client that probes
// a resource reads it rather than guessing.
self.apply_status(
ctx,
http_exception_response(405, "Method Not Allowed".to_json(), [
("allow", dedupe(allowed).join(", ")),
]),
)
} else {
self.apply_status(
ctx,
http_exception_response(404, "Not Found".to_json(), []),
)
}
}
///|
/// The distinct entries of `names`, in first-seen order.
fn dedupe(names : Array[String]) -> Array[String] {
let out : Array[String] = []
for n in names {
if !out.contains(n) {
out.push(n)
}
}
out
}
///|
/// 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.
pub fn App::handle_with_background(
self : App,
request : @moonasgi.Request,
) -> (@moonasgi.Response, BackgroundTasks) {
let bg = BackgroundTasks::new()
let base : @moonasgi.Handler = req => self.route_and_dispatch(req, bg)
let resp = @moonasgi.compose(self.middlewares, base)(request)
(resp, bg)
}
///|
/// 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.
pub fn App::handle(
self : App,
request : @moonasgi.Request,
) -> @moonasgi.Response {
self.handle_with_background(request).0
}
///|
/// Strip a mount `prefix` off `path` by whole segments, returning the sub-app's
/// path (`"/"` when the request hits the mount root). `None` when `path` does not
/// lie under `prefix` — the comparison is segment-wise, so `/sub` does not match
/// `/subway`.
fn strip_prefix(prefix : String, path : String) -> String? {
let pp = segments(prefix)
let ph = segments(path)
if pp.length() > ph.length() {
return None
}
for i = 0; i < pp.length(); i = i + 1 {
if pp[i] != ph[i] {
return None
}
}
let sb = StringBuilder()
for i = pp.length(); i < ph.length(); i = i + 1 {
sb.write_string("/")
sb.write_string(ph[i])
}
let rest = sb.to_string()
if rest == "" {
Some("/")
} else {
Some(rest)
}
}
///|
async fn drain_body(receive : @moonasgi.Receive) -> Bytes {
let buf = Buffer()
let mut more = true
while more {
match receive() {
HttpRequest(body~, more_body~) => {
buf.write_bytes(body)
more = more_body
}
_ => more = false
}
}
buf.to_bytes()
}
///|
/// 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.
pub fn App::lifespan_handler(self : App) -> @moonasgi.LifespanHandler {
@moonasgi.LifespanHandler::new(
on_startup=_scope => {
match self.run_startup_hooks() {
None => Complete
Some(why) => Failed(message=why)
}
},
on_shutdown=_scope => {
match self.run_shutdown_hooks() {
None => Complete
Some(why) => Failed(message=why)
}
},
)
}
///|
/// Answer the ASGI lifespan protocol: run each server message through the handler
/// above and send back its reply. The run ends on shutdown, or on a failed startup —
/// ASGI has the server abort the boot there and never send `lifespan.shutdown`.
async fn App::serve_lifespan(
self : App,
scope : @moonasgi.Scope,
receive : @moonasgi.Receive,
send : @moonasgi.Send,
) -> Unit {
let handler = self.lifespan_handler()
for ;; {
let event = receive()
let replies = @moonasgi.run_lifespan(handler, scope, [event])
for reply in replies {
send(reply)
}
if event is LifespanShutdown {
break
}
if replies.iter().any(r => r is LifespanStartupFailed(_)) {
break
}
}
}
///|
/// Run every startup hook, this app's mounts included, stopping at the first that
/// raises and returning its message.
fn App::run_startup_hooks(self : App) -> String? {
for hook in self.startup_hooks {
hook() catch {
err => return Some(err.to_string())
}
}
for mount in self.mounts {
if mount.1.run_startup_hooks() is Some(why) {
return Some(why)
}
}
None
}
///|
/// Run every shutdown hook in reverse registration order, mounts first since they
/// were started last. Every hook runs even if an earlier one raised; the first
/// failure is what comes back.
fn App::run_shutdown_hooks(self : App) -> String? {
let mut failure = None
for i = self.mounts.length() - 1; i >= 0; i = i - 1 {
if self.mounts[i].1.run_shutdown_hooks() is Some(why) && failure is None {
failure = Some(why)
}
}
for i = self.shutdown_hooks.length() - 1; i >= 0; i = i - 1 {
self.shutdown_hooks[i]() catch {
err => if failure is None { failure = Some(err.to_string()) }
}
}
failure
}
///|
/// 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.
pub fn App::to_asgi(self : App) -> @moonasgi.AsgiApp {
(scope, receive, send) => {
match scope {
Http(hs) => {
let body = drain_body(receive)
let request : @moonasgi.Request = {
http_method: hs.http_method,
path: hs.path,
query_string: hs.query_string,
headers: hs.headers,
body,
}
let (resp, bg) = self.handle_with_background(request)
send(
@moonasgi.Event::HttpResponseStart(
status=resp.status,
headers=resp.headers,
trailers=false,
),
)
send(@moonasgi.Event::HttpResponseBody(body=resp.body, more_body=false))
// The response is on the wire; now drain any background tasks.
bg.run()
}
WebSocket(wss) => self.serve_websocket(wss, receive, send)
Lifespan(_) => self.serve_lifespan(scope, receive, send)
}
}
}