///|
/// A MoonBit web framework with an Express-like API, served natively by
/// µWebSockets through `src/uws_shim.cpp`.
///
/// µWebSockets matches one catch-all route (`/*`). Everything Express-shaped -
/// middleware order, route patterns, params, errors, 404 - is resolved here in
/// MoonBit, while the HTTP engine, parser and event loop stay in µWebSockets.

// ---------- global registry ----------

///|
/// Registered applications, keyed by the id handed to the shim.
let g_apps : Map[UInt64, App] = Map([])

///|
/// Per-request state, keyed by the id handed to the shim.
let g_requests : Map[UInt64, RequestState] = Map([])

///|
/// Monotonic id source for apps and requests.
let g_next_id : Array[UInt64] = [1]

///|
fn next_id() -> UInt64 {
  let id = g_next_id[0]
  g_next_id[0] = id + 1
  id
}

///|
priv struct RequestState {
  req : Req
  res : Res
  app : App
  mut body_reader : ((String, Bool) -> Unit)?
}

// ---------- Request ----------

///|
/// Identifies the route currently handling the request, mirroring
/// `req.route` in Express.
pub struct RouteInfo {
  http_method : String
  path : String
}

///|
/// The method of the matched route. `method` is a reserved word in MoonBit,
/// so the accessor is declared as `http_method` and exposed under the Express
/// name with `#alias`.
#alias(method)
pub fn RouteInfo::http_method(self : RouteInfo) -> String {
  self.http_method
}

///|
pub struct Req {
  priv uws_res : UwsRes
  priv request_id : UInt64
  priv app_ref : App
  priv settings : Map[String, String]
  priv headers : Map[String, String]
  priv http_method : String
  priv mut url_path : String
  priv mut base_url : String
  priv original_url : String
  priv mut params : Map[String, String]
  priv mut base_params : Array[(String, String)]
  priv mut route_info : RouteInfo?
  priv mut next : Next?
  priv query_params : Map[String, String]
  priv query_raw : String
  priv mut body_text : String
  priv mut body_bytes : Bytes
  priv mut body_json : Json?
  priv mut body_form : Map[String, String]
  priv mut body_form_json : Json?
  priv cookies : Map[String, String]
  priv remote_ip : String
  priv mut aborted : Bool
  priv locals : Map[String, String]
  priv protocol : String
}

///|
/// The HTTP method of the request in uppercase (`"GET"`, `"POST"`, ...).
///
/// This is `req.method` in Express. `method` is a reserved word in MoonBit,
/// so the function is declared as `http_method` and exposed under the Express
/// name with `#alias`.
#alias(method)
pub fn Req::http_method(self : Req) -> String {
  self.http_method
}

///|
pub fn Req::path(self : Req) -> String {
  self.url_path
}

///|
/// The URL of the request relative to the current mount point, path plus
/// query string, like `req.url` inside an Express router.
pub fn Req::url(self : Req) -> String {
  if self.query_raw == "" {
    self.url_path
  } else {
    self.url_path + "?" + self.query_raw
  }
}

///|
/// The full URL of the original request, `req.originalUrl`.
pub fn Req::original_url(self : Req) -> String {
  self.original_url
}

///|
/// The mount path of the current router, `""` at the app level.
pub fn Req::base_url(self : Req) -> String {
  self.base_url
}

///|
/// `req.xhr`: whether the request was issued by an XMLHttpRequest-style
/// client (`X-Requested-With: XMLHttpRequest`).
pub fn Req::xhr(self : Req) -> Bool {
  self.header("x-requested-with").to_lower() == "xmlhttprequest"
}

///|
/// The route currently handling the request, like `req.route`.
pub fn Req::route(self : Req) -> RouteInfo? {
  self.route_info
}

///|
/// The application that owns this request, like `req.app`.
pub fn Req::app(self : Req) -> App {
  self.app_ref
}

///|
/// The remote TCP port of the request, like `req.socket.remotePort`.
pub fn Req::remote_port(self : Req) -> Int32 {
  uwb_res_remote_port(self.uws_res, self.ssl_flag())
}

///|
fn Req::ssl_flag(self : Req) -> Int32 {
  if self.protocol == "https" {
    1
  } else {
    0
  }
}

///|
fn Req::setting(self : Req, key : String) -> String {
  match self.settings.get(key) {
    Some(value) => value
    None => ""
  }
}

///|
fn Req::set_next(self : Req, value : Next) -> Unit {
  self.next = Some(value)
}

///|
fn Req::set_route(self : Req, value : RouteInfo?) -> Unit {
  self.route_info = value
}

///|
/// Returns a route parameter, or `""` when it is not present.
pub fn Req::param(self : Req, name : String) -> String {
  match self.params.get(name) {
    Some(value) => value
    None => ""
  }
}

///|
pub fn Req::has_param(self : Req, name : String) -> Bool {
  self.params.contains(name)
}

///|
pub fn Req::params(self : Req) -> Map[String, String] {
  self.params
}

///|
/// Returns a parsed query parameter, or `""` when it is not present.
pub fn Req::query_param(self : Req, name : String) -> String {
  match self.query_params.get(name) {
    Some(value) => value
    None => ""
  }
}

///|
pub fn Req::query_map(self : Req) -> Map[String, String] {
  self.query_params
}

///|
/// The raw query string, without the leading `?`.
pub fn Req::query(self : Req) -> String {
  self.query_raw
}

///|
pub fn Req::raw_query(self : Req) -> String {
  self.query_raw
}

///|
pub fn Req::header(self : Req, name : String) -> String {
  match self.headers.get(name.to_lower()) {
    Some(value) => value
    None => ""
  }
}

///|
pub fn Req::get(self : Req, name : String) -> String {
  self.header(name)
}

///|
pub fn Req::body(self : Req) -> String {
  self.body_text
}

///|
pub fn Req::json_body(self : Req) -> Json? {
  self.body_json
}

///|
/// The raw request body bytes. Always binary safe; `express_raw` fills it.
pub fn Req::body_bytes(self : Req) -> Bytes {
  self.body_bytes
}

///|
/// Form data parsed by `express_urlencoded(extended=true)`, with nested
/// objects and arrays, or `None` for the flat parser.
pub fn Req::form_json(self : Req) -> Json? {
  self.body_form_json
}

///|
pub fn Req::form(self : Req, name : String) -> String {
  match self.body_form.get(name) {
    Some(value) => value
    None => ""
  }
}

///|
pub fn Req::form_map(self : Req) -> Map[String, String] {
  self.body_form
}

///|
/// Returns a request cookie value, or `""` when it is not present.
pub fn Req::cookie(self : Req, name : String) -> String {
  match self.cookies.get(name) {
    Some(value) => value
    None => ""
  }
}

///|
pub fn Req::cookies(self : Req) -> Map[String, String] {
  self.cookies
}

///|
/// The client address. Follows `X-Forwarded-For` when `trust proxy` is set,
/// mirroring `req.ip`.
pub fn Req::ip(self : Req) -> String {
  let addresses = self.forwarded_addresses()
  let limit = self.trusted_address_count(addresses)
  addresses[limit]
}

///|
/// The `X-Forwarded-For` chain, farthest first, like `req.ips`. Empty when no
/// proxy is trusted.
pub fn Req::ips(self : Req) -> Array[String] {
  let addresses = self.forwarded_addresses()
  let limit = self.trusted_address_count(addresses)
  let result : Array[String] = []
  let mut index = limit
  while index >= 1 {
    result.push(addresses[index])
    index = index - 1
  }
  result
}

///|
/// `[socket, ...X-Forwarded-For reversed]`, closest hop first.
fn Req::forwarded_addresses(self : Req) -> Array[String] {
  let result : Array[String] = [self.remote_ip]
  let forwarded = self.header("x-forwarded-for")
  if forwarded != "" {
    let hops : Array[String] = []
    for raw in forwarded.split(",") {
      let hop = raw.to_owned().trim().to_owned()
      if hop != "" {
        hops.push(hop)
      }
    }
    let mut index = hops.length() - 1
    while index >= 0 {
      result.push(hops[index])
      index = index - 1
    }
  }
  result
}

///|
/// Index of the first untrusted address in the forwarded chain, which is the
/// closest one to the server that `req.ip` reports.
fn Req::trusted_address_count(self : Req, addresses : Array[String]) -> Int32 {
  let setting = self.setting("trust proxy")
  let mut index = 0
  while index < addresses.length() - 1 {
    if !trusted_proxy(setting, addresses[index], index) {
      return index
    }
    index = index + 1
  }
  addresses.length() - 1
}

///|
/// Evaluates the `trust proxy` setting against one hop. Accepts `true`,
/// `false`, a hop count, or a comma-separated list of IPs and the predefined
/// ranges `loopback`, `linklocal` and `uniquelocal`.
fn trusted_proxy(setting : String, address : String, index : Int32) -> Bool {
  let setting = setting.trim().to_owned()
  if setting == "" || setting.to_lower() == "false" {
    return false
  }
  if setting.to_lower() == "true" {
    return true
  }
  if is_all_digits(setting) {
    return index < parse_hop_count(setting)
  }
  for raw in setting.split(",") {
    let entry = raw.to_owned().trim().to_lower()
    let address = address.to_lower()
    if entry == address {
      return true
    }
    if entry == "loopback" && is_loopback(address) {
      return true
    }
    if entry == "linklocal" && is_linklocal(address) {
      return true
    }
    if entry == "uniquelocal" && is_uniquelocal(address) {
      return true
    }
  }
  false
}

