///|
/// Route groups — prefix-based grouping with shared middleware.
///
/// Groups allow you to organize routes under a common path prefix
/// and apply middleware to all routes within the group.
///
/// ## Example
///
/// ```
/// let api = app.group("/api")
/// api.use(auth_middleware)
/// api.get("/users", [list_users])
/// api.post("/users", [create_user])
/// // Registers: GET /api/users, POST /api/users (both with auth middleware)
/// ```
///|
/// A route group shares a path prefix and middleware.
pub(all) struct Group {
router : Router
prefix : String
/// Middleware applied to all routes in this group
middlewares : Array[Handler]
}
///|
/// Create a new route group.
pub fn Group::new(
router : Router,
prefix : String,
) -> Group {
{ router, prefix, middlewares: [] }
}
///|
/// Add middleware to this group. Middleware is applied to all routes
/// registered through this group, in the order added.
/// Returns the group for method chaining.
pub fn Group::use(self : Group, mw : Handler) -> Group {
self.middlewares.push(mw)
self
}
///|
/// Return the middleware handlers attached to this group.
/// See `group.Handlers` equivalent.
pub fn Group::handlers(self : Group) -> Array[Handler] {
self.middlewares
}
///|
/// Normalize a pattern by joining the group prefix with the route pattern.
fn Group::full_path(self : Group, pattern : String) -> String {
if self.prefix == "" || self.prefix == "/" {
return pattern
}
if pattern == "/" {
return self.prefix
}
if pattern.has_prefix("/") {
return self.prefix + pattern
}
return self.prefix + "/" + pattern
}
///|
/// Build the full handler chain: group middleware + route handlers.
fn Group::build_chain(self : Group, handlers : Array[Handler]) -> Array[Handler] {
let chain : Array[Handler] = []
for mw in self.middlewares {
chain.push(mw)
}
for h in handlers {
chain.push(h)
}
chain
}
///|
/// Register a GET route under this group.
pub fn Group::get(
self : Group,
pattern : String,
handlers : Array[Handler],
) -> Unit {
self.router.get(self.full_path(pattern), self.build_chain(handlers))
}
///|
/// Register a POST route under this group.
pub fn Group::post(
self : Group,
pattern : String,
handlers : Array[Handler],
) -> Unit {
self.router.post(self.full_path(pattern), self.build_chain(handlers))
}
///|
/// Register a PUT route under this group.
pub fn Group::put(
self : Group,
pattern : String,
handlers : Array[Handler],
) -> Unit {
self.router.put(self.full_path(pattern), self.build_chain(handlers))
}
///|
/// Register a DELETE route under this group.
pub fn Group::del(
self : Group,
pattern : String,
handlers : Array[Handler],
) -> Unit {
self.router.del(self.full_path(pattern), self.build_chain(handlers))
}
///|
/// Register a PATCH route under this group.
pub fn Group::patch(
self : Group,
pattern : String,
handlers : Array[Handler],
) -> Unit {
self.router.patch(self.full_path(pattern), self.build_chain(handlers))
}
///|
/// Register a HEAD route under this group.
pub fn Group::head(
self : Group,
pattern : String,
handlers : Array[Handler],
) -> Unit {
self.router.head(self.full_path(pattern), self.build_chain(handlers))
}
///|
/// Register an OPTIONS route under this group.
pub fn Group::options(
self : Group,
pattern : String,
handlers : Array[Handler],
) -> Unit {
self.router.options(self.full_path(pattern), self.build_chain(handlers))
}
///|
/// Handle multiple HTTP methods for the same pattern under this group.
pub fn Group::handle(
self : Group,
methods : Array[Method],
pattern : String,
handlers : Array[Handler],
) -> Unit {
self.router.handle(methods, self.full_path(pattern), self.build_chain(handlers))
}
///|
/// Register a route that matches ANY HTTP method under this group.
///
/// ```
/// group.any("/health", [health_check])
/// ```
pub fn Group::any(
self : Group,
pattern : String,
handlers : Array[Handler],
) -> Unit {
self.router.any(self.full_path(pattern), self.build_chain(handlers))
}
///|
/// Create a nested group under this group.
/// The nested group inherits this group's prefix and middleware.
pub fn Group::group(self : Group, prefix : String) -> Group {
let nested = Group::new(self.router, self.full_path(prefix))
// Inherit parent middleware
for mw in self.middlewares {
nested.middlewares.push(mw)
}
nested
}
///|
/// Return the base path of this group.
///
/// ```
/// let api = app.group("/api/v1")
/// println(api.base_path()) // "/api/v1"
/// ```
pub fn Group::base_path(self : Group) -> String {
self.prefix
}
///|
/// Serve static files from a directory under this group's prefix.
///
/// ```
/// let assets = app.group("/assets")
/// assets.static("/css", "./public/css")
/// ```
pub fn Group::static_files(
self : Group,
relative_path : String,
root : String,
) -> Unit {
let prefix = self.full_path(relative_path)
self.router.get(
prefix + "/*filepath",
self.build_chain([
async fn(ctx) {
let file = ctx.param_default("filepath", "")
let file_path = root + "/" + file
try {
let content = @fs.read_file(file_path).text()
let ct = guess_content_type(file_path)
ctx.data(200, ct, content)
} catch {
_ => ctx.abort_with_status(404, "File not found")
}
},
]),
)
}
///|
/// Serve a single static file under this group's prefix.
///
/// ```
/// let assets = app.group("/assets")
/// assets.static_file("/favicon.ico", "./public/favicon.ico")
/// ```
pub fn Group::static_file(
self : Group,
relative_path : String,
file_path : String,
) -> Unit {
self.router.get(
self.full_path(relative_path),
self.build_chain([
async fn(ctx) {
try {
let content = @fs.read_file(file_path).text()
let ct = guess_content_type(file_path)
ctx.data(200, ct, content)
} catch {
_ => ctx.abort_with_status(404, "File not found")
}
},
]),
)
}
///|
/// Serve static files from a file system under this group's prefix.
/// See `group.StaticFS()` equivalent.
///
/// ```
/// let assets = app.group("/assets")
/// assets.static_fs("/css", "./public/css")
/// ```
pub fn Group::static_fs(
self : Group,
relative_path : String,
root : String,
) -> Unit {
self.static_files(relative_path, root)
}