ASGI 3.0 for MoonBit: the interface between servers and frameworks.
moon add moonbitstack/moonasgilet greet : @moonasgi.Handler = fn(_request) {
@moonasgi.Response::text("hello")
}
let client = @moonasgi.TestClient::new(greet)
let response = client.get("/")
println("\{response.status} \{response.text()}")200 hello| Name | What it is |
|---|---|
| Scope | Http · WebSocket · Lifespan — the connection, as typed data |
| Event | Every message in both directions, one variant each, extensions included |
| Receive | async () -> Event — pull the next event in |
| Send | async (Event) -> Unit — push the next event out |
| AsgiApp | async (Scope, Receive, Send) -> Unit — an application |
| Handler | (Request) -> Response — the short form a server lifts onto AsgiApp |
| Middleware | (Handler) -> Handler — one handler wrapped around another |
| Core | Drives |
|---|---|
| run_http | a Handler over a scope and an inbound event list |
| run_http_scoped | a scope-level application, with the full HttpScope |
| run_http_stream | a StreamingResponse, lowered to the wire order |
| ws_run | a WebSocket connection, connect to close |
| run_lifespan | startup and shutdown, in the order a server runs them |
let answer = @moonasgi.run_http(greet, scope, inbound) // Array[Event]@moonasgi.Response::text("hello") // 200, text/plain
@moonasgi.Response::json(status=201, { "id": 233 }) // any status
@moonasgi.Response::new(204, [("x-trace", "1")], b"") // headers and raw bytes@moonasgi.StreamingResponse::new(
status=200,
headers=[("content-type", "text/event-stream")],
chunks=[b"data: 233\n\n", b"data: done\n\n"],
trailers=[("x-digest", "9f2c")],
early_hints=[["</style.css>; rel=preload"]],
)let handler = @moonasgi.WebSocketHandler::new(
on_receive=fn(message) {
match message {
Text(text) => [SendText("echo:" + text)]
Binary(bytes) => [SendBinary(bytes)]
}
},
)let lifecycle = @moonasgi.LifespanHandler::new(
on_startup=fn(scope) {
scope.state["db_pool"] = Json::string("pool://main")
Complete
},
on_shutdown=fn(_scope) { Complete },
)| Middleware | What it does | Default |
|---|---|---|
| cors | Answers preflights, echoes the origin, exposes headers | no origin, GET |
| gzip | Compresses a single-message body that is large enough | 500 octets, level 9 |
| trusted_hosts | Refuses other Host values, redirects to www. | any host, redirect on |
| https_redirect | http to https, ws to wss, by 307 | — |
| proxy_headers | Restores the client and scheme from a trusted proxy | trusts 127.0.0.1 |
| body_limit | Answers 413 over the limit, by length or as it streams | 1 MiB |
| server_errors | A 500 when the answer has not started, then the error on | no detail |
let app : @moonasgi.AsgiApp = async fn(_, receive, send) {
ignore(receive())
send(HttpResponseStart(status=200, headers=[], trailers=false))
send(HttpResponseBody(body=b"ok", more_body=false))
}
let stacked = @mid.stack(
[
@mid.server_errors,
@mid.TrustedHosts::new(hosts=["example.com"]),
@mid.cors,
@mid.gzip,
],
app,
)let app = @dispatch.mount([
("/api", api_app),
("/mcp", mcp_app),
])
let by_host = @dispatch.hosts([("docs.example.com", docs_app)], fallback=app)let legacy : @wsgi.WsgiApp = (environ, start_response) => {
ignore(start_response("200 OK", [("Content-Type", "text/plain")]))
[@utf8.encode("hello from "), @utf8.encode(environ.path_info)].iter()
}
let app : @moonasgi.AsgiApp = @wsgi.to_asgi(legacy)let scope = @moonasgi.HttpScope::from_h2_headers([
(":method", "POST"),
(":scheme", "https"),
(":authority", "example.test"),
(":path", "/items?page=2"),
])| Package | What is in it |
|---|---|
| moonbitstack/moonasgi | Everything below, re-exported under one import |
| …/spec | The scopes, the events, the extensions, the HTTP/2 lowering |
| …/http | Request, Response, StreamingResponse, the HTTP cores |
| …/ws | WebSocketHandler, WsAccept, WsMessage, ws_run |
| …/lifespan | LifespanHandler and run_lifespan |
| …/client | TestClient for handlers and WebSocket sessions |
| …/mid | The middleware stack and the seven above |
| …/dispatch | mount, hosts and a shared lifespan |
| …/wsgi | A synchronous application on the same events |
| …/testing | A real AsgiApp driven in-process, event by event |
| …/conform | The conformance harness and the ordering rules |
moon run src/examples/00-hello| # | Example | Shows |
|---|---|---|
| 00 | hello | A handler, a test client, and the same handler on the raw seam |
| 01 | responses | Text, JSON, bytes, custom status and headers |
| 02 | request-methods | Every verb, the request line, a streamed body |
| 03 | middleware | A chain of handler transformers, outermost first |
| 04 | streaming | Chunks, trailers and 103 Early Hints |
| 05 | scope | The full scope a framework binds to |
| 06 | extensions | Capability flags and the extension events |
| 07 | websocket | Accept, reject, deny, echo |
| 08 | lifespan | Startup seeding state, and a failed boot |
| 09 | validate | Naming the first ordering violation |
| 10 | http2 | Lowering a HEADERS block |
| 11 | headers | The latin-1 header codec |
| 12 | conformance | The harness, run |
| 13 | legacy | The older double-callable shape |
| 14 | stack | Real middleware around a real application |
| 15 | testing | An async application tested in-process |
| 16 | dispatch | Three applications behind one |
| 17 | wsgi | A synchronous application, mounted |
| 18 | trace | Every event of a request, in order |
fn guarantee_single_callable(app : AsgiApplication) -> (async (Scope, async () -> Event, async (Event) -> Unit) -> Unit)fn latin1_decode(bytes : Bytes) -> Stringfn latin1_encode(s : String) -> Bytesfn percent_decode(target : String) -> Bytes