///|
fn is_all_digits(value : String) -> Bool {
  if value == "" {
    return false
  }
  for ch in value {
    let digit = ch.to_int() - 48
    if digit < 0 || digit > 9 {
      return false
    }
  }
  true
}

///|
fn parse_hop_count(value : String) -> Int32 {
  let mut count = 0
  for ch in value {
    count = count * 10 + (ch.to_int() - 48)
  }
  count
}

///|
fn is_loopback(address : String) -> Bool {
  address == "::1" || address.has_prefix("127.")
}

///|
fn is_linklocal(address : String) -> Bool {
  address.has_prefix("169.254.") || address.has_prefix("fe80")
}

///|
fn is_uniquelocal(address : String) -> Bool {
  if address.has_prefix("10.") || address.has_prefix("192.168.") {
    return true
  }
  if address.has_prefix("fc") || address.has_prefix("fd") {
    return true
  }
  if address.has_prefix("172.") {
    let parts = address.split(".").to_array()
    if parts.length() == 4 {
      let octet = parse_hop_count(parts[1].to_owned())
      octet >= 16 && octet <= 31
    } else {
      false
    }
  } else {
    false
  }
}

///|
/// Returns the request protocol: `"http"` or `"https"`, honoring
/// `X-Forwarded-Proto` when the client is trusted.
pub fn Req::protocol(self : Req) -> String {
  if trusted_proxy(self.setting("trust proxy"), self.remote_ip, 0) {
    let forwarded = self.header("x-forwarded-proto")
    if forwarded != "" {
      for raw in forwarded.split(",") {
        let value = raw.to_owned().trim().to_owned().to_lower()
        if value != "" {
          return value
        }
      }
    }
  }
  self.protocol
}

///|
/// Returns `true` when the request was made over TLS.
pub fn Req::secure(self : Req) -> Bool {
  self.protocol() == "https"
}

///|
/// The `Host` header (or `X-Forwarded-Host` for trusted clients), `req.host`.
pub fn Req::host(self : Req) -> String {
  if trusted_proxy(self.setting("trust proxy"), self.remote_ip, 0) {
    let forwarded = self.header("x-forwarded-host")
    if forwarded != "" {
      for raw in forwarded.split(",") {
        let value = raw.to_owned().trim().to_owned()
        if value != "" {
          return value
        }
      }
    }
  }
  self.header("host")
}

///|
/// The host name without the port, like `req.hostname`.
pub fn Req::hostname(self : Req) -> String {
  let host = self.host()
  if host == "" {
    return ""
  }
  let offset = if host.has_prefix("[") {
    match host.find("]") {
      Some(index) => index + 1
      None => 0
    }
  } else {
    0
  }
  let rest = host[offset:].to_owned()
  match rest.find(":") {
    Some(_) => host[:offset].to_owned()
    None => host
  }
}

///|
/// The subdomain parts of `req.hostname`, most specific first, with the
/// `subdomain offset` setting (default `2`) segments removed. IP hosts yield
/// an empty array, like Express.
pub fn Req::subdomains(self : Req) -> Array[String] {
  self.subdomains_with(self.setting("subdomain offset"))
}

///|
/// Same as `subdomains`, with an explicit offset. A negative offset is
/// treated as `0`.
pub fn Req::subdomains_with(self : Req, offset : String) -> Array[String] {
  let hostname = self.hostname()
  if hostname == "" {
    return []
  }
  let parts : Array[String] = if is_ip_address(hostname) {
    [hostname]
  } else {
    let reversed : Array[String] = []
    let segments = hostname.split(".").to_array()
    let mut index = segments.length() - 1
    while index >= 0 {
      reversed.push(segments[index].to_owned())
      index = index - 1
    }
    reversed
  }
  let start = if offset == "" { 2 } else { parse_hop_count(offset) }
  let result : Array[String] = []
  let mut index = start
  while index < parts.length() {
    result.push(parts[index])
    index = index + 1
  }
  result
}

///|
fn is_ip_address(value : String) -> Bool {
  if value.contains(":") {
    return true
  }
  let segments = value.split(".").to_array()
  if segments.length() != 4 {
    return false
  }
  for raw in segments {
    let segment = raw.to_owned()
    if segment == "" || !is_all_digits(segment) {
      return false
    }
  }
  true
}

///|
/// Content negotiation: the best match from `types` for the `Accept` header,
/// or `""` when none is acceptable. Extensions such as `html` are supported.
pub fn Req::accepts(self : Req, types : Array[String]) -> String {
  negotiate_media(self.header("accept"), types)
}

///|
/// `req.acceptsCharsets`, returning the client's preferred charset out of
/// `charsets` or `""`.
pub fn Req::accepts_charsets(self : Req, charsets : Array[String]) -> String {
  negotiate_charsets(self.header("accept-charset"), charsets)
}

///|
/// `req.acceptsEncodings`, returning the client's preferred encoding out of
/// `encodings` or `""`.
pub fn Req::accepts_encodings(self : Req, encodings : Array[String]) -> String {
  negotiate_encodings(self.header("accept-encoding"), encodings)
}

///|
/// `req.acceptsLanguages`, returning the client's preferred language out of
/// `languages` or `""`.
pub fn Req::accepts_languages(self : Req, languages : Array[String]) -> String {
  negotiate_languages(self.header("accept-language"), languages)
}

///|
/// The outcome of parsing a `Range` header, mirroring `range-parser`.
pub enum RangeResult {
  /// No `Range` header.
  NoRange
  /// The header was malformed (`-2`).
  Malformed
  /// The header asked for bytes outside the entity (`-1`).
  Unsatisfiable
  /// Inclusive `(start, end)` byte ranges.
  Ranges(Array[(Int32, Int32)])
}

///|
/// Parses the `Range` header for an entity of `size` bytes, like
/// `req.range(size)`.
pub fn Req::range(self : Req, size : Int32) -> RangeResult {
  parse_range_header(self.header("range"), size)
}

///|
/// Parses a `Range` header for an entity of `size` bytes, the same grammar as
/// the `range-parser` package: `bytes=0-4`, `bytes=5-`, `bytes=-3` and
/// multiple comma separated ranges.
pub fn parse_range_header(header : String, size : Int32) -> RangeResult {
  if header == "" {
    return NoRange
  }
  let equals = match header.find("=") {
    Some(index) => index
    None => return Malformed
  }
  if header[:equals].to_owned().to_lower() != "bytes" {
    return Unsatisfiable
  }
  let ranges : Array[(Int32, Int32)] = []
  for raw in header[equals + 1:].split(",") {
    let part = raw.to_owned().trim().to_owned()
    if part == "" {
      continue
    }
    let dash = match part.find("-") {
      Some(index) => index
      None => return Malformed
    }
    let start_text = part[:dash].to_owned().trim().to_owned()
    let end_text = part[dash + 1:].to_owned().trim().to_owned()
    let mut start = if start_text == "" {
      -1
    } else {
      parse_hop_count(start_text)
    }
    let mut end = if end_text == "" { -1 } else { parse_hop_count(end_text) }
    if start_text == "" && end_text == "" {
      return Malformed
    }
    if start == -1 {
      start = size - end
      end = size - 1
      if start < 0 {
        continue
      }
    } else if end == -1 {
      end = size - 1
    }
    if start > end || start >= size {
      continue
    }
    if end >= size {
      end = size - 1
    }
    ranges.push((start, end))
  }
  if ranges.length() == 0 {
    return Unsatisfiable
  }
  Ranges(ranges)
}

///|
/// `req.fresh`: whether the conditional request headers match the current
/// response, so the response can be a `304 Not Modified`.
pub fn Req::fresh(self : Req) -> Bool {
  if self.http_method != "GET" && self.http_method != "HEAD" {
    return false
  }
  let modified_since = self.header("if-modified-since")
  let none_match = self.header("if-none-match")
  if modified_since == "" && none_match == "" {
    return false
  }
  let cache_control = self.header("cache-control")
  if cache_control != "" && cache_control.to_lower().contains("no-cache") {
    return false
  }
  let res = match g_requests.get(self.request_id) {
    Some(state) => state.res
    None => return false
  }
  match res.status_code() {
    200..<=299 | 304 => ()
    _ => return false
  }
  if none_match != "" && none_match != "*" {
    let etag = res.header("ETag")
    if etag == "" {
      return false
    }
    let mut etag_stale = true
    for raw in none_match.split(",") {
      let token = raw.to_owned().trim().to_owned()
      if token == etag || token == "W/" + etag || "W/" + token == etag {
        etag_stale = false
      }
    }
    if etag_stale {
      return false
    }
  }
  if modified_since != "" {
    let last_modified = res.header("Last-Modified")
    if last_modified == "" {
      return false
    }
    let lm = match parse_http_date(last_modified) {
      Some(value) => value
      None => return false
    }
    let ims = match parse_http_date(modified_since) {
      Some(value) => value
      None => return false
    }
    if lm > ims {
      return false
    }
  }
  true
}

///|
/// `req.stale`, the negation of `req.fresh`.
pub fn Req::stale(self : Req) -> Bool {
  !self.fresh()
}

///|
/// Returns a signed cookie, verified against the `cookie secret` setting.
/// `""` when the cookie is missing or its signature does not match.
pub fn Req::signed_cookie(self : Req, name : String) -> String {
  let raw = self.cookie(name)
  if !raw.has_prefix("s:") {
    return ""
  }
  match unsign_cookie(self.setting("cookie secret"), raw[2:].to_owned()) {
    Some(value) => value
    None => ""
  }
}

