///|
/// Three applications behind one, the way a single server carries an API, an
/// MCP endpoint and a site:
///
/// moon run src/examples/16-dispatch --target native
///
/// `mount` sends each request to the application whose path prefix it falls
/// under, longest prefix first, and tells that application where it is mounted
/// through `root_path`. `hosts` does the same by host name. The lifespan reaches
/// every application, so each opens what it needs at startup. Around the whole
/// thing, `ProxyHeaders` restores the client a reverse proxy saw and `BodyLimit`
/// refuses oversized uploads.
#coverage.skip
async fn main {
tour() catch {
e => println("failed: \{e}")
}
}
///|
/// An application that names itself, the path it was given and where it is
/// mounted, and says when its lifespan starts and stops.
#coverage.skip
fn named(name : String) -> @spec.AsgiApp {
async fn(scope, receive, send) {
match scope {
Http(hs) => {
let client = match hs.client {
Some((host, _)) => host
None => "-"
}
let text = "\{name}: path \{hs.path}, mounted at \{hs.root_path}, client \{client}, \{hs.scheme}"
send(HttpResponseStart(status=200, headers=[], trailers=false))
send(HttpResponseBody(body=@utf8.encode(text), more_body=false))
}
Lifespan(_) =>
while true {
match receive() {
LifespanStartup => {
println(" \{name} started")
send(LifespanStartupComplete)
}
LifespanShutdown => {
println(" \{name} stopped")
send(LifespanShutdownComplete)
break
}
_ => break
}
}
WebSocket(_) => send(WebSocketClose(code=1000, reason=""))
}
}
}
///|
#coverage.skip
async fn tour() -> Unit {
// One application per prefix; the site takes everything else.
let site = @dispatch.mount(
[
("/api", named("api")),
("/api/v2", named("api v2")),
("/mcp", named("mcp")),
],
fallback=named("site"),
)
// A second host serves only the documentation.
let by_host = @dispatch.hosts(
[("docs.example.com", named("docs"))],
fallback=site,
)
// Behind a reverse proxy on this machine, with uploads capped at 64 bytes.
let app = @mid.stack(
[@mid.ProxyHeaders::new(), @mid.BodyLimit::new(limit=64)],
by_host,
)
let client = @testing.Client::new(app, client=Some(("127.0.0.1", 50000)))
client.lifespan(async fn() {
println("requests:")
for path in ["/api/users", "/api/v2/users", "/mcp", "/apis", "/"] {
println(" GET \{path} -> " + client.get(path).text())
}
let docs = client.get("/guide", headers=[("host", "docs.example.com")])
println(" GET docs.example.com/guide -> " + docs.text())
let proxied = client.get("/api/me", headers=[
("x-forwarded-for", "203.0.113.7"),
("x-forwarded-proto", "https"),
])
println(" GET /api/me through the proxy -> " + proxied.text())
let big = client.post("/api/upload", body=Bytes::make(100, b'x'))
println(" POST 100 bytes -> \{big.status} \{big.text()}")
println("lifespan:")
})
}