///|
/// The main application type that holds route mappings, middleware, and WebSocket handlers.
///
/// Routing state is split by access pattern:
/// - `static_routes` — `Map[method, Map[path, handler]]` for O(1) static lookup
/// - `dynamic_routes` — radix tree for O(path_length) dynamic lookup
/// - `route_keys` — flat list of `(method, path)` for introspection via `routes()`
pub struct Mocket {
priv base_path : String
priv route_keys : Array[(String, String)]
priv middlewares : Array[(String, Middleware)]
priv static_routes : Map[String, Map[String, HttpHandler]]
priv dynamic_routes : RadixRouter[HttpHandler]
priv ws_static_routes : Map[String, WebSocketHandler]
// WebSocket dynamic routes share the radix tree for consistent precedence
// with HTTP routes. "WS" is used as a synthetic method key.
priv ws_dynamic_routes : RadixRouter[WebSocketHandler]
priv ws_runtime_id : String
priv mut not_found_handler : HttpHandler?
fn new(base_path? : String) -> Mocket
}
///|
let mocket_instance_counter : Ref[Int] = Ref::new(0)
///|
fn next_mocket_instance_id() -> String {
mocket_instance_counter.val += 1
"mocket-\{mocket_instance_counter.val}"
}
///|
/// Creates a new Mocket application instance with an optional base path prefix.
pub fn Mocket::new(base_path? : String = "") -> Mocket {
{
base_path,
route_keys: [],
middlewares: [],
static_routes: {},
dynamic_routes: RadixRouter::new(),
ws_static_routes: {},
ws_dynamic_routes: RadixRouter::new(),
ws_runtime_id: next_mocket_instance_id(),
not_found_handler: None,
}
}
///|
/// Returns an iterator over the registered route keys as `(method, path)` pairs.
pub fn Mocket::routes(self : Mocket) -> Iter[(String, String)] {
self.route_keys.iter()
}
///|
/// The SameSite attribute for cookies, controlling cross-site request behavior.
pub(all) enum SameSiteOption {
Lax
Strict
SameSiteNone
} derive(Debug, Eq)
///|
pub impl Show for SameSiteOption with output(self, logger) {
match self {
Lax => logger.write_string("Lax")
Strict => logger.write_string("Strict")
SameSiteNone => logger.write_string("SameSiteNone")
}
}
///|
pub impl ToJson for SameSiteOption with to_json(self : SameSiteOption) -> Json {
match self {
Lax => "lax"
Strict => "strict"
SameSiteNone => "none"
}
}
///|
/// Represents an HTTP cookie with its name, value, and optional attributes.
pub(all) struct CookieItem {
name : String
value : String
max_age : Int?
path : String?
domain : String?
secure : Bool?
http_only : Bool?
same_site : SameSiteOption?
fn new(
name~ : String,
value~ : String,
max_age? : Int,
path? : String,
domain? : String,
secure? : Bool,
http_only? : Bool,
same_site? : SameSiteOption,
) -> CookieItem
} derive(Eq)
///|
/// Creates a new `CookieItem` with the given name, value, and optional attributes.
pub fn CookieItem::new(
name~ : String,
value~ : String,
max_age? : Int,
path? : String,
domain? : String,
secure? : Bool,
http_only? : Bool,
same_site? : SameSiteOption,
) -> CookieItem {
{ name, value, max_age, path, domain, secure, http_only, same_site }
}
///|
fn sanitize_cookie_value(s : String) -> String {
if s.contains("\r") || s.contains("\n") || s.contains(";") {
let buf = StringBuilder::new()
for c in s {
if c != '\r' && c != '\n' && c != ';' {
buf.write_char(c)
}
}
buf.to_string()
} else {
s
}
}
///|
pub impl Show for CookieItem with output(self, logger) -> Unit {
logger.write_string(sanitize_cookie_value(self.name))
logger.write_char('=')
logger.write_string(sanitize_cookie_value(self.value))
if self.max_age is Some(max_age) {
logger.write_string("; Max-Age=")
logger.write_string(max_age.to_string())
}
if self.path is Some(path) {
logger.write_string("; Path=")
logger.write_string(path)
}
if self.domain is Some(domain) {
logger.write_string("; Domain=")
logger.write_string(domain)
}
if self.secure == Some(true) {
logger.write_string("; Secure")
}
if self.http_only == Some(true) {
logger.write_string("; HttpOnly")
}
if self.same_site is Some(same_site) {
logger.write_string("; SameSite=")
logger.write_string(same_site.to_string())
}
}
///|
pub impl Show for CookieItem with to_string(self : CookieItem) -> String {
let buf = @buffer.new()
self.output(buf)
buf.to_string()
}
///|
/// Registers a route handler for the given HTTP method and path.
pub fn Mocket::on(
self : Mocket,
event : String,
path : String,
handler : HttpHandler,
) -> Unit {
let path = self.base_path + path
self.route_keys.push((event, path))
// Cache routes by path type for faster lookup
if !path.contains(":") && !path.contains("*") {
// Static path: cache directly
if self.static_routes.get(event) is Some(method_routes) {
method_routes.set(path, handler)
} else {
let new_routes : Map[String, HttpHandler] = Map::new()
new_routes.set(path, handler)
self.static_routes.set(event, new_routes)
}
// Dynamic path: compile and insert into radix tree
} else {
let compiled = CompiledRoute::compile(path)
self.dynamic_routes.insert(event, compiled, handler)
}
}
///|
/// Registers a noraise GET handler. For most use cases, prefer `get()`.
pub fn Mocket::get_raw(
self : Mocket,
path : String,
handler : HttpHandler,
) -> Unit {
self.on("GET", path, handler)
}
///|
/// Registers a noraise POST handler.
pub fn Mocket::post_raw(
self : Mocket,
path : String,
handler : HttpHandler,
) -> Unit {
self.on("POST", path, handler)
}
///|
/// Registers a noraise PATCH handler.
pub fn Mocket::patch_raw(
self : Mocket,
path : String,
handler : HttpHandler,
) -> Unit {
self.on("PATCH", path, handler)
}
///|
/// Registers a noraise CONNECT handler.
pub fn Mocket::connect_raw(
self : Mocket,
path : String,
handler : HttpHandler,
) -> Unit {
self.on("CONNECT", path, handler)
}
///|
/// Registers a noraise PUT handler.
pub fn Mocket::put_raw(
self : Mocket,
path : String,
handler : HttpHandler,
) -> Unit {
self.on("PUT", path, handler)
}
///|
/// Registers a noraise DELETE handler.
pub fn Mocket::delete_raw(
self : Mocket,
path : String,
handler : HttpHandler,
) -> Unit {
self.on("DELETE", path, handler)
}
///|
/// Registers a noraise HEAD handler.
pub fn Mocket::head_raw(
self : Mocket,
path : String,
handler : HttpHandler,
) -> Unit {
self.on("HEAD", path, handler)
}
///|
/// Registers a noraise OPTIONS handler.
pub fn Mocket::options_raw(
self : Mocket,
path : String,
handler : HttpHandler,
) -> Unit {
self.on("OPTIONS", path, handler)
}
///|
/// Registers a noraise TRACE handler.
pub fn Mocket::trace_raw(
self : Mocket,
path : String,
handler : HttpHandler,
) -> Unit {
self.on("TRACE", path, handler)
}
///|
/// Registers a noraise handler that matches all HTTP methods.
pub fn Mocket::all_raw(
self : Mocket,
path : String,
handler : HttpHandler,
) -> Unit {
self.on("*", path, handler)
}
///|
/// Creates a route group with a shared base path prefix, merging its routes and middleware into the app.
pub fn Mocket::group(
self : Mocket,
base_path : String,
configure : (Mocket) -> Unit,
) -> Unit {
let group = Mocket(base_path=self.base_path + base_path)
configure(group)
// Merge route keys for introspection
self.route_keys.append(group.route_keys)
// Merge static route tables
group.static_routes
.iter()
.each(i => {
let http_method = i.0
let group_routes = i.1
match self.static_routes.get(http_method) {
Some(existing_routes) =>
group_routes.iter().each(route => existing_routes.set(route.0, route.1))
None => self.static_routes.set(http_method, group_routes)
}
})
// Merge dynamic routes (radix tree)
self.dynamic_routes.merge(group.dynamic_routes)
group.ws_static_routes.iter().each(i => self.ws_static_routes.set(i.0, i.1))
self.ws_dynamic_routes.merge(group.ws_dynamic_routes)
// Merge middlewares
self.middlewares.append(group.middlewares)
// Merge not_found_handler (group's handler takes precedence if set)
if group.not_found_handler is Some(_) {
self.not_found_handler = group.not_found_handler
}
}
///|
/// Sets a custom handler for 404 Not Found responses.
pub fn Mocket::set_not_found_handler(
self : Mocket,
handler : HttpHandler,
) -> Unit {
self.not_found_handler = Some(handler)
}
///|
/// Returns true if a custom not-found handler has been registered.
pub fn Mocket::has_not_found_handler(self : Mocket) -> Bool {
self.not_found_handler is Some(_)
}
///|
/// Registers a WebSocket handler for the given path, matched regardless of HTTP method.
///
/// Dynamic patterns (`:param`, `*`, `**`) are inserted into the same radix
/// tree implementation used for HTTP routes, giving them consistent precedence:
/// static > param > wildcard > globstar. Re-registering the same path overrides
/// the previous handler.
pub fn Mocket::ws(
self : Mocket,
path : String,
handler : WebSocketHandler,
) -> Unit {
let path = self.base_path + path
// Static path: direct map lookup
if !path.contains(":") && !path.contains("*") {
self.ws_static_routes.set(path, handler)
} else {
// Dynamic path: insert into radix tree (WS is a synthetic method key)
let compiled = CompiledRoute::compile(path)
self.ws_dynamic_routes.insert("WS", compiled, handler)
}
}