///|
/// All cookies with verified signatures, without the `s:` prefix.
pub fn Req::signed_cookies(self : Req) -> Map[String, String] {
  let secret = self.setting("cookie secret")
  let out : Map[String, String] = Map([])
  for pair in self.cookies.iter() {
    let value = pair.1
    if value.has_prefix("s:") {
      match unsign_cookie(secret, value[2:].to_owned()) {
        Some(unsigned) => out.set(pair.0, unsigned)
        None => ()
      }
    }
  }
  out
}

///|
pub fn Req::set_body(self : Req, body : String) -> Unit {
  self.body_text = body
}

///|
pub fn Req::set_body_bytes(self : Req, body : Bytes) -> Unit {
  self.body_bytes = body
}

///|
pub fn Req::set_form_json(self : Req, value : Json?) -> Unit {
  self.body_form_json = value
}

///|
pub fn Req::set_json_body(self : Req, value : Json?) -> Unit {
  self.body_json = value
}

///|
pub fn Req::set_form(self : Req, value : Map[String, String]) -> Unit {
  self.body_form = value
}

///|
/// Replaces the route params. Params captured by a parent router are merged
/// in first, so mounted routers keep `req.params` populated.
fn Req::set_params(self : Req, value : Array[(String, String)]) -> Unit {
  let map : Map[String, String] = Map([])
  for pair in self.base_params {
    map.set(pair.0, pair.1)
  }
  for pair in value {
    map.set(pair.0, pair.1)
  }
  self.params = map
}

///|
fn Req::mark_aborted(self : Req) -> Unit {
  self.aborted = true
}

///|
/// Returns the request-scoped local variables map.
pub fn Req::locals(self : Req) -> Map[String, String] {
  self.locals
}

///|
pub fn Req::is_aborted(self : Req) -> Bool {
  self.aborted
}

///|
/// Checks whether the request Content-Type matches `content_type`.
/// Returns the matched type or `""`.
pub fn Req::content_type_is(self : Req, content_type : String) -> String {
  let ct = self.header("content-type").to_lower()
  if ct == content_type.to_lower() {
    content_type
  } else if ct.has_prefix(content_type.to_lower() + ";") {
    content_type
  } else {
    ""
  }
}

// ---------- Response ----------

///|
pub struct Res {
  priv uws_res : UwsRes
  priv request_id : UInt64
  priv settings : Map[String, String]
  priv mut status_code : Int32
  priv headers : Array[(String, String)]
  priv mut ended : Bool
  priv mut aborted : Bool
  priv mut headers_sent : Bool
  priv locals : Map[String, String]
  priv vary_headers : Array[String]
  priv is_ssl : Bool
}

///|
pub fn Res::status(self : Res, code : Int32) -> Res {
  self.status_code = code
  self
}

///|
pub fn Res::set(self : Res, name : String, value : String) -> Res {
  let value = if name.to_lower() == "content-type" {
    mime_content_type(value)
  } else {
    value
  }
  self.remove_header(name)
  self.headers.push((name, value))
  self
}

///|
pub fn Res::set_header(self : Res, name : String, value : String) -> Unit {
  ignore(self.set(name, value))
}

///|
/// Alias of `header`, matching `res.get` in Express.
pub fn Res::get(self : Res, name : String) -> String {
  self.header(name)
}

///|
/// Sets `Content-Type`, accepting either a media type or an extension like
/// `html`, and adding the default charset like Express does.
pub fn Res::type_(self : Res, content_type : String) -> Res {
  self.set("Content-Type", content_type)
}

///|
pub fn Res::header(self : Res, name : String) -> String {
  let wanted = name.to_lower()
  let mut found = ""
  for pair in self.headers {
    if pair.0.to_lower() == wanted {
      found = pair.1
    }
  }
  found
}

///|
fn Res::has_header(self : Res, name : String) -> Bool {
  self.header(name) != ""
}

///|
fn Res::remove_header(self : Res, name : String) -> Unit {
  let wanted = name.to_lower()
  let kept : Array[(String, String)] = []
  for pair in self.headers {
    if pair.0.to_lower() != wanted {
      kept.push(pair)
    }
  }
  self.headers.clear()
  for pair in kept {
    self.headers.push(pair)
  }
}

///|
fn Res::ssl_flag(self : Res) -> Int32 {
  if self.is_ssl {
    1
  } else {
    0
  }
}

///|
pub fn Res::is_ended(self : Res) -> Bool {
  self.ended
}

///|
pub fn Res::is_aborted(self : Res) -> Bool {
  self.aborted
}

///|
/// Whether the status line and headers have been written to the socket
/// already (after `write`, `flush_headers` or a `send`).
pub fn Res::headers_sent(self : Res) -> Bool {
  self.headers_sent
}

///|
pub fn Res::status_code(self : Res) -> Int32 {
  self.status_code
}

///|
fn Res::setting(self : Res, key : String) -> String {
  match self.settings.get(key) {
    Some(value) => value
    None => ""
  }
}

///|
fn Res::request(self : Res) -> Req? {
  match g_requests.get(self.request_id) {
    Some(state) => Some(state.req)
    None => None
  }
}

///|
fn Res::application(self : Res) -> App? {
  match g_requests.get(self.request_id) {
    Some(state) => Some(state.app)
    None => None
  }
}

///|
fn Res::etag_enabled(self : Res) -> Bool {
  self.setting("etag") != "false"
}

///|
fn Res::request_fresh(self : Res) -> Bool {
  match self.request() {
    Some(req) => req.fresh()
    None => false
  }
}

///|
/// A weak ETag for in-memory bodies: `W/"-"`, the shape used by
/// the `etag` package that Express relies on.
fn weak_etag(data : Bytes) -> String {
  let digest = base64_encode(sha256_bytes(data)).replace_all(old="=", new="")
  "W/\"" + data.length().to_string() + "-" + digest[:27].to_owned() + "\""
}

///|
/// A stats-based weak ETag for files, `W/"-"` in hex.
fn stats_etag(size : Int32, mtime : Int64) -> String {
  "W/\"" + size.to_string() + "-" + mtime.to_string() + "\""
}

///|
/// Writes the buffered headers and the body, applying Express' rules for
/// `ETag`, `Content-Length`, conditional requests and empty bodies.
fn Res::finish_bytes(self : Res, body : Bytes) -> Unit {
  if self.ended || self.aborted {
    return
  }
  if self.headers_sent || self.ended {
    self.ended = true
    ignore(uwb_res_write(self.uws_res, self.ssl_flag(), body, body.length()))
    uwb_res_end_empty(self.uws_res, self.ssl_flag())
    release_request(self.request_id)
    return
  }
  self.ended = true
  if !self.has_header("ETag") && self.etag_enabled() {
    ignore(self.set("ETag", weak_etag(body)))
  }
  if self.request_fresh() {
    self.status_code = 304
  }
  if self.status_code == 204 || self.status_code == 304 {
    self.remove_header("Content-Type")
    self.remove_header("Content-Length")
    self.remove_header("Transfer-Encoding")
    self.write_head()
    uwb_res_end_empty(self.uws_res, self.ssl_flag())
    release_request(self.request_id)
    return
  }
  let is_head = match self.request() {
    Some(req) => req.http_method() == "HEAD"
    None => false
  }
  if is_head && !self.has_header("Content-Length") {
    ignore(self.set("Content-Length", body.length().to_string()))
  }
  self.write_head()
  if is_head {
    uwb_res_end_empty(self.uws_res, self.ssl_flag())
  } else {
    uwb_res_end(self.uws_res, self.ssl_flag(), body, body.length())
  }
  release_request(self.request_id)
}

///|
/// Sends a text response. Defaults `Content-Type` to `text/html`, like
/// Express. HEAD requests are answered by µWebSockets with headers only.
pub fn Res::send(self : Res, body : String) -> Unit {
  if self.ended || self.aborted {
    return
  }
  if !self.has_header("Content-Type") {
    ignore(self.type_("html"))
  }
  self.finish_bytes(utf8_bytes(body))
}

///|
/// Sends raw bytes, useful for static files and binary payloads. Defaults
/// `Content-Type` to `application/octet-stream`, like `res.send(Buffer)`.
pub fn Res::send_bytes(self : Res, body : Bytes) -> Unit {
  if self.ended || self.aborted {
    return
  }
  if !self.has_header("Content-Type") {
    ignore(self.type_("bin"))
  }
  self.finish_bytes(body)
}

///|
/// Ends the response without a body.
pub fn Res::send_empty(self : Res) -> Unit {
  if self.ended || self.aborted {
    return
  }
  self.finish_bytes(Bytes::make(0, b'\x00'))
}

///|
/// Sets the status from `code` and sends the matching reason phrase (or the
/// number) as the body, like `res.sendStatus`.
pub fn Res::send_status(self : Res, code : Int32) -> Unit {
  ignore(self.status(code))
  let message = status_text(code)
  let body = if message == "Unknown" { code.to_string() } else { message }
  ignore(self.type_("txt"))
  self.send(body)
}

///|
/// Streams a chunk of the response body. Writes the status line and buffered
/// headers on first use. Returns `false` on socket backpressure.
pub fn Res::write(self : Res, chunk : String) -> Bool {
  self.write_bytes(utf8_bytes(chunk))
}

