///|
/// Context — the central abstraction of mbit, inspired by Go's net/http context pattern.
///
/// Context carries the request, response writer, parsed parameters,
/// and the middleware/handler chain through a single request lifecycle.
///
/// ## Middleware chain (`ctx.next()`)
///
/// Each request runs through a chain of handlers. Middleware calls `c.next()`
/// to yield to the next handler in the chain:
///
/// ```
/// // Logging middleware
/// fn logger(ctx) {
/// let start = now()
/// ctx.next() // <-- runs the rest of the chain
/// let elapsed = now() - start
/// println("\{ctx.method()} \{ctx.path()} -> \{ctx.status_code} (\{elapsed}ms)")
/// }
/// ```
///|
/// Handler function signature.
/// All route handlers and middleware share this signature.
pub type Handler = async (Context) -> Unit
///| ——————————————————————————————————————————————————————————————————————
/// Response connection abstraction
///| ——————————————————————————————————————————————————————————————————————
///|
/// In-memory connection used by tests — captures status, headers, and body.
pub(all) struct TestConn {
mut status : Int
mut reason : String
headers : Map[String, String]
mut body : String
mut header_sent : Bool
mut ended : Bool
}
///|
/// Create a fresh in-memory test connection.
pub fn TestConn::new() -> TestConn {
{
status: 0,
reason: "",
headers: Map([]),
body: "",
header_sent: false,
ended: false,
}
}
///|
/// The response sink — either a real HTTP server connection or an
/// in-memory buffer used for testing.
pub(all) enum ResponseConn {
Real(@http.ServerConnection)
Test(TestConn)
}
///|
/// Send the response status line and headers.
pub async fn ResponseConn::send_response(
self : ResponseConn,
code : Int,
reason : String,
extra_headers~ : Map[String, String] = Map([]),
) -> Unit {
match self {
Real(c) => c.send_response(code, reason, extra_headers~)
Test(t) => {
t.status = code
t.reason = reason
for k, v in extra_headers {
t.headers.set(k, v)
}
t.header_sent = true
}
}
}
///|
/// Write a chunk of the response body.
pub async fn ResponseConn::write_body(self : ResponseConn, s : String) -> Unit {
match self {
Real(c) => c.write_string(s)
Test(t) => t.body = t.body + s
}
}
///|
/// Finish the response.
pub async fn ResponseConn::end_response(self : ResponseConn) -> Unit {
match self {
Real(c) => c.end_response()
Test(t) => t.ended = true
}
}
///|
/// Flush buffered data to the client.
pub async fn ResponseConn::flush(self : ResponseConn) -> Unit {
match self {
Real(c) => c.flush()
Test(_) => ()
}
}
///|
/// Context holds all per-request state.
pub(all) struct Context {
// === Raw HTTP ===
/// The incoming HTTP request (method, path, headers, etc.)
req : @http.Request
/// Reader for reading the request body
reader : &@io.Reader
/// Response sink for sending the response
conn : ResponseConn
// === Parsed state ===
/// Path parameters extracted from the route pattern (e.g., `:id`)
mut params : Map[String, String]
/// Query parameters — parsed lazily on first access
mut query_cache : Map[String, String]?
/// Full request body — read lazily on first access
mut body : String?
/// Full request body as raw bytes — read lazily on first access
mut body_bytes : Bytes?
/// Post form data — parsed lazily on first access
mut post_form_cache : Map[String, String]?
// === Middleware chain ===
/// All handlers (middleware + route handler) for this request
mut handlers : Array[Handler]
/// Current position in the handler chain
mut index : Int
// === Response ===
/// HTTP status code to send (default 200)
mut status_code : Int
/// Whether the response has already been written
mut written : Bool
/// Whether the handler chain has been aborted
mut aborted : Bool
/// Number of bytes written to the response
mut size : Int
// === Key-value store ===
/// Arbitrary data shared between middleware and handlers
mut store : Map[String, Json]
// === Errors ===
/// Errors collected during request processing
mut errors : Array[String]
// === Response headers ===
/// Extra headers to include in the response
mut headers : Map[String, String]
}
///|
/// Create a new Context from the raw HTTP components and handler chain.
pub fn Context::new(
req : @http.Request,
reader : &@io.Reader,
conn : ResponseConn,
handlers : Array[Handler],
) -> Context {
{
req,
reader,
conn,
params: Map([]),
query_cache: None,
body: None,
body_bytes: None,
post_form_cache: None,
handlers,
index: 0,
status_code: 200,
written: false,
aborted: false,
size: 0,
store: Map([]),
errors: [],
headers: Map([]),
}
}
///| ——————————————————————————————————————————————————————————————————————
/// Request accessors
///| ——————————————————————————————————————————————————————————————————————
///|
/// The HTTP method of the request (e.g., "GET", "POST").
pub fn Context::http_method(self : Context) -> String {
Method::from_native(self.req.meth).to_string()
}
///|
/// Request returns the raw HTTP request.
/// See `c.Request` equivalent.
pub fn Context::request(self : Context) -> @http.Request {
self.req
}
///|
/// The full request path including query string.
pub fn Context::full_path(self : Context) -> String {
self.req.path
}
///|
/// The request path without the query string.
pub fn Context::path(self : Context) -> String {
match self.req.path.find("?") {
Some(pos) => self.req.path[:pos].to_owned()
None => self.req.path
}
}
///|
/// A request header value, if present.
///
/// Note: the underlying HTTP server lower-cases all request header keys
/// (`moonbitlang/async/http` parser does `to_lower()`), so lookups fall back
/// to the lower-cased key to stay correct under real HTTP traffic.
pub fn Context::header(self : Context, key : String) -> String? {
match self.req.headers.get(key) {
Some(v) => Some(v)
None => self.req.headers.get(key.to_lower())
}
}
///|
/// GetHeader is an alias for `header()` — for API compatibility.
/// See `c.GetHeader()` equivalent.
pub fn Context::get_header(self : Context, key : String) -> String? {
self.header(key)
}
///|
/// The client's IP address.
pub fn Context::client_ip(self : Context) -> String {
// Check X-Forwarded-For first (common proxy header)
match self.req.headers.get("x-forwarded-for") {
Some(ip) =>
// Take the first IP if there are multiple
match ip.find(",") {
Some(pos) => ip[:pos].trim().to_owned()
None => ip
}
None =>
match self.req.headers.get("x-real-ip") {
Some(ip) => ip
None => "127.0.0.1"
}
}
}
///|
/// Returns the value of the `Content-Type` request header.
pub fn Context::content_type(self : Context) -> String {
match self.header("Content-Type") {
Some(ct) => ct
None => ""
}
}
///|
/// Check if the request is a WebSocket upgrade request.
pub fn Context::is_websocket(self : Context) -> Bool {
match self.header("Connection") {
Some(conn) =>
conn.to_lower() == "upgrade" &&
(match self.header("Upgrade") {
Some(up) => up.to_lower() == "websocket"
None => false
})
None => false
}
}
///| ——————————————————————————————————————————————————————————————————————
/// Cookie helpers
///| ——————————————————————————————————————————————————————————————————————
///|
/// Get a cookie value by name from the request headers.
///
/// ```
/// let session = ctx.cookie("session_id")
/// ```
pub fn Context::cookie(self : Context, name : String) -> String? {
let cookie_header = match self.header("Cookie") {
Some(c) => c
None => return None
}
// Parse "name1=value1; name2=value2"
let parts = cookie_header.split(";")
for part in parts {
let trimmed = part.trim()
match trimmed.find("=") {
Some(pos) => {
let key = trimmed[:pos].trim().to_owned()
if key == name {
return Some(trimmed[pos + 1:].trim().to_owned())
}
}
None => ()
}
}
None
}
///|
/// Set a cookie in the response headers.
///
/// ```
/// ctx.set_cookie("session_id", "abc123", max_age=3600, path="/", http_only=true)
/// ```
pub fn Context::set_cookie(
self : Context,
name : String,
value : String,
max_age~ : Int = 0,
path~ : String = "/",
domain~ : String = "",
secure~ : Bool = false,
http_only~ : Bool = false,
same_site~ : String = "",
) -> Unit {
let mut cookie = name + "=" + value
if path != "" {
cookie = cookie + "; Path=" + path
}
if domain != "" {
cookie = cookie + "; Domain=" + domain
}
if max_age > 0 {
cookie = cookie + "; Max-Age=" + max_age.to_string()
}
if secure {
cookie = cookie + "; Secure"
}
if http_only {
cookie = cookie + "; HttpOnly"
}
if same_site != "" {
cookie = cookie + "; SameSite=" + same_site
}
self.set_header("Set-Cookie", cookie)
}
///| ——————————————————————————————————————————————————————————————————————
/// Path parameters
///| ——————————————————————————————————————————————————————————————————————
///|
/// Get a path parameter by name.
/// Route patterns like `/api/article/:id` populate `ctx.param("id")`.
pub fn Context::param(self : Context, key : String) -> String? {
self.params.get(key)
}
///|
/// Get a path parameter, returning a default value if not present.
pub fn Context::param_default(
self : Context,
key : String,
default : String,
) -> String {
match self.params.get(key) {
Some(v) => v
None => default
}
}
///|
/// Get a path parameter parsed as Int64.
pub fn Context::param_int64(self : Context, key : String) -> Int64? {
match self.params.get(key) {
Some(v) =>
try { Some(@string.parse_int64(v)) } catch { _ => None }
None => None
}
}
///|
/// All parsed path parameters.
pub fn Context::params_all(self : Context) -> Map[String, String] {
self.params
}
///| ——————————————————————————————————————————————————————————————————————
/// Query parameters (lazy)
///| ——————————————————————————————————————————————————————————————————————
///|
/// Parse query string on first access and cache for subsequent calls.
fn Context::parse_query(self : Context) -> Unit {
match self.query_cache {
Some(_) => ()
None => {
let qs = match self.req.path.find("?") {
Some(pos) => self.req.path[pos + 1:].to_owned()
None => ""
}
let map : Map[String, String] = Map([])
if qs != "" {
let pairs = qs.split("&")
for pair in pairs {
match pair.find("=") {
Some(i) => {
let k = url_decode(pair[:i].to_owned())
let v = url_decode(pair[i + 1:].to_owned())
map.set(k, v)
}
None => ()
}
}
}
self.query_cache = Some(map)
}
}
}
///|
/// Get a query parameter by name.
pub fn Context::query(self : Context, key : String) -> String? {
self.parse_query()
match self.query_cache {
Some(map) => map.get(key)
None => None
}
}
///|
/// Get a query parameter with a default fallback.
pub fn Context::query_default(
self : Context,
key : String,
default : String,
) -> String {
match self.query(key) {
Some(v) => v
None => default
}
}
///|
/// Get a query parameter parsed as Int64.
pub fn Context::query_int64(self : Context, key : String) -> Int64? {
match self.query(key) {
Some(v) =>
try { Some(@string.parse_int64(v)) } catch { _ => None }
None => None
}
}
///|
/// Get a query parameter parsed as Int.
pub fn Context::query_int(self : Context, key : String) -> Int? {
match self.query(key) {
Some(v) =>
try { Some(@string.parse_int(v)) } catch { _ => None }
None => None
}
}
///| ——————————————————————————————————————————————————————————————————————
/// Post form parameters (application/x-www-form-urlencoded body)
///| ——————————————————————————————————————————————————————————————————————
///|
/// Get a value from the POST form body.
/// Parses the body as application/x-www-form-urlencoded on first access.
pub async fn Context::post_form(self : Context, key : String) -> String? {
self.parse_post_form()
match self.post_form_cache {
Some(map) => map.get(key)
None => None
}
}
///|
/// Get a post form value with a default fallback.
pub async fn Context::default_post_form(
self : Context,
key : String,
default : String,
) -> String {
match self.post_form(key) {
Some(v) => v
None => default
}
}
///|
/// Parse the request body as URL-encoded form data (lazy, cached).
async fn Context::parse_post_form(self : Context) -> Unit {
match self.post_form_cache {
Some(_) => ()
None => {
let raw = self.body_string()
let map : Map[String, String] = Map([])
if raw != "" {
let pairs = raw.split("&")
for pair in pairs {
match pair.find("=") {
Some(i) => {
let k = url_decode(pair[:i].to_owned())
let v = url_decode(pair[i + 1:].to_owned())
map.set(k, v)
}
None => map.set(url_decode(pair.to_owned()), "")
}
}
}
self.post_form_cache = Some(map)
}
}
}
///| ——————————————————————————————————————————————————————————————————————
/// Middleware chain (middleware chain pattern)
///| ——————————————————————————————————————————————————————————————————————
///|
/// Next should be called only inside middleware.
/// It executes the remaining handlers in the chain.
///
/// This is the heart of the middleware chain pattern:
/// - Middleware runs code BEFORE `c.next()`
/// - `c.next()` yields to the next handler and waits for it
/// - Middleware runs code AFTER `c.next()` returns
pub async fn Context::next(self : Context) -> Unit {
self.index = self.index + 1
if self.index < self.handlers.length() && !self.aborted {
let h = self.handlers[self.index]
h(self)
}
}
///|
/// Abort the handler chain. No further handlers will be executed.
/// Does NOT write a response — you should call a render method separately.
pub fn Context::abort(self : Context) -> Unit {
self.aborted = true
}
///|
/// Abort with a specific status code and JSON error message.
pub async fn Context::abort_with_status(
self : Context,
code : Int,
message : String,
) -> Unit {
self.aborted = true
let body : Json = Json::object({
"error": Json::string(message),
"code": Json::number(code.to_double()),
})
self.json(code, body)
}
///|
/// Whether the chain has been aborted.
pub fn Context::is_aborted(self : Context) -> Bool {
self.aborted
}
///| ——————————————————————————————————————————————————————————————————————
/// Key-value store (for middleware data sharing)
///| ——————————————————————————————————————————————————————————————————————
///|
/// Store a value in the context. Useful for middleware to pass data to handlers.
/// Example: auth middleware stores `ctx.set("user_id", json)`.
pub fn Context::set(self : Context, key : String, value : Json) -> Unit {
self.store.set(key, value)
}
///|
/// Retrieve a value from the context store.
pub fn Context::get(self : Context, key : String) -> Json? {
self.store.get(key)
}
///|
/// Retrieve a string value from the context store.
pub fn Context::get_string(self : Context, key : String) -> String? {
match self.store.get(key) {
Some(Json::String(s)) => Some(s)
_ => None
}
}
///|
/// Retrieve an Int64 value from the context store.
pub fn Context::get_int64(self : Context, key : String) -> Int64? {
match self.store.get(key) {
Some(Json::Number(n, ..)) => Some(n.to_int64())
_ => None
}
}
///|
/// GetTime retrieves a time value (stored as Int64 nanoseconds) from the store.
/// See `c.GetTime()` equivalent.
pub fn Context::get_time(self : Context, key : String) -> Int64? {
self.get_int64(key)
}
///|
/// GetDuration retrieves a duration value (stored as Int64 nanoseconds) from the store.
/// See `c.GetDuration()` equivalent.
pub fn Context::get_duration(self : Context, key : String) -> Int64? {
self.get_int64(key)
}
///|
/// GetStringMap returns a string map from the store.
/// See `c.GetStringMap()` equivalent.
pub fn Context::get_string_map(
self : Context,
key : String,
) -> Map[String, String]? {
match self.store.get(key) {
Some(Json::Object(map)) => {
let result : Map[String, String] = Map([])
for k, v in map {
match v {
Json::String(s) => result.set(k, s)
_ => ()
}
}
Some(result)
}
_ => None
}
}
///|
/// GetStringMapString returns a flat string map from the store.
/// See `c.GetStringMapString()` equivalent.
pub fn Context::get_string_map_string(
self : Context,
key : String,
) -> Map[String, String]? {
self.get_string_map(key)
}
///|
/// Retrieve a Bool value from the context store.
pub fn Context::get_bool(self : Context, key : String) -> Bool? {
match self.store.get(key) {
Some(Json::True) => Some(true)
Some(Json::False) => Some(false)
_ => None
}
}
///|
/// Retrieve a Float64 value from the context store.
pub fn Context::get_float64(self : Context, key : String) -> Double? {
match self.store.get(key) {
Some(Json::Number(n, ..)) => Some(n)
_ => None
}
}
///|
/// MustGet returns the value for the given key if it exists, otherwise raises
/// a catchable `Failure` (recovered to 500 by `recovery()`). Do NOT use
/// `abort()` here — it is a hard panic that terminates the whole process.
pub async fn Context::must_get(self : Context, key : String) -> Json {
match self.store.get(key) {
Some(v) => v
None => raise(Failure("Key \{key} does not exist in context store"))
}
}
///| ——————————————————————————————————————————————————————————————————————
/// Response rendering
///| ——————————————————————————————————————————————————————————————————————
///|
/// Set the HTTP status code for the response.
pub fn Context::status(self : Context, code : Int) -> Unit {
self.status_code = code
}
///|
/// Set a response header.
pub fn Context::set_header(self : Context, key : String, value : String) -> Unit {
self.headers.set(key, value)
}
///|
/// SetAccepted sets the accepted content types for content negotiation.
/// See `c.SetAccepted()` equivalent.
pub fn Context::set_accepted(self : Context, formats : Array[String]) -> Unit {
self.set_header("Accept", formats[0])
}
///|
/// Content negotiation: returns the best accepted content type from the
/// `Accept` header. If none matches, sets a 406 status and returns `None`.
///
/// ```
/// match ctx.negotiate_format(["application/json", "text/html"]) {
/// Some("application/json") => ctx.json(200, data)
/// Some("text/html") => ctx.html(200, "Hello
")
/// _ => () // 406 already set
/// }
/// ```
pub async fn Context::negotiate_format(
self : Context,
offered : Array[String],
) -> String? {
let accept = match self.header("Accept") {
Some(a) => a
None => {
// No Accept header — return the first offered type
if offered.length() > 0 {
return Some(offered[0])
}
return None
}
}
// Parse Accept header: "text/html, application/json;q=0.9, */*;q=0.8"
// Simple implementation: exact match
for offer in offered {
if accept.find(offer) is Some(_) {
return Some(offer)
}
}
// Check for wildcard
if accept.find("*/*") is Some(_) && offered.length() > 0 {
return Some(offered[0])
}
// No match — 406 Not Acceptable
self.abort_with_status(406, "Not Acceptable")
None
}
///|
/// Render JSON. This finalizes the response; no further writes are allowed.
pub async fn Context::json(self : Context, code : Int, data : Json) -> Unit {
if self.written {
return
}
self.written = true
self.status_code = code
let all_headers = self.headers
all_headers.set("Content-Type", "application/json")
self.conn.send_response(code, status_text(code), extra_headers=all_headers)
self.conn.write_body(data.stringify())
self.conn.end_response()
}
///|
/// Render a plain text string.
pub async fn Context::string(self : Context, code : Int, s : String) -> Unit {
if self.written {
return
}
self.written = true
self.status_code = code
let all_headers = self.headers
all_headers.set("Content-Type", "text/plain; charset=utf-8")
self.conn.send_response(code, status_text(code), extra_headers=all_headers)
self.conn.write_body(s)
self.conn.end_response()
}
///|
/// StringF renders a formatted string response (printf-style).
/// Supports simple {0}, {1} placeholders for variable substitution.
///
/// ```
/// ctx.string_f(200, "Hello {0}, you are {1} years old", ["Alice", "30"])
/// ```
pub async fn Context::string_f(
self : Context,
code : Int,
template : String,
args : Array[String],
) -> Unit {
let mut result = template
for i = 0; i < args.length(); i = i + 1 {
let placeholder = "{" + i.to_string() + "}"
result = result.replace_all(old=placeholder, new=args[i])
}
self.string(code, result)
}
///|
/// Render HTML.
pub async fn Context::html(self : Context, code : Int, html : String) -> Unit {
if self.written {
return
}
self.written = true
self.status_code = code
let all_headers = self.headers
all_headers.set("Content-Type", "text/html; charset=utf-8")
self.conn.send_response(code, status_text(code), extra_headers=all_headers)
self.conn.write_body(html)
self.conn.end_response()
}
///|
/// Render an HTML template response using the global template store.
pub async fn Context::html_template(
self : Context,
code : Int,
name : String,
data : Map[String, String],
) -> Unit {
match @template.render_template(name, data) {
Some(html) => self.html(code, html)
None => self.abort_with_status(500, "Template not found: " + name)
}
}
///|
/// Send a response with custom content type.
pub async fn Context::data(
self : Context,
code : Int,
content_type : String,
body : String,
) -> Unit {
if self.written {
return
}
self.written = true
self.status_code = code
let all_headers = self.headers
all_headers.set("Content-Type", content_type)
self.conn.send_response(code, status_text(code), extra_headers=all_headers)
self.conn.write_body(body)
self.conn.end_response()
}
///|
/// Redirect to another URL.
pub async fn Context::redirect(
self : Context,
code : Int,
location : String,
) -> Unit {
if self.written {
return
}
self.written = true
self.status_code = code
let all_headers = self.headers
all_headers.set("Location", location)
self.conn.send_response(code, status_text(code), extra_headers=all_headers)
self.conn.end_response()
}
///|
/// Render XML. Converts a JSON object to XML format.
/// The JSON keys become XML tag names, values become text content.
///
/// ```
/// ctx.xml(200, Json::object(H([
/// ("user", Json::object(H([
/// ("name", Json::string("Alice")),
/// ("age", Json::string("30")),
/// ]))),
/// ])))
/// // Output: Alice30
/// ```
pub async fn Context::xml(
self : Context,
code : Int,
data : Json,
root_tag~ : String = "root",
) -> Unit {
if self.written {
return
}
self.written = true
self.status_code = code
let all_headers = self.headers
all_headers.set("Content-Type", "application/xml; charset=utf-8")
self.conn.send_response(code, status_text(code), extra_headers=all_headers)
let xml = json_to_xml(root_tag, data)
self.conn.write_body(xml)
self.conn.end_response()
}
///|
/// Render YAML. Converts a JSON object to YAML format.
///
/// ```
/// ctx.yaml(200, Json::object(H([
/// ("name", Json::string("Alice")),
/// ("age", Json::string("30")),
/// ])))
/// // Output:
/// // name: Alice
/// // age: "30"
/// ```
pub async fn Context::yaml(
self : Context,
code : Int,
data : Json,
) -> Unit {
if self.written {
return
}
self.written = true
self.status_code = code
let all_headers = self.headers
all_headers.set("Content-Type", "application/x-yaml; charset=utf-8")
self.conn.send_response(code, status_text(code), extra_headers=all_headers)
let yaml = json_to_yaml(data, indent=0)
self.conn.write_body(yaml)
self.conn.end_response()
}
///|
/// Render indented / pretty-printed JSON.
/// Same as `json()` but with indentation for readability.
pub async fn Context::indented_json(
self : Context,
code : Int,
data : Json,
) -> Unit {
if self.written {
return
}
self.written = true
self.status_code = code
let all_headers = self.headers
all_headers.set("Content-Type", "application/json")
self.conn.send_response(code, status_text(code), extra_headers=all_headers)
// Try to produce indented JSON if the parser supports it
let raw = data.stringify()
let indented = pretty_json(raw)
self.conn.write_body(indented)
self.conn.end_response()
}
///|
/// Render SecureJSON — prepends `while(1);` to prevent JSON hijacking.
/// See `c.SecureJSON()` equivalent.
pub async fn Context::secure_json(
self : Context,
code : Int,
data : Json,
) -> Unit {
if self.written {
return
}
self.written = true
self.status_code = code
let all_headers = self.headers
all_headers.set("Content-Type", "application/json")
self.conn.send_response(code, status_text(code), extra_headers=all_headers)
self.conn.write_body("while(1);" + data.stringify())
self.conn.end_response()
}
///|
/// Render PureJSON — writes JSON without escaping HTML characters.
/// (In MoonBit, the default JSON stringify does not escape HTML, so this is
/// equivalent to `json()` — included for API completeness.)
pub async fn Context::pure_json(self : Context, code : Int, data : Json) -> Unit {
self.json(code, data)
}
///|
/// Render JSONP — wraps JSON in a callback function.
/// The callback name is taken from the `callback` query parameter.
///
/// See `c.JSONP()` equivalent.
pub async fn Context::jsonp(self : Context, code : Int, data : Json) -> Unit {
if self.written {
return
}
self.written = true
self.status_code = code
let callback = self.query_default("callback", "callback")
let all_headers = self.headers
all_headers.set("Content-Type", "application/javascript")
self.conn.send_response(code, status_text(code), extra_headers=all_headers)
self.conn.write_body(callback + "(" + data.stringify() + ")")
self.conn.end_response()
}
///|
/// Send a file as an attachment (forces download with Content-Disposition).
///
/// See `c.FileAttachment()` equivalent.
pub async fn Context::file_attachment(
self : Context,
file_path : String,
filename~ : String = "",
) -> Unit {
if self.written {
return
}
self.written = true
self.status_code = 200
let name = if filename == "" {
// Extract filename from path
match file_path.rev_find("/") {
Some(pos) => file_path[pos + 1:].to_owned()
None => file_path
}
} else {
filename
}
let ct = guess_content_type(file_path)
let all_headers = self.headers
all_headers.set("Content-Type", ct)
all_headers.set(
"Content-Disposition",
"attachment; filename=\"" + name + "\"",
)
self.conn.send_response(200, "OK", extra_headers=all_headers)
try {
let content = @fs.read_file(file_path).text()
self.conn.write_body(content)
} catch {
_ => ()
}
self.conn.end_response()
}
///|
/// HandlerName returns the name of the last handler in the chain.
/// Useful for debugging and logging.
pub fn Context::handler_name(self : Context) -> String {
if self.handlers.length() > 0 {
let last_idx = self.handlers.length() - 1
// Return a simple identifier — in MoonBit we use index as name
"handler_" + last_idx.to_string()
} else {
"unknown"
}
}
///|
/// HandlerNames returns the names of all handlers in the chain.
pub fn Context::handler_names(self : Context) -> Array[String] {
let names : Array[String] = []
for i = 0; i < self.handlers.length(); i = i + 1 {
names.push("handler_" + i.to_string())
}
names
}
///|
/// Simple JSON pretty-printing — adds indentation and newlines.
fn pretty_json(raw : String) -> String {
let mut result = ""
let mut indent = 0
let mut i = 0
let chars = raw.to_array()
while i < chars.length() {
let ch = chars[i]
match ch {
'{' | '[' => {
result = result + ch.to_string() + "\n"
indent = indent + 2
result = result + " ".repeat(indent)
}
'}' | ']' => {
indent = indent - 2
if indent < 0 {
indent = 0
}
result = result + "\n" + " ".repeat(indent) + ch.to_string()
}
',' => result = result + ",\n" + " ".repeat(indent)
':' => result = result + ": "
_ => result = result + ch.to_string()
}
i = i + 1
}
result
}
///|
/// Send a 204 No Content response.
pub async fn Context::no_content(self : Context) -> Unit {
if self.written {
return
}
self.written = true
self.status_code = 204
let all_headers = self.headers
self.conn.send_response(204, "No Content", extra_headers=all_headers)
self.conn.end_response()
}
///|
/// FileFromFS serves a file from a filesystem path.
/// See `c.FileFromFS()` equivalent.
pub async fn Context::file_from_fs(self : Context, file_path : String) -> Unit {
self.file(200, guess_content_type(file_path), file_path)
}
///|
/// Send a file as the response body with the given content type.
/// The file content is read from disk and sent inline.
///
/// ```
/// ctx.file(200, "application/pdf", "./reports/annual.pdf")
/// ```
pub async fn Context::file(
self : Context,
code : Int,
content_type : String,
file_path : String,
) -> Unit {
if self.written {
return
}
self.written = true
self.status_code = code
let all_headers = self.headers
all_headers.set("Content-Type", content_type)
self.conn.send_response(code, status_text(code), extra_headers=all_headers)
// Read and stream file content
try {
let content = @fs.read_file(file_path).text()
self.conn.write_body(content)
} catch {
_ => ()
}
self.conn.end_response()
}
///|
/// Stream a Server-Sent Event (SSE) to the client.
/// Sets the appropriate headers for SSE.
///
/// ```
/// // Basic SSE
/// ctx.sse("message", "hello world")
/// ```
pub async fn Context::sse(self : Context, event : String, data : String) -> Unit {
self.sse_ex(event, data~, id="", retry=0)
}
///|
/// Backward-compatible alias for `sse()`.
pub async fn Context::stream_sse(
self : Context,
event : String,
data : String,
) -> Unit {
self.sse(event, data)
}
///|
/// SSE with full options: id, retry, comment.
pub async fn Context::sse_ex(
self : Context,
event : String,
data~ : String,
id~ : String = "",
retry~ : Int = 0,
comment~ : String = "",
) -> Unit {
let mut payload = ""
// Comment (for keep-alive pings or debugging)
if comment != "" {
let lines = comment.split("\n")
for line in lines {
payload = payload + ": " + line.to_owned() + "\n"
}
}
// Event ID — allows client to resume from last received event
if id != "" {
payload = payload + "id: " + id + "\n"
}
// Retry — reconnection time in milliseconds
if retry > 0 {
payload = payload + "retry: " + retry.to_string() + "\n"
}
// Event type
if event != "" {
payload = payload + "event: " + event + "\n"
}
// Data (supports multi-line: each line becomes "data: ...")
let data_lines = data.split("\n")
for line in data_lines {
payload = payload + "data: " + line.to_owned() + "\n"
}
// Double newline terminates the event
payload = payload + "\n"
if self.written {
// After initial SSE headers, subsequent writes are fine
self.conn.write_body(payload)
return
}
self.written = true
self.status_code = 200
let all_headers = self.headers
all_headers.set("Content-Type", "text/event-stream")
all_headers.set("Cache-Control", "no-cache")
all_headers.set("Connection", "keep-alive")
all_headers.set("X-Accel-Buffering", "no") // Disable nginx buffering
self.conn.send_response(200, "OK", extra_headers=all_headers)
self.conn.write_body(payload)
}
///|
/// SSE keep-alive — sends a comment-only event to prevent connection timeout.
/// Useful for long-lived SSE connections.
///
/// ```
/// ctx.sse_keepalive("ping")
/// ```
pub async fn Context::sse_keepalive(self : Context, comment : String) -> Unit {
self.sse_ex("", data="", comment~)
}
///|
/// Flush the SSE response — sends buffered data to the client immediately.
pub async fn Context::sse_flush(self : Context) -> Unit {
self.conn.flush()
}
///| ——————————————————————————————————————————————————————————————————————
/// Streaming responses
///| ——————————————————————————————————————————————————————————————————————
///|
/// Stream writes a streaming response with the given status code and
/// content type. The `writer` callback receives a function that writes
/// chunks to the connection.
///
/// ```
/// ctx.stream(200, "text/event-stream", fn(write) {
/// for i = 0; i < 10; i = i + 1 {
/// write("data: chunk " + i.to_string() + "\n\n")
/// }
/// })
/// ```
pub async fn Context::stream(
self : Context,
code : Int,
content_type : String,
writer : async (async (String) -> Unit) -> Unit,
) -> Unit {
if self.written {
return
}
self.written = true
self.status_code = code
let all_headers = self.headers
all_headers.set("Content-Type", content_type)
all_headers.set("Transfer-Encoding", "chunked")
all_headers.set("X-Content-Type-Options", "nosniff")
self.conn.send_response(code, status_text(code), extra_headers=all_headers)
// The writer callback sends chunks
writer(async fn(chunk : String) {
self.conn.write_body(chunk)
self.size = self.size + chunk.length()
})
self.conn.end_response()
}
///|
/// DataFromReader writes the specified content type and reads data
/// from the given reader, sending it as the response body.
///
/// ```
/// ctx.data_from_reader(200, content_length, "application/octet-stream", reader)
/// ```
pub async fn Context::data_from_reader(
self : Context,
code : Int,
content_length : Int64,
content_type : String,
reader : &@io.Reader,
) -> Unit {
if self.written {
return
}
self.written = true
self.status_code = code
let all_headers = self.headers
all_headers.set("Content-Type", content_type)
all_headers.set("Content-Length", content_length.to_string())
self.conn.send_response(code, status_text(code), extra_headers=all_headers)
// Read everything from the reader and write it to the connection
let s = (reader.read_all().text()) catch { _ => "" }
self.conn.write_body(s)
self.size = self.size + s.length()
self.conn.end_response()
}
///| ——————————————————————————————————————————————————————————————————————
/// Response headers (bulk)
///| ——————————————————————————————————————————————————————————————————————
///|
/// Set multiple response headers at once.
pub fn Context::set_headers(self : Context, hdrs : Map[String, String]) -> Unit {
for k, v in hdrs {
self.headers.set(k, v)
}
}
///| ——————————————————————————————————————————————————————————————————————
/// Request body
///| ——————————————————————————————————————————————————————————————————————
///|
/// Read the full request body as raw bytes (lazy, cached).
/// Unlike `body_string()`, this preserves arbitrary binary content and never
/// fails on non-UTF-8 payloads (e.g. binary file uploads).
pub async fn Context::body_bytes(self : Context) -> Bytes {
match self.body_bytes {
Some(b) => b
None => {
let b = self.reader.read_all().binary()
self.body_bytes = Some(b)
b
}
}
}
///|
/// Read the full request body as a string (lazy, cached).
/// For binary bodies use `body_bytes()`; text decoding of non-UTF-8 payloads
/// yields an empty string (same as before).
pub async fn Context::body_string(self : Context) -> String {
match self.body {
Some(b) => b
None => {
let s = try { @encoding/utf8.decode(self.body_bytes().view()) } catch { _ => "" }
self.body = Some(s)
s
}
}
}
///|
/// Find `pat` in `b` starting at byte offset `from`.
fn bytes_find_from(b : Bytes, from : Int, pat : Bytes) -> Int? {
if from > b.length() {
return None
}
let rest = match b.get_view(start=from, end=b.length()) {
Some(v) => v.to_owned()
None => return None
}
match rest.find(pat) {
Some(rel) => Some(from + rel)
None => None
}
}
///|
/// Return a copy of `b[start:start+len]`.
fn bytes_slice(b : Bytes, start : Int, len : Int) -> Bytes {
match b.get_view(start=start, end=start + len) {
Some(v) => v.to_owned()
None => Bytes::new(0)
}
}
///|
/// Strip a single trailing CRLF or LF from a byte buffer.
fn bytes_strip_crlf(b : Bytes) -> Bytes {
if b.has_suffix(b"\r\n") {
match b.get_view(start=0, end=b.length() - 2) {
Some(v) => v.to_owned()
None => b
}
} else if b.has_suffix(b"\n") {
match b.get_view(start=0, end=b.length() - 1) {
Some(v) => v.to_owned()
None => b
}
} else {
b
}
}
///|
/// Parse the request body as JSON.
pub async fn Context::body_json(self : Context) -> Json? {
let raw = self.body_string()
try { Some(@json.parse(raw)) } catch { _ => None }
}
///|
/// Auto-detect the content type and bind the request body to a struct.
/// Supports JSON (`application/json`) and form-encoded
/// (`application/x-www-form-urlencoded`).
///
/// Returns `Some(value)` on success, `None` on failure.
pub async fn[T : @json.FromJson] Context::should_bind(self : Context) -> T? {
let ct = self.content_type()
if ct.find("application/json") is Some(_) {
bind_json(self)
} else if ct.find("application/x-www-form-urlencoded") is Some(_) {
bind_form(self)
} else {
// Default to JSON
bind_json(self)
}
}
///|
/// Like `should_bind` but raises a catchable `Failure` (recovered to 400/500 by
/// `recovery()`) if binding fails. Deliberately uses `raise(Failure(...))`
/// instead of `abort()` so a bad request cannot crash the server.
pub async fn[T : @json.FromJson] Context::must_bind(self : Context) -> T {
let bound : T? = self.should_bind()
match bound {
Some(v) => v
None => {
self.abort_with_status(400, "Invalid request body")
raise(Failure("must_bind failed"))
}
}
}
///|
/// ShouldBindJSON binds the JSON request body to a struct.
/// See `c.ShouldBindJSON()` equivalent.
pub async fn[T : @json.FromJson] Context::should_bind_json(
self : Context,
) -> T? {
bind_json(self)
}
///|
/// ShouldBindQuery binds query parameters to a struct.
/// See `c.ShouldBindQuery()` equivalent.
pub fn[T : @json.FromJson] Context::should_bind_query(self : Context) -> T? {
bind_query(self)
}
///|
/// ShouldBindForm binds form-encoded body to a struct.
pub async fn[T : @json.FromJson] Context::should_bind_form(
self : Context,
) -> T? {
bind_form(self)
}
///|
/// ShouldBindHeader binds request headers to a struct.
pub fn[T : @json.FromJson] Context::should_bind_header(self : Context) -> T? {
bind_header(self)
}
///|
/// ShouldBindUri binds URI (path) parameters to a struct.
/// See `c.ShouldBindUri()` equivalent.
pub fn[T : @json.FromJson] Context::should_bind_uri(self : Context) -> T? {
bind_path(self)
}
///|
/// ShouldBindWith binds using a custom binding function.
/// See `c.ShouldBindWith()` equivalent.
pub async fn[T] Context::should_bind_with(
self : Context,
binder : async (Context) -> T?,
) -> T? {
binder(self)
}
///|
/// MustBindWith binds using a custom binding function, raising a catchable
/// `Failure` (recovered by `recovery()`) instead of aborting on failure.
pub async fn[T] Context::must_bind_with(
self : Context,
binder : async (Context) -> T?,
) -> T {
match binder(self) {
Some(v) => v
None => {
self.abort_with_status(400, "Binding failed")
raise(Failure("must_bind_with failed"))
}
}
}
///|
/// Bind auto-detects content type and binds, aborting on failure.
/// See `c.Bind()` equivalent.
pub async fn[T : @json.FromJson] Context::bind_api(self : Context) -> T? {
self.bind_json_api()
}
///|
/// Get the raw request body as a string.
pub async fn Context::raw_data(self : Context) -> String {
self.body_string()
}
///| ——————————————————————————————————————————————————————————————————————
/// Error collection
///| ——————————————————————————————————————————————————————————————————————
///|
/// Add an error to the context's error list.
pub fn Context::add_error(self : Context, err : String) -> Unit {
self.errors.push(err)
}
///|
/// All collected errors.
pub fn Context::errors_all(self : Context) -> Array[String] {
self.errors
}
///| ——————————————————————————————————————————————————————————————————————
/// Internal helpers
///| ——————————————————————————————————————————————————————————————————————
///|
/// Set parsed path parameters (called by the router after matching).
pub fn Context::set_params(
self : Context,
params : Map[String, String],
) -> Unit {
self.params = params
}
///|
/// Whether the response has been written.
pub fn Context::is_written(self : Context) -> Bool {
self.written
}
///|
/// Returns the number of bytes written to the response so far.
/// See `c.Writer.Size()` equivalent.
pub fn Context::size(self : Context) -> Int {
self.size
}
///|
/// Flush writes any buffered data to the client.
/// See `c.Writer.Flush()` equivalent.
pub async fn Context::flush(self : Context) -> Unit {
self.conn.flush()
}
///|
/// WriteHeaderNow forces the HTTP headers to be written.
/// See `c.Writer.WriteHeaderNow()` equivalent.
pub async fn Context::write_header_now(self : Context) -> Unit {
if !self.written {
self.conn.send_response(
self.status_code,
status_text(self.status_code),
extra_headers=self.headers,
)
self.written = true
}
}
///|
/// Written returns true if the response has been written.
/// See `c.Writer.Written()` equivalent.
pub fn Context::written(self : Context) -> Bool {
self.written
}
///|
/// Reset the context state for reuse (used by HandleContext).
pub fn Context::reset(self : Context) -> Unit {
self.index = 0
self.aborted = false
self.written = false
self.status_code = 200
self.errors = []
self.headers = Map([])
}
///| ——————————————————————————————————————————————————————————————————————
/// Additional convenience methods
///| ——————————————————————————————————————————————————————————————————————
///|
/// GetRawData returns the raw request body as a string.
/// See `c.GetRawData()` equivalent.
pub async fn Context::get_raw_data(self : Context) -> String {
self.body_string()
}
///|
/// GetQueryArray returns all values for a query parameter key.
/// e.g., `?tag=a&tag=b` → `["a", "b"]`
pub fn Context::get_query_array(self : Context, key : String) -> Array[String] {
let results : Array[String] = []
// Parse raw query string for duplicate keys
let qs = match self.req.path.find("?") {
Some(pos) => self.req.path[pos + 1:].to_owned()
None => ""
}
if qs != "" {
let pairs = qs.split("&")
for pair in pairs {
match pair.find("=") {
Some(i) => {
let k = pair[:i].to_owned()
if k == key {
results.push(pair[i + 1:].to_owned())
}
}
None => ()
}
}
}
results
}
///|
/// DefaultQuery returns the query value or a default.
/// See `c.DefaultQuery()` equivalent.
pub fn Context::default_query(
self : Context,
key : String,
default : String,
) -> String {
match self.query(key) {
Some(v) => v
None => default
}
}
///|
/// QueryMap returns all query parameters as a map.
/// See `c.QueryMap()` equivalent.
pub fn Context::query_map(self : Context) -> Map[String, String] {
self.parse_query()
match self.query_cache {
Some(map) => map
None => Map([])
}
}
///|
/// PostFormMap returns all POST form parameters as a map.
pub async fn Context::post_form_map(self : Context) -> Map[String, String] {
self.parse_post_form()
match self.post_form_cache {
Some(map) => map
None => Map([])
}
}
///|
/// GetPostFormArray returns all values for a post form parameter key.
/// e.g., form with `tag=a&tag=b` → `["a", "b"]`
pub async fn Context::get_post_form_array(
self : Context,
key : String,
) -> Array[String] {
let results : Array[String] = []
let raw = self.body_string()
if raw != "" {
let pairs = raw.split("&")
for pair in pairs {
match pair.find("=") {
Some(i) => {
let k = url_decode(pair[:i].to_owned())
if k == key {
results.push(url_decode(pair[i + 1:].to_owned()))
}
}
None => ()
}
}
}
results
}
///|
/// AbortWithStatusJSON aborts with a JSON error response.
/// Convenience method combining abort + JSON error response.
pub async fn Context::abort_with_status_json(
self : Context,
code : Int,
message : String,
) -> Unit {
self.aborted = true
let body : Json = Json::object({
"error": Json::string(message),
"code": Json::number(code.to_double()),
})
self.json(code, body)
}
///| ——————————————————————————————————————————————————————————————————————
/// Multipart form file upload
///| ——————————————————————————————————————————————————————————————————————
///|
/// FormFile returns the content of an uploaded file from a multipart form.
/// The file is identified by the form field name.
///
/// Returns `Some(content)` if the file was found, `None` otherwise.
pub async fn Context::form_file(
self : Context,
field_name : String,
) -> Bytes? {
let content_type = self.content_type()
if !content_type.has_prefix("multipart/form-data") {
return None
}
// Extract boundary from Content-Type
let boundary = match content_type.find("boundary=") {
Some(pos) => {
let start = pos + "boundary=".length()
content_type[start:].to_owned()
}
None => return None
}
// Parse over raw bytes so binary file content is preserved.
let body = self.body_bytes()
let field_marker = @encoding/utf8.encode("name=\"" + field_name + "\"")
match body.find(field_marker) {
Some(field_pos) => {
// Find the blank line after headers (separates headers from content)
let after_headers = match bytes_find_from(body, field_pos, b"\r\n\r\n") {
Some(pos) => pos + 4
None =>
match bytes_find_from(body, field_pos, b"\n\n") {
Some(pos) => pos + 2
None => return None
}
}
// Find the next boundary after the content
let boundary_marker = @encoding/utf8.encode("--" + boundary)
match bytes_find_from(body, after_headers, boundary_marker) {
Some(end_pos) => {
let content = bytes_slice(body, after_headers, end_pos - after_headers)
Some(bytes_strip_crlf(content))
}
None => None
}
}
None => None
}
}
///|
/// SaveUploadedFile saves an uploaded file to the specified destination path.
/// Accepts raw `Bytes` so binary files are written verbatim.
///
/// ```
/// match ctx.form_file("avatar") {
/// Some(content) => ctx.save_uploaded_file(content, "./uploads/avatar.png")
/// None => ctx.abort_with_status(400, "No file uploaded")
/// }
/// ```
pub async fn Context::save_uploaded_file(
self : Context,
content : Bytes,
dst : String,
) -> Unit {
try {
@fs.write_file(dst, content)
} catch {
_ => self.add_error("Failed to save file to " + dst)
}
}
///|
/// GetPostFormFile returns the filename from a multipart form file upload.
/// Returns the filename extracted from the Content-Disposition header.
pub async fn Context::get_post_form_file(
self : Context,
field_name : String,
) -> String? {
let content_type = self.content_type()
if !content_type.has_prefix("multipart/form-data") {
return None
}
let body = self.body_bytes()
let field_marker = @encoding/utf8.encode("name=\"" + field_name + "\"")
match body.find(field_marker) {
Some(pos) => {
// Look for filename= in the part headers before the field marker
let header_section = bytes_slice(body, 0, pos)
match header_section.rev_find(b"filename=\"") {
Some(fn_start) => {
let after_fn = fn_start + "filename=\"".length()
match bytes_find_from(body, after_fn, b"\"") {
Some(fn_end) =>
Some(@encoding/utf8.decode(bytes_slice(body, after_fn, fn_end - after_fn).view()))
None => None
}
}
None => None
}
}
None => None
}
}
///|
/// AsciiJSON serializes JSON with ASCII-only characters (escapes non-ASCII).
pub async fn Context::ascii_json(
self : Context,
code : Int,
data : Json,
) -> Unit {
if self.written {
return
}
self.written = true
self.status_code = code
let all_headers = self.headers
all_headers.set("Content-Type", "application/json")
self.conn.send_response(code, status_text(code), extra_headers=all_headers)
let raw = data.stringify()
// Escape non-ASCII characters to \uXXXX
let ascii = escape_to_ascii(raw)
self.conn.write_body(ascii)
self.conn.end_response()
}
///|
/// Escape non-ASCII chars in a string to \uXXXX format.
fn escape_to_ascii(s : String) -> String {
let mut result = ""
let chars = s.to_array()
for i = 0; i < chars.length(); i = i + 1 {
let ch = chars[i]
if ch.to_int() <= 127 {
result = result + ch.to_string()
} else {
let hex = ch.to_int().reinterpret_as_uint().to_string(radix=16)
result = result + "\\u" + "0".repeat(4 - hex.length()) + hex
}
}
result
}
///|
/// Start a chunked stream response. The response is NOT finalized,
/// allowing multiple `write` calls afterwards; call `end_stream` to finish.
pub async fn Context::stream_start(
self : Context,
code : Int,
content_type : String,
) -> Unit {
if self.written {
return
}
self.written = true
self.status_code = code
let all_headers = self.headers
all_headers.set("Content-Type", content_type)
all_headers.set("Transfer-Encoding", "chunked")
self.conn.send_response(code, status_text(code), extra_headers=all_headers)
}
///|
/// Write raw data to an ongoing stream response.
pub async fn Context::write(self : Context, data : String) -> Unit {
self.conn.write_body(data)
}
///|
/// End a stream response.
pub async fn Context::end_stream(self : Context) -> Unit {
self.conn.end_response()
}
///|
/// AbortWithError aborts with a status code and adds an error to the context.
pub async fn Context::abort_with_error(
self : Context,
code : Int,
err : String,
) -> Unit {
self.add_error(err)
self.abort_with_status(code, err)
}
///|
///|
/// Simple JSON-to-XML conversion for flat objects.
fn json_to_xml(root : String, json : Json) -> String {
let mut result = "\n<" + root + ">"
match json {
Json::Object(map) =>
for k, v in map {
result = result + "\n <" + k + ">"
result = result + escape_xml(json_value_to_string(v))
result = result + "" + k + ">"
}
_ => result = result + escape_xml(json.stringify())
}
result + "\n" + root + ">"
}
///|
fn json_value_to_string(v : Json) -> String {
match v {
Json::String(s) => s
Json::Number(n, ..) => n.to_string()
Json::True => "true"
Json::False => "false"
Json::Null => ""
_ => v.stringify()
}
}
///|
fn escape_xml(s : String) -> String {
s
.replace_all(old="&", new="&")
.replace_all(old="<", new="<")
.replace_all(old=">", new=">")
.replace_all(old="\"", new=""")
}
///|
/// Simple JSON-to-YAML conversion. Handles nested objects.
fn json_to_yaml(json : Json, indent~ : Int = 0) -> String {
let prefix = " ".repeat(indent)
match json {
Json::Object(map) => {
if map.is_empty() {
"{}"
} else {
let mut result = ""
for k, v in map {
if result != "" {
result = result + "\n"
}
let val = match v {
Json::Object(_) => "\n" + json_to_yaml(v, indent=indent + 2)
Json::Array(arr) => {
if arr.is_empty() {
"[]"
} else {
let mut items = ""
for item in arr {
if items != "" {
items = items + ", "
}
items = items + json_value_to_string(item)
}
"[" + items + "]"
}
}
_ => json_value_to_string(v)
}
result = result + prefix + k + ": " + val
}
result
}
}
_ => json_value_to_string(json)
}
}
///| ——————————————————————————————————————————————————————————————————————
/// Remaining API completeness
///| ——————————————————————————————————————————————————————————————————————
///|
/// Keys returns all keys in the context store.
/// See `c.Keys` equivalent.
pub fn Context::keys(self : Context) -> Array[String] {
let result : Array[String] = []
for k, _ in self.store {
result.push(k)
}
result
}
///|
/// Error returns the last error in the context's error list.
/// See `c.Errors.Last()` equivalent.
pub fn Context::error(self : Context) -> String? {
if self.errors.length() > 0 {
Some(self.errors[self.errors.length() - 1])
} else {
None
}
}
///|
/// BindJSON binds the request body as JSON. On failure, aborts with 400.
/// See `c.BindJSON()` equivalent.
pub async fn[T : @json.FromJson] Context::bind_json_api(self : Context) -> T? {
let bound : T? = self.should_bind_json()
match bound {
Some(v) => Some(v)
None => {
self.abort_with_status_json(400, "Invalid JSON body")
None
}
}
}
///|
/// BindQuery binds query parameters. On failure, aborts with 400.
pub async fn[T : @json.FromJson] Context::bind_query_api(self : Context) -> T? {
let bound : T? = self.should_bind_query()
match bound {
Some(v) => Some(v)
None => {
self.abort_with_status_json(400, "Invalid query parameters")
None
}
}
}
///|
/// BindForm binds form body. On failure, aborts with 400.
pub async fn[T : @json.FromJson] Context::bind_form_api(self : Context) -> T? {
let bound : T? = self.should_bind_form()
match bound {
Some(v) => Some(v)
None => {
self.abort_with_status_json(400, "Invalid form data")
None
}
}
}
///|
/// BindHeader binds request headers. On failure, aborts with 400.
pub async fn[T : @json.FromJson] Context::bind_header_api(self : Context) -> T? {
let bound : T? = self.should_bind_header()
match bound {
Some(v) => Some(v)
None => {
self.abort_with_status_json(400, "Invalid headers")
None
}
}
}
///|
/// Render dispatches to the appropriate renderer based on content type.
/// See `c.Render()` equivalent.
pub async fn Context::render(
self : Context,
code : Int,
render_type : RenderType,
data : Json,
) -> Unit {
match render_type {
JSON => self.json(code, data)
IndentedJSON => self.indented_json(code, data)
SecureJSON => self.secure_json(code, data)
AsciiJSON => self.ascii_json(code, data)
PureJSON => self.pure_json(code, data)
JSONP => self.jsonp(code, data)
XML => self.xml(code, data)
YAML => self.yaml(code, data)
TOML => self.toml(code, data)
ProtoBuf => self.proto_buf(code, data)
}
}
///|
/// Render type enumeration for the Render() method.
pub(all) enum RenderType {
JSON
IndentedJSON
SecureJSON
AsciiJSON
PureJSON
JSONP
XML
YAML
TOML
ProtoBuf
} derive(Debug, Eq)
///|
/// MultipartForm returns the full multipart form data as parsed fields.
/// Returns a map of field name → file content for simple file uploads.
pub async fn Context::multipart_form(self : Context) -> Map[String, String] {
let content_type = self.content_type()
if !content_type.has_prefix("multipart/form-data") {
return Map([])
}
let boundary = match content_type.find("boundary=") {
Some(pos) => {
let start = pos + "boundary=".length()
content_type[start:].to_owned()
}
None => return Map([])
}
let body = self.body_bytes()
let result : Map[String, String] = Map([])
// Parse all parts over raw bytes: find each `name="..."` field and extract
// its content (binary-safe).
let boundary_marker = @encoding/utf8.encode("--" + boundary)
let mut search_from = 0
while search_from < body.length() {
match bytes_find_from(body, search_from, b"name=\"") {
Some(name_start) => {
let after_name = name_start + "name=\"".length()
match bytes_find_from(body, after_name, b"\"") {
Some(name_end) => {
let field_name = @encoding/utf8.decode(bytes_slice(body, after_name, name_end - after_name).view())
// Find content after headers
match bytes_find_from(body, name_end, b"\r\n\r\n") {
Some(content_start) => {
let content_pos = content_start + 4
match bytes_find_from(body, content_pos, boundary_marker) {
Some(end) => {
let content = bytes_slice(body, content_pos, end - content_pos)
let content = bytes_strip_crlf(content)
result.set(field_name, @encoding/utf8.decode(content.view()))
search_from = end + boundary_marker.length()
}
None => break
}
}
None => break
}
}
None => break
}
}
None => break
}
}
result
}
///|
/// TOML renders a TOML response (placeholder — returns JSON).
pub async fn Context::toml(self : Context, code : Int, obj : Json) -> Unit {
if self.written {
return
}
self.written = true
self.status_code = code
let all_headers = self.headers
all_headers.set("Content-Type", "application/toml; charset=utf-8")
self.conn.send_response(code, status_text(code), extra_headers=all_headers)
self.conn.write_body(obj.stringify())
self.conn.end_response()
}
///|
/// ProtoBuf renders a protobuf response (placeholder — returns JSON).
pub async fn Context::proto_buf(self : Context, code : Int, obj : Json) -> Unit {
if self.written {
return
}
self.written = true
self.status_code = code
let all_headers = self.headers
all_headers.set("Content-Type", "application/protobuf")
self.conn.send_response(code, status_text(code), extra_headers=all_headers)
self.conn.write_body(obj.stringify())
self.conn.end_response()
}