///|
/// Streams a binary chunk of the response body.
pub fn Res::write_bytes(self : Res, chunk : Bytes) -> Bool {
  if self.ended || self.aborted {
    return false
  }
  if !self.headers_sent {
    self.headers_sent = true
    self.write_head()
  }
  uwb_res_write(self.uws_res, self.ssl_flag(), chunk, chunk.length()) != 0
}

///|
/// Ends a streamed response.
pub fn Res::end_stream(self : Res) -> Unit {
  if self.ended || self.aborted {
    return
  }
  if !self.headers_sent {
    self.headers_sent = true
    self.write_head()
  }
  self.ended = true
  uwb_res_end_empty(self.uws_res, self.ssl_flag())
  release_request(self.request_id)
}

///|
/// Writes the status line and headers without ending the response, for
/// streaming responses (Server-Sent Events, chunked downloads).
pub fn Res::flush_headers(self : Res) -> Unit {
  if self.ended || self.aborted || self.headers_sent {
    return
  }
  self.headers_sent = true
  self.write_head()
}

///|
/// Serializes `value` as JSON and sends it.
pub fn Res::json(self : Res, value : Json) -> Unit {
  ignore(self.set("Content-Type", "application/json; charset=utf-8"))
  let raw = if self.setting("json escape") == "true" {
    escape_json_html(value.stringify())
  } else {
    value.stringify()
  }
  self.send(raw)
}

///|
/// Escapes `<`, `>`, `&`, `U+2028` and `U+2029` in a JSON string, mirroring
/// the `json escape` setting.
fn escape_json_html(raw : String) -> String {
  raw
  .replace_all(old="&", new="\\u0026")
  .replace_all(old="<", new="\\u003c")
  .replace_all(old=">", new="\\u003e")
  .replace_all(old="\u{2028}", new="\\u2028")
  .replace_all(old="\u{2029}", new="\\u2029")
}

///|
/// Sends an already serialized JSON string.
pub fn Res::json_string(self : Res, raw : String) -> Unit {
  ignore(self.set("Content-Type", "application/json; charset=utf-8"))
  self.send(raw)
}

///|
/// Sends JSONP. When the callback query parameter (named by the
/// `jsonp callback name` setting, `callback` by default) is present, the body
/// is a script invoking it; otherwise the response is plain JSON.
pub fn Res::jsonp(self : Res, value : Json) -> Unit {
  let callback_name = match self.setting("jsonp callback name") {
    "" => "callback"
    name => name
  }
  let raw = match self.request() {
    Some(req) => req.query_param(callback_name)
    None => ""
  }
  ignore(self.set("X-Content-Type-Options", "nosniff"))
  if raw == "" {
    ignore(self.set("Content-Type", "application/json; charset=utf-8"))
    self.send(value.stringify())
    return
  }
  let callback = sanitize_jsonp_callback(raw)
  if callback == "" {
    self
    .status(500)
    .send("Callback parameter must consist of characters in [\\[\\]\\w$.]")
    return
  }
  ignore(self.set("Content-Type", "text/javascript; charset=utf-8"))
  let payload = value
    .stringify()
    .replace_all(old="\u{2028}", new="\\u2028")
    .replace_all(old="\u{2029}", new="\\u2029")
  self.send(
    "/**/ typeof " +
    callback +
    " === 'function' && " +
    callback +
    "(" +
    payload +
    ");",
  )
}

///|
fn sanitize_jsonp_callback(raw : String) -> String {
  let sb = StringBuilder()
  for ch in raw {
    let code = ch.to_int()
    let alnum = (code >= 48 && code <= 57) ||
      (code >= 65 && code <= 90) ||
      (code >= 97 && code <= 122) ||
      code == 95
    if alnum || ch == '$' || ch == '.' || ch == '[' || ch == ']' || ch == '\\' {
      sb.write_char(ch)
    }
  }
  sb.to_string()
}

///|
/// Content negotiation for responses, like `res.format`. `handlers` maps a
/// media type or extension (or `"default"`) to a handler; when nothing
/// matches and no default is given the request fails with `406`.
pub fn Res::format(self : Res, handlers : Array[(String, Handler)]) -> Unit {
  let keys : Array[String] = []
  let mut default_handler : Handler? = None
  for pair in handlers {
    if pair.0 == "default" {
      default_handler = Some(pair.1)
    } else {
      keys.push(pair.0)
    }
  }
  let req = self.request()
  let key = match req {
    Some(req) => if keys.length() > 0 { req.accepts(keys) } else { "" }
    None => ""
  }
  ignore(self.vary("Accept"))
  let mut handled = false
  if key != "" {
    ignore(self.type_(mime_for_type_name(key)))
    for pair in handlers {
      if pair.0 == key {
        match req {
          Some(req) =>
            match req.next {
              Some(next) => (pair.1)(req, self, next)
              None => ()
            }
          None => ()
        }
        handled = true
      }
    }
  }
  if handled {
    return
  }
  match default_handler {
    Some(handler) =>
      match req {
        Some(req) =>
          match req.next {
            Some(next) => handler(req, self, next)
            None => ()
          }
        None => ()
      }
    None =>
      match req {
        Some(req) =>
          match req.next {
            Some(next) => next.fail_status(406, "Not Acceptable")
            None => self.status(406).send("Not Acceptable")
          }
        None => self.status(406).send("Not Acceptable")
      }
  }
}

///|
/// Appends a `Set-Cookie` header with `Path=/`.
pub fn Res::cookie(self : Res, name : String, value : String) -> Res {
  self.set_cookie(name, value, "/", -1, false, "", "", false, "", false)
}

///|
/// Appends a `Set-Cookie` header. A negative `max_age` omits `Max-Age` and
/// `expires` is a full HTTP date string. With `signed` the value is prefixed
/// with `s:` and signed with the `cookie secret` setting, like
/// `res.cookie(name, value, { signed: true })`.
pub fn Res::cookie_with(
  self : Res,
  name : String,
  value : String,
  path? : String,
  max_age? : Int32,
  http_only? : Bool,
  domain? : String,
  expires? : String,
  secure? : Bool,
  same_site? : String,
  signed? : Bool,
) -> Res {
  let path = match path {
    Some(value) => value
    None => "/"
  }
  let max_age = match max_age {
    Some(value) => value
    None => -1
  }
  let http_only = match http_only {
    Some(value) => value
    None => false
  }
  let domain = match domain {
    Some(value) => value
    None => ""
  }
  let expires = match expires {
    Some(value) => value
    None => ""
  }
  let secure = match secure {
    Some(value) => value
    None => false
  }
  let same_site = match same_site {
    Some(value) => value
    None => ""
  }
  let signed = match signed {
    Some(value) => value
    None => false
  }
  self.set_cookie(
    name, value, path, max_age, http_only, domain, expires, secure, same_site, signed,
  )
}

///|
/// Expires a cookie, mirroring Express `res.clearCookie`.
pub fn Res::clear_cookie(self : Res, name : String, path? : String) -> Res {
  let path = match path {
    Some(value) => value
    None => "/"
  }
  self.set_cookie(
    name, "", path, 0, false, "", "Thu, 01 Jan 1970 00:00:00 GMT", false, "", false,
  )
}

///|
fn Res::set_cookie(
  self : Res,
  name : String,
  value : String,
  path : String,
  max_age : Int32,
  http_only : Bool,
  domain : String,
  expires : String,
  secure : Bool,
  same_site : String,
  signed : Bool,
) -> Res {
  let stored = if signed {
    "s:" + sign_cookie(self.setting("cookie secret"), value)
  } else {
    value
  }
  let sb = StringBuilder()
  sb.write_string(name)
  sb.write_string("=")
  sb.write_string(percent_encode(stored))
  if max_age >= 0 {
    sb.write_string("; Max-Age=")
    sb.write_string(max_age.to_string())
  }
  if domain != "" {
    sb.write_string("; Domain=")
    sb.write_string(domain)
  }
  if path != "" {
    sb.write_string("; Path=")
    sb.write_string(path)
  }
  if expires != "" {
    sb.write_string("; Expires=")
    sb.write_string(expires)
  }
  if http_only {
    sb.write_string("; HttpOnly")
  }
  if secure {
    sb.write_string("; Secure")
  }
  if same_site != "" {
    sb.write_string("; SameSite=")
    sb.write_string(same_site)
  }
  self.headers.push(("Set-Cookie", sb.to_string()))
  self
}

///|
/// Redirects to `url` with `302 Found` (or the given status), negotiating a
/// text or HTML body from the `Accept` header like Express.
pub fn Res::redirect(self : Res, url : String, status? : Int32) -> Unit {
  let code = match status {
    Some(value) => value
    None => 302
  }
  let address = encode_url(url)
  ignore(self.set("Location", address))
  let accepted = match self.request() {
    Some(req) => req.accepts(["html", "text"])
    None => "text"
  }
  let message = status_text(code)
  let body = if accepted == "html" {
    ignore(self.type_("html"))
    "

" + escape_html(message + ". Redirecting to " + address) + "

" } else if accepted == "text" { ignore(self.type_("text")) message + ". Redirecting to " + address } else { "" } ignore(self.status(code)) ignore(self.set("Content-Length", body.length().to_string())) if body == "" { self.ended = true self.write_head() uwb_res_end_empty(self.uws_res, self.ssl_flag()) } else { self.ended = true self.write_head() let bytes = utf8_bytes(body) uwb_res_end(self.uws_res, self.ssl_flag(), bytes, bytes.length()) } release_request(self.request_id) } ///| /// Percent-encodes a URL for a `Location` header, leaving reserved characters /// and already-escaped `%XX` sequences untouched (a small `encodeurl`). fn encode_url(url : String) -> String { let bytes = utf8_bytes(url) let sb = StringBuilder() let mut index = 0 while index < bytes.length() { let byte = bytes[index].to_int() let printable = byte >= 0x21 && byte <= 0x7e let safe = printable && byte != 0x22 && byte != 0x23 && byte != 0x24 && byte != 0x27 && byte != 0x28 && byte != 0x29 && byte != 0x2a && byte != 0x2b && byte != 0x2c && byte != 0x3c && byte != 0x3e && byte != 0x5c && byte != 0x5e && byte != 0x60 && byte != 0x7b && byte != 0x7c && byte != 0x7d if byte == 0x25 { if index + 2 < bytes.length() && is_hex_digit(bytes[index + 1].to_int()) && is_hex_digit(bytes[index + 2].to_int()) { sb.write_char('%') sb.write_char(bytes[index + 1].to_int().unsafe_to_char()) sb.write_char(bytes[index + 2].to_int().unsafe_to_char()) index = index + 3 continue } sb.write_string("%25") index = index + 1 continue } if safe { sb.write_char(byte.unsafe_to_char()) } else { sb.write_string("%") sb.write_char(hex_char(byte / 16)) sb.write_char(hex_char(byte % 16)) } index = index + 1 } sb.to_string() } ///| fn is_hex_digit(value : Int32) -> Bool { (value >= 48 && value <= 57) || (value >= 65 && value <= 70) || (value >= 97 && value <= 102) } ///| /// Escapes HTML special characters, like `escape-html`. pub fn escape_html(value : String) -> String { value .replace_all(old="&", new="&") .replace_all(old="<", new="<") .replace_all(old=">", new=">") .replace_all(old="\"", new=""") .replace_all(old="'", new="'") } ///| fn Res::write_head(self : Res) -> Unit { let line = self.status_code.to_string() + " " + status_text(self.status_code) let status_bytes = utf8_bytes(line) uwb_res_write_status( self.uws_res, self.ssl_flag(), status_bytes, status_bytes.length(), ) for pair in self.headers { let name = utf8_bytes(pair.0) let value = utf8_bytes(pair.1) uwb_res_write_header( self.uws_res, self.ssl_flag(), name, name.length(), value, value.length(), ) } } ///| fn Res::mark_aborted(self : Res) -> Unit { self.aborted = true } ///| /// Returns the response-scoped local variables map. pub fn Res::locals(self : Res) -> Map[String, String] { self.locals } ///| /// Appends a response header (allows multiple values for the same header). pub fn Res::append(self : Res, name : String, value : String) -> Res { self.headers.push((name, value)) self } ///| /// Sets the `Location` header. pub fn Res::location(self : Res, url : String) -> Res { self.set("Location", url) } ///| /// Sets the `Link` header from an array of link objects. /// Each link is a string like `; rel="stylesheet"`. pub fn Res::links(self : Res, links : Array[String]) -> Res { let joined = links.join(", ") self.set("Link", joined) } ///| /// Sets `Content-Disposition: attachment` to trigger a download. /// If `filename` is provided, includes it in the header. pub fn Res::attachment(self : Res, filename? : String) -> Res { let value = match filename { Some(name) => "attachment; filename=\"" + name + "\"" None => "attachment" } self.set("Content-Disposition", value) } ///| /// Adds a field to the `Vary` header. pub fn Res::vary(self : Res, field : String) -> Res { self.vary_headers.push(field) let joined = self.vary_headers.join(", ") self.set("Vary", joined) } ///| /// Options shared by `res.send_file` and `express_static`, modeled on the /// `send` package Express delegates to. pub struct FileOptions { /// `"ignore"` (default), `"deny"` or `"allow"`. dotfiles : String /// File served for directory requests, `"index.html"` by default. `""` /// disables directory indexes. index : String etag : Bool last_modified : Bool /// `Cache-Control: public, max-age=N` in seconds; negative disables it. max_age : Int32 immutable : Bool /// Whether a missing file falls through to the next middleware. fallthrough : Bool /// Extra extensions tried when the exact path is missing, like /// `["html"]`. extensions : Array[String] /// Whether directory requests without a trailing slash get a `301`. redirect : Bool } ///| pub fn FileOptions::default() -> FileOptions { { dotfiles: "ignore", index: "index.html", etag: true, last_modified: true, max_age: 0, immutable: false, fallthrough: true, extensions: [], redirect: true, } } ///| /// Transfers the file at `path` as the response, like `res.sendFile`: sets /// `Content-Type`, `Content-Length`, `Last-Modified`, a weak `ETag`, and /// handles conditional requests and byte ranges. pub fn Res::send_file( self : Res, path : String, options? : FileOptions, ) -> Unit { if self.ended || self.aborted { return } let options = match options { Some(value) => value None => FileOptions::default() } if !(options.dotfiles == "allow") && has_dotfile_segment(path) { if options.dotfiles == "deny" { self.status(403).send("Forbidden") } else { self.status(404).send("Not Found") } return } let kind = fs_kind(path) if kind != 1 { self.status(404).send("Not Found") return } let mut data = fs_read_file(path) let size = data.length() let mut status : Int32 = 200 ignore(self.set("Accept-Ranges", "bytes")) match self.request() { Some(req) => match req.range(size) { Ranges(ranges) => { let (start, end) = combine_ranges(ranges) data = slice_bytes(data, start, end) status = 206 ignore( self.set( "Content-Range", "bytes " + start.to_string() + "-" + end.to_string() + "/" + size.to_string(), ), ) } Unsatisfiable => { ignore(self.set("Content-Range", "bytes */" + size.to_string())) self.status(416).send("Range Not Satisfiable") return } NoRange | Malformed => () } None => () } if !self.has_header("Content-Type") { ignore(self.set("Content-Type", mime_type(path))) } let mtime = fs_mtime(path) if options.last_modified && mtime >= 0 { ignore(self.set("Last-Modified", format_http_date(mtime))) } if options.etag && self.etag_enabled() && !self.has_header("ETag") { ignore(self.set("ETag", stats_etag(size, mtime))) } if options.max_age >= 0 && !self.has_header("Cache-Control") { ignore( self.set( "Cache-Control", "public, max-age=" + options.max_age.to_string() + (if options.immutable { ", immutable" } else { "" }), ), ) } if status == 200 && self.request_fresh() { status = 304 } if status == 304 { self.remove_header("Content-Type") self.remove_header("Content-Length") self.remove_header("Transfer-Encoding") } let is_head = match self.request() { Some(req) => req.http_method() == "HEAD" None => false } if is_head && status != 304 && !self.has_header("Content-Length") { ignore(self.set("Content-Length", data.length().to_string())) } ignore(self.status(status)) self.ended = true self.write_head() if status == 304 || is_head { uwb_res_end_empty(self.uws_res, self.ssl_flag()) } else { uwb_res_end(self.uws_res, self.ssl_flag(), data, data.length()) } release_request(self.request_id) } ///| /// Transfers the file at `path` as an attachment. /// Calls `send_file` after setting `Content-Disposition`. pub fn Res::download( self : Res, path : String, filename? : String, options? : FileOptions, ) -> Unit { ignore(self.attachment(filename?)) self.send_file(path, options?) } ///| /// Renders `view` through the app's view engine and sends the result, like /// `res.render`. `locals` and `res.locals` are merged with `app.locals`. pub fn Res::render(self : Res, view : String, locals? : Json) -> Unit { let app = match self.application() { Some(app) => app None => { self.status(500).send("res.render called outside of a request") return } } let options = merged_view_options(app, self.locals, locals) let html = app.render(view, locals=options) catch { error => { self.status(500).send("View render failed: " + error.to_string()) return } } if !self.has_header("Content-Type") { ignore(self.type_("html")) } self.send(html) } ///| fn merged_view_options( app : App, res_locals : Map[String, String], locals : Json?, ) -> Json { let map : Map[String, Json] = Map([]) for pair in app.locals.iter() { map.set(pair.0, Json::string(pair.1)) } for pair in res_locals.iter() { map.set(pair.0, Json::string(pair.1)) } match locals { Some(Json::Object(entries)) => for pair in entries.iter() { map.set(pair.0, pair.1) } _ => () } Json::object(map) } ///| /// Merges byte ranges into one, the `combine: true` mode of `range-parser` /// that `send` uses. fn combine_ranges(ranges : Array[(Int32, Int32)]) -> (Int32, Int32) { let sorted = ranges.copy() sorted.sort_by(fn(a, b) { if a.0 == b.0 { if a.1 < b.1 { -1 } else if a.1 > b.1 { 1 } else { 0 } } else if a.0 < b.0 { -1 } else { 1 } }) let start = sorted[0].0 let mut end = sorted[0].1 let mut index = 1 while index < sorted.length() { let (next_start, next_end) = sorted[index] if next_start <= end + 1 { if next_end > end { end = next_end } } else { return (start, end) } index = index + 1 } (start, end) } ///| fn slice_bytes(data : Bytes, start : Int32, end : Int32) -> Bytes { let out : Array[Byte] = [] let mut index = start while index <= end && index < data.length() { out.push(data[index]) index = index + 1 } Bytes::from_array(out) } ///| /// Whether any path segment starts with a dot. fn has_dotfile_segment(path : String) -> Bool { for segment in path.split("/") { let segment = segment.to_owned() if segment.length() > 1 && segment.has_prefix(".") { return true } } false } ///| fn release_request(request_id : UInt64) -> Unit { g_requests.remove(request_id) } // ---------- Middleware ---------- ///| pub type Handler = (Req, Res, Next) -> Unit ///| /// Continues the middleware chain. Call `proceed()` to keep going or /// `fail(message)` to hand control to the error middleware. pub struct Next { priv run : (String?) -> Unit priv run_status : (Int32, String) -> Unit priv proceed_fn : () -> Unit } ///| pub fn Next::proceed(self : Next) -> Unit { (self.proceed_fn)() } ///| pub fn Next::fail(self : Next, message : String) -> Unit { (self.run)(Some(message)) } ///| /// Fails with an HTTP status, the equivalent of Express' /// `next(createError(status, message))`. pub fn Next::fail_status(self : Next, status : Int32, message : String) -> Unit { (self.run_status)(status, message) } ///| pub fn Next::resume_with(self : Next, error : String?) -> Unit { (self.run)(error) } ///| fn Next::resume_with_status( self : Next, error : String?, status : Int32?, ) -> Unit { match (error, status) { (Some(message), Some(status)) => (self.run_status)(status, message) (Some(message), None) => (self.run)(Some(message)) _ => (self.run)(None) } } ///| pub type Middleware = (Req, Res, Next) -> Unit ///| pub type ErrorMiddleware = (String, Req, Res, Next) -> Unit // ---------- Application ---------- ///| priv enum Layer { Mw(PathPattern, Middleware) Err(PathPattern, ErrorMiddleware) Route(String, PathPattern, Handler) Mount(PathPattern, Router, String) } ///| pub struct App { priv id : UInt64 priv uws : UwsApp priv mut is_ssl : Bool priv layers : Array[Layer] priv ws_routes : Array[(Int32, String)] priv settings : Map[String, String] priv locals : Map[String, String] priv param_callbacks : Array[(String, (Req, Next) -> Unit)] priv engines : Map[String, ViewEngine] priv mut prepared : Bool } ///| /// A view engine: receives the resolved template path, the template source /// and the locals, and returns the rendered string. pub type ViewEngine = (String, String, Json) -> String ///| /// A standalone router, like `express.Router()`. Mount it with /// `app.use_router(path, router)`; `merge_params` controls whether the /// parent router's params stay visible inside (Express' `mergeParams`). pub struct Router { priv layers : Array[Layer] priv param_callbacks : Array[(String, (Req, Next) -> Unit)] priv merge_params : Bool } ///| /// Creates a new router, like `express.Router()`. pub fn express_router() -> Router { Router::new() } ///| pub fn Router::new(merge_params? : Bool) -> Router { let merge_params = match merge_params { Some(value) => value None => false } { layers: [], param_callbacks: [], merge_params, } } ///| /// A route builder for chaining handlers on one path: /// `app.route("/x").get(show).post(create)`. pub struct RouteBuilder { priv path : String priv add : (String, String, Handler) -> Unit } ///| /// Creates a new application. pub fn express() -> App { init_shim() { id: next_id(), uws: uwb_app_new(), is_ssl: false, layers: [], ws_routes: [], settings: Map([]), locals: Map([]), param_callbacks: [], engines: Map([]), prepared: false, } } ///| /// Registers middleware for every request, `app.use(mw)` in Express. /// /// `use` is a reserved word in MoonBit, so the function is declared as /// `use_middleware` and exposed under the Express name with `#alias`. #alias(use) pub fn App::use_middleware(self : App, middleware : Middleware) -> Unit { self.layers.push(Mw(make_pattern("/"), middleware)) } ///| /// Registers middleware that only runs for paths starting with `path`, for /// example `app.use_at("/api", auth)`. pub fn App::use_at(self : App, path : String, middleware : Middleware) -> Unit { self.layers.push(Mw(make_pattern(path), middleware)) } ///| /// Registers error-handling middleware. It runs when a previous middleware /// called `next.fail(...)`. pub fn App::use_error(self : App, middleware : ErrorMiddleware) -> Unit { self.layers.push(Err(make_pattern("/"), middleware)) } ///| pub fn App::use_error_at( self : App, path : String, middleware : ErrorMiddleware, ) -> Unit { self.layers.push(Err(make_pattern(path), middleware)) } ///| /// Registers a handler for one HTTP method (the low-level form of /// `app.get/post/...`, which build routes on top of it). pub fn App::add_route( self : App, http_method : String, path : String, handler : Handler, ) -> Unit { self.layers.push(Route(http_method.to_upper(), make_pattern(path), handler)) } ///| /// Creates a route builder for chaining handlers on one path, like /// `app.route("/x").get(show).post(create)`. pub fn App::route(self : App, path : String) -> RouteBuilder { { path, add: fn(http_method, route_path, handler) { self.add_route(http_method, route_path, handler) }, } } ///| /// Mounts a router under `path`, like `app.use("/api", router)`. Requests /// see the mount prefix stripped in `req.url` and accumulated in /// `req.base_url`. pub fn App::use_router(self : App, path : String, router : Router) -> Unit { let prefix = normalize_mount(path) self.layers.push(Mount(make_pattern(path), router, prefix)) } ///| pub fn App::get(self : App, path : String, handler : Handler) -> Unit { self.add_route("GET", path, handler) } ///| pub fn App::post(self : App, path : String, handler : Handler) -> Unit { self.add_route("POST", path, handler) } ///| pub fn App::put(self : App, path : String, handler : Handler) -> Unit { self.add_route("PUT", path, handler) } ///| pub fn App::patch(self : App, path : String, handler : Handler) -> Unit { self.add_route("PATCH", path, handler) } ///| pub fn App::delete(self : App, path : String, handler : Handler) -> Unit { self.add_route("DELETE", path, handler) } ///| pub fn App::options(self : App, path : String, handler : Handler) -> Unit { self.add_route("OPTIONS", path, handler) } ///| pub fn App::head(self : App, path : String, handler : Handler) -> Unit { self.add_route("HEAD", path, handler) } ///| pub fn App::all(self : App, path : String, handler : Handler) -> Unit { self.add_route("ALL", path, handler) } ///| /// Registers a handler on a router for one HTTP method. pub fn Router::add_route( self : Router, http_method : String, path : String, handler : Handler, ) -> Unit { self.layers.push(Route(http_method.to_upper(), make_pattern(path), handler)) } ///| /// Creates a route builder on a router, like `router.route("/x")`. pub fn Router::route(self : Router, path : String) -> RouteBuilder { { path, add: fn(http_method, route_path, handler) { self.add_route(http_method, route_path, handler) }, } } ///| pub fn Router::get(self : Router, path : String, handler : Handler) -> Unit { self.add_route("GET", path, handler) } ///| pub fn Router::post(self : Router, path : String, handler : Handler) -> Unit { self.add_route("POST", path, handler) } ///| pub fn Router::put(self : Router, path : String, handler : Handler) -> Unit { self.add_route("PUT", path, handler) } ///| pub fn Router::patch(self : Router, path : String, handler : Handler) -> Unit { self.add_route("PATCH", path, handler) } ///| pub fn Router::delete(self : Router, path : String, handler : Handler) -> Unit { self.add_route("DELETE", path, handler) } ///| pub fn Router::options(self : Router, path : String, handler : Handler) -> Unit { self.add_route("OPTIONS", path, handler) } ///| pub fn Router::head(self : Router, path : String, handler : Handler) -> Unit { self.add_route("HEAD", path, handler) } ///| pub fn Router::all(self : Router, path : String, handler : Handler) -> Unit { self.add_route("ALL", path, handler) } ///| /// Registers middleware on a router for every request, `router.use(mw)` in /// Express. Declared as `use_middleware` and exposed as `use` with `#alias` /// because `use` is reserved in MoonBit. #alias(use) pub fn Router::use_middleware(self : Router, middleware : Middleware) -> Unit { self.layers.push(Mw(make_pattern("/"), middleware)) } ///| /// Registers middleware on a router for paths starting with `path`. pub fn Router::use_at( self : Router, path : String, middleware : Middleware, ) -> Unit { self.layers.push(Mw(make_pattern(path), middleware)) } ///| pub fn Router::use_error(self : Router, middleware : ErrorMiddleware) -> Unit { self.layers.push(Err(make_pattern("/"), middleware)) } ///| pub fn Router::use_error_at( self : Router, path : String, middleware : ErrorMiddleware, ) -> Unit { self.layers.push(Err(make_pattern(path), middleware)) } ///| /// Adds a param callback to a router, like `router.param(name, fn)`. pub fn Router::param( self : Router, name : String, callback : (Req, Next) -> Unit, ) -> Unit { self.param_callbacks.push((name, callback)) } ///| /// Mounts a nested router, like `router.use("/sub", subRouter)`. pub fn Router::use_router( self : Router, path : String, router : Router, ) -> Unit { let prefix = normalize_mount(path) self.layers.push(Mount(make_pattern(path), router, prefix)) } ///| pub fn RouteBuilder::get( self : RouteBuilder, handler : Handler, ) -> RouteBuilder { (self.add)("GET", self.path, handler) self } ///| pub fn RouteBuilder::post( self : RouteBuilder, handler : Handler, ) -> RouteBuilder { (self.add)("POST", self.path, handler) self } ///| pub fn RouteBuilder::put( self : RouteBuilder, handler : Handler, ) -> RouteBuilder { (self.add)("PUT", self.path, handler) self } ///| pub fn RouteBuilder::patch( self : RouteBuilder, handler : Handler, ) -> RouteBuilder { (self.add)("PATCH", self.path, handler) self } ///| pub fn RouteBuilder::delete( self : RouteBuilder, handler : Handler, ) -> RouteBuilder { (self.add)("DELETE", self.path, handler) self } ///| pub fn RouteBuilder::options( self : RouteBuilder, handler : Handler, ) -> RouteBuilder { (self.add)("OPTIONS", self.path, handler) self } ///| pub fn RouteBuilder::head( self : RouteBuilder, handler : Handler, ) -> RouteBuilder { (self.add)("HEAD", self.path, handler) self } ///| pub fn RouteBuilder::all( self : RouteBuilder, handler : Handler, ) -> RouteBuilder { (self.add)("ALL", self.path, handler) self } ///| /// Normalizes a mount path: `/api/` becomes `/api`, and `/` becomes `""`. fn normalize_mount(path : String) -> String { let mut value = path while value.length() > 1 && value.has_suffix("/") { value = value[:value.length() - 1].to_owned() } if value == "/" { "" } else { value } } ///| /// Strips a mount prefix from a request path, keeping the leading slash. fn mount_relative(prefix : String, path : String) -> String { if prefix == "" { return path } if path.length() <= prefix.length() { return "/" } let rest = path[prefix.length():].to_owned() if rest.has_prefix("/") { rest } else { "/" + rest } } ///| pub fn App::set(self : App, key : String, value : String) -> Unit { self.settings.set(key, value) } ///| /// Returns a setting, `""` when unset. pub fn App::get_setting(self : App, key : String) -> String { match self.settings.get(key) { Some(value) => value None => "" } } ///| /// Turns a boolean setting on, like `app.enable(name)`. pub fn App::enable(self : App, key : String) -> Unit { self.set(key, "true") } ///| /// Turns a boolean setting off, like `app.disable(name)`. pub fn App::disable(self : App, key : String) -> Unit { self.set(key, "false") } ///| /// Whether a boolean setting is enabled, like `app.enabled(name)`. pub fn App::enabled(self : App, key : String) -> Bool { self.get_setting(key) == "true" } ///| /// Whether a boolean setting is disabled, like `app.disabled(name)`. pub fn App::disabled(self : App, key : String) -> Bool { self.get_setting(key) == "false" } ///| /// Registers a view engine for an extension, like `app.engine("html", fn)`. pub fn App::engine(self : App, extension : String, render : ViewEngine) -> Unit { self.engines.set(normalize_extension(extension), render) } ///| /// Renders a view through the registered engine, resolving it below the /// `views` setting (default `./views`). Uses the `view engine` setting when /// the view has no engine extension. pub fn App::render(self : App, view : String, locals? : Json) -> String raise { let views = match self.get_setting("views") { "" => "./views" value => value } let mut file = view let mut extension = extension_of(view) if extension == "" || !self.engines.contains(extension) { let default_extension = normalize_extension(self.get_setting("view engine")) if default_extension != "" { file = view + "." + default_extension extension = default_extension } } let engine = match self.engines.get(extension) { Some(engine) => engine None => raise Failure( "no view engine registered for \"" + extension + "\" (use app.engine or set \"view engine\")", ) } let path = if file.has_prefix("/") { file } else { join_path(views, file) } if fs_kind(path) != 1 { raise Failure("view not found: " + path) } let source = utf8_string(fs_read_file(path)) let options = match locals { Some(value) => value None => Json::null() } engine(path, source, options) } ///| /// The extension of a path without the dot, lower-cased. fn extension_of(path : String) -> String { match path.rev_split_once(".") { Some((_, ext)) => ext.to_owned().to_lower() None => "" } } ///| fn normalize_extension(value : String) -> String { let value = value.to_lower() if value.has_prefix(".") { value[1:].to_owned() } else { value } } ///| /// Returns the app-scoped local variables map. pub fn App::locals(self : App) -> Map[String, String] { self.locals } ///| /// Adds a param callback. Called when a route param with `name` is matched. /// The callback receives `(req, next)` and should call `next.proceed()` to continue. pub fn App::param( self : App, name : String, callback : (Req, Next) -> Unit, ) -> Unit { self.param_callbacks.push((name, callback)) } ///| /// Registers the catch-all route and publishes the app to the shim. Must run /// before `listen_async`. pub fn App::prepare(self : App) -> Unit { if self.prepared { return } self.prepared = true g_apps.set(self.id, self) let pattern = utf8_bytes("/*") uwb_app_any(self.uws, self.id, pattern, pattern.length()) for route in self.ws_routes { let route_id = route.0 let path = utf8_bytes(route.1) uwb_app_ws(self.uws, route_id, path, path.length()) } } ///| /// Starts listening on all interfaces and runs the event loop. This call /// blocks until the server is closed. pub fn App::listen(self : App, port : Int32, cb : (Bool) -> Unit) -> Unit { self.prepare() let ok = uwb_app_listen(self.uws, self.id, utf8_bytes(""), 0, port) != 0 cb(ok) if ok { uwb_app_run(self.uws) } } ///| /// Starts listening on a specific host and runs the event loop. This call /// blocks until the server is closed. pub fn App::listen_host( self : App, host : String, port : Int32, cb : (Bool) -> Unit, ) -> Unit { self.prepare() let host_bytes = utf8_bytes(host) let ok = uwb_app_listen( self.uws, self.id, host_bytes, host_bytes.length(), port, ) != 0 cb(ok) if ok { uwb_app_run(self.uws) } } ///| /// Starts listening without blocking. Call `run()` yourself afterwards, for /// example when the process has other work to do. pub fn App::listen_async( self : App, host : String, port : Int32, cb : (Bool) -> Unit, ) -> Unit { self.prepare() let host_bytes = utf8_bytes(host) let ok = uwb_app_listen( self.uws, self.id, host_bytes, host_bytes.length(), port, ) != 0 cb(ok) } ///| /// Switches the app to TLS before it starts listening. `cert_file` and /// `key_file` must point to PEM-encoded certificate and key. For extra /// options (CA bundle, ciphers, ...) use `enable_tls_with`. pub fn App::enable_tls( self : App, cert_file : String, key_file : String, ) -> Unit { self.enable_tls_with(UwsSslOptions::{ cert_file, key_file, passphrase: None, dh_params_file: None, ca_file: None, ssl_ciphers: None, }) } ///| /// Switches the app to TLS with full `UwsSslOptions`. Must run before /// `listen*` (or before `prepare()`); a TLS app is started with the same /// `listen` calls as a plain one. pub fn App::enable_tls_with(self : App, options : UwsSslOptions) -> Unit { if self.prepared { abort("enable_tls must be called before prepare()/listen()") } enable_ssl(self.uws, options) self.is_ssl = true } ///| /// Switches to TLS and starts listening on all interfaces, blocking in the /// µWebSockets event loop until `close()` is called. pub fn App::listen_tls( self : App, cert_file : String, key_file : String, port : Int32, cb : (Bool) -> Unit, ) -> Unit { self.enable_tls(cert_file, key_file) self.listen(port, cb) } ///| /// Switches to TLS and starts listening on a specific host. This call blocks. pub fn App::listen_tls_host( self : App, cert_file : String, key_file : String, host : String, port : Int32, cb : (Bool) -> Unit, ) -> Unit { self.enable_tls(cert_file, key_file) self.listen_host(host, port, cb) } ///| /// Switches to TLS and starts listening without blocking. Call `run()` /// yourself afterwards. pub fn App::listen_tls_async( self : App, cert_file : String, key_file : String, host : String, port : Int32, cb : (Bool) -> Unit, ) -> Unit { self.enable_tls(cert_file, key_file) self.listen_async(host, port, cb) } ///| /// Runs the µWebSockets event loop. Blocks until `close()` is called. pub fn App::run(self : App) -> Unit { uwb_app_run(self.uws) } ///| /// Closes all sockets, which also stops the event loop. pub fn App::close(self : App) -> Unit { uwb_app_close(self.uws) } // ---------- dispatch ---------- ///| priv struct DispatchState { mut idx : Int32 mut error : String? mut error_status : Int32? } ///| /// Registers the three C callbacks. Runs once. /// /// The lambdas must not capture anything: a non-capturing MoonBit `FuncRef` /// is passed to C as a plain function pointer, which the shim calls directly. fn init_shim() -> Unit { if g_shim_ready[0] { return } g_shim_ready[0] = true let on_request : FuncRef[(UInt64, UwsRes, UwsReq) -> Unit] = ( app_id, uws_res, req, ) => dispatch_request(app_id, uws_res, req) let on_body : FuncRef[(UInt64, Bytes, Int32, Int32) -> Unit] = ( request_id, body, len, status, ) => dispatch_body(request_id, body, len, status) let on_abort : FuncRef[(UInt64) -> Unit] = request_id => { dispatch_abort(request_id) } uwb_set_callbacks(on_request, on_body, on_abort) init_websockets() } ///| /// Dispatches an incoming HTTP request to the registered app. pub fn dispatch_request( app_id : UInt64, uws_res : UwsRes, _uws_req : UwsReq, ) -> Unit { match g_apps.get(app_id) { Some(app) => start_request(app, uws_res) None => () } } ///| /// Dispatches request body data to the pending request. The raw bytes stay on /// the `Req` so that binary-safe parsers can use them. pub fn dispatch_body( request_id : UInt64, body : Bytes, _len : Int32, status : Int32, ) -> Unit { match g_requests.get(request_id) { Some(state) => { if status == 0 { state.req.set_body_bytes(body) } match state.body_reader { Some(reader) => { state.body_reader = None reader(utf8_string(body), status == 0) } None => () } } None => () } } ///| /// Dispatches an abort notification to the pending request. pub fn dispatch_abort(request_id : UInt64) -> Unit { match g_requests.get(request_id) { Some(state) => { state.req.mark_aborted() state.res.mark_aborted() release_request(request_id) } None => () } } ///| let g_shim_ready : Array[Bool] = [false] ///| fn start_request(app : App, uws_res : UwsRes) -> Unit { let request_id = next_id() let http_method = utf8_string(uwb_req_method()).to_upper() let url_path = utf8_string(uwb_req_url()) let query_raw = utf8_string(uwb_req_query()) let headers : Map[String, String] = Map([]) let header_count = uwb_req_header_count() let mut index = 0 while index < header_count { headers.set( utf8_string(uwb_req_header_name(index)).to_lower(), utf8_string(uwb_req_header_value(index)), ) index = index + 1 } let cookie_header = match headers.get("cookie") { Some(value) => value None => "" } let ssl_flag : Int32 = if app.is_ssl { 1 } else { 0 } let original_url = if query_raw == "" { url_path } else { url_path + "?" + query_raw } let req = Req::{ uws_res, request_id, app_ref: app, settings: app.settings, headers, http_method, url_path, base_url: "", original_url, params: Map([]), base_params: [], route_info: None, next: None, query_params: parse_query(query_raw), query_raw, body_text: "", body_bytes: Bytes::make(0, b'\x00'), body_json: None, body_form: Map([]), body_form_json: None, cookies: parse_cookies(cookie_header), remote_ip: utf8_string(uwb_res_remote_address(uws_res, ssl_flag)), aborted: false, locals: Map([]), protocol: if app.is_ssl { "https" } else { "http" }, } let res = Res::{ uws_res, request_id, settings: app.settings, status_code: 200, headers: [], ended: false, aborted: false, headers_sent: false, locals: Map([]), vary_headers: [], is_ssl: app.is_ssl, } let state : RequestState = { req, res, app, body_reader: None, } g_requests.set(request_id, state) uwb_res_on_aborted(uws_res, ssl_flag, request_id) dispatch(app, req, res) } ///| /// Starts reading the request body. `reader` receives `(body, ok)`. fn read_body( req : Req, res : Res, limit : Int32, reader : (String, Bool) -> Unit, ) -> Unit { match g_requests.get(res.request_id) { Some(state) => { state.body_reader = Some(reader) let ssl_flag : Int32 = if res.is_ssl { 1 } else { 0 } uwb_res_on_data(req.uws_res, ssl_flag, res.request_id, limit) } None => reader("", false) } } ///| fn dispatch(app : App, req : Req, res : Res) -> Unit { dispatch_layers(app.layers, app.param_callbacks, req, res, fn(state) { finalize(state, app, req, res) }) } ///| /// Runs a layer list with Express' middleware semantics: layers run in order, /// a layer that responds stops the chain, `next()` resumes it and errors skip /// to the next error handler. `on_done` runs when the list is exhausted. fn dispatch_layers( layers : Array[Layer], param_callbacks : Array[(String, (Req, Next) -> Unit)], req : Req, res : Res, on_done : (DispatchState) -> Unit, ) -> Unit { let state = DispatchState::{ idx: 0, error: None, error_status: None, } let steps : Array[() -> Unit] = [fn() { () }] let step = fn() { let mut running = true while running && state.idx < layers.length() { let layer = layers[state.idx] state.idx = state.idx + 1 match layer { Mw(pattern, middleware) => if state.error is None { match pattern.match_path(req.path(), true) { Some(params) => { req.set_params(params) running = false let next = make_next(state, steps[0]) req.set_next(next) run_param_callbacks(param_callbacks, req, state, steps[0], fn() { middleware(req, res, next) }) } None => () } } Mount(pattern, router, prefix) => if state.error is None { match pattern.match_path(req.path(), true) { Some(params) => { req.set_params(params) running = false let next = make_next(state, steps[0]) req.set_next(next) run_param_callbacks(param_callbacks, req, state, steps[0], fn() { mount_router(router, prefix, req, res, next) }) } None => () } } Err(pattern, middleware) => match state.error { Some(message) => match pattern.match_path(req.path(), true) { Some(params) => { req.set_params(params) state.error = None state.error_status = None running = false let next = make_next(state, steps[0]) req.set_next(next) run_param_callbacks(param_callbacks, req, state, steps[0], fn() { middleware(message, req, res, next) }) } None => () } None => () } Route(route_method, pattern, handler) => if state.error is None && method_matches(route_method, req.http_method()) { match pattern.match_path(req.path(), false) { Some(params) => { req.set_params(params) req.set_route( Some({ http_method: route_method, path: pattern.source(), }), ) running = false let next = make_next(state, steps[0]) req.set_next(next) run_param_callbacks(param_callbacks, req, state, steps[0], fn() { handler(req, res, next) }) } None => () } } } } if running { on_done(state) } } steps[0] = step step() } ///| /// Runs a mounted router: `req.url` loses the mount prefix, `req.base_url` /// accumulates it, and params captured by the mount stay visible when the /// router asked for `mergeParams`. Finishing the router resumes the parent /// chain; errors propagate to the parent's error middleware. fn mount_router( router : Router, prefix : String, req : Req, res : Res, next : Next, ) -> Unit { let saved_path = req.url_path let saved_base = req.base_url let saved_params = req.params let saved_base_params = req.base_params let saved_route = req.route_info let saved_next = req.next req.url_path = mount_relative(prefix, req.path()) req.base_url = req.base_url + prefix req.base_params = if router.merge_params { saved_params.to_array() } else { [] } dispatch_layers(router.layers, router.param_callbacks, req, res, fn(state) { req.url_path = saved_path req.base_url = saved_base req.params = saved_params req.base_params = saved_base_params req.route_info = saved_route req.next = saved_next next.resume_with_status(state.error, state.error_status) }) } ///| fn make_next(state : DispatchState, step : () -> Unit) -> Next { { run: fn(error) { state.error = error step() }, run_status: fn(status, message) { state.error = Some(message) state.error_status = Some(status) step() }, proceed_fn: step, } } ///| /// Runs the `param` callbacks that apply to the params captured by the layer /// currently being dispatched, in registration order, then invokes `then_fn` /// (normally the layer handler). Each callback receives a `Next`: calling /// `proceed()` moves on to the remaining callbacks and finally `then_fn`; /// calling `fail(message)` hands control to the error middleware. fn run_param_callbacks( param_callbacks : Array[(String, (Req, Next) -> Unit)], req : Req, state : DispatchState, dispatch_step : () -> Unit, then_fn : () -> Unit, ) -> Unit { let cursor : Array[Int32] = [0] let steps : Array[() -> Unit] = [fn() { () }] let step_params = fn() { let mut i = cursor[0] while i < param_callbacks.length() { let (name, callback) = param_callbacks[i] cursor[0] = i + 1 if req.has_param(name) { let next : Next = { run: fn(error) { state.error = error dispatch_step() }, run_status: fn(status, message) { state.error = Some(message) state.error_status = Some(status) dispatch_step() }, proceed_fn: steps[0], } callback(req, next) return } i = i + 1 } then_fn() } steps[0] = step_params step_params() } ///| fn method_matches(route_method : String, actual : String) -> Bool { route_method == "ALL" || route_method == actual || (actual == "HEAD" && route_method == "GET") } ///| fn finalize(state : DispatchState, app : App, req : Req, res : Res) -> Unit { if res.is_ended() || res.is_aborted() { return } match state.error { Some(message) => { let status = match state.error_status { Some(status) => status None => 500 } println("express: unhandled error: " + message) if res.headers_sent() { res.end_stream() return } if status >= 500 { res.status(status).send(status_text(status)) } else { res.status(status).send(message) } } None => if req.http_method() == "OPTIONS" { let methods = collect_options_methods(app.layers, req.path()) if methods.length() > 0 { let allow = methods.join(", ") res .status(200) .set("Allow", allow) .set("Content-Type", "text/plain") .set("X-Content-Type-Options", "nosniff") .send(allow) } else { res.status(404).send("Cannot " + req.http_method() + " " + req.path()) } } else { res.status(404).send("Cannot " + req.http_method() + " " + req.path()) } } } ///| /// The methods Express would advertise for an OPTIONS request: the methods of /// every route matching the path, plus `HEAD` for a `GET`, sorted and /// deduplicated. Mounted routers contribute their own routes. fn collect_options_methods( layers : Array[Layer], path : String, ) -> Array[String] { let found : Array[String] = [] for layer in layers { match layer { Route(route_method, pattern, _) => if route_method != "OPTIONS" && route_method != "ALL" && pattern.match_path(path, false) is Some(_) && !found.contains(route_method) { found.push(route_method) } Mount(pattern, router, prefix) => if pattern.match_path(path, true) is Some(_) { for verb in collect_options_methods( router.layers, mount_relative(prefix, path), ) { if !found.contains(verb) { found.push(verb) } } } _ => () } } if found.contains("GET") && !found.contains("HEAD") { found.push("HEAD") } found.sort() found }