use rustc_hash::FxHashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RouteMethod {
Get,
Post,
Put,
Delete,
Patch,
Head,
Options,
Trace,
Connect,
Any,
}
impl RouteMethod {
pub fn as_str(&self) -> &'static str {
match self {
RouteMethod::Get => "GET",
RouteMethod::Post => "POST",
RouteMethod::Put => "PUT",
RouteMethod::Delete => "DELETE",
RouteMethod::Patch => "PATCH",
RouteMethod::Head => "HEAD",
RouteMethod::Options => "OPTIONS",
RouteMethod::Trace => "TRACE",
RouteMethod::Connect => "CONNECT",
RouteMethod::Any => "ANY",
}
}
pub fn matches(&self, method: zenith_api::Method) -> bool {
if *self == RouteMethod::Any {
return true;
}
matches!(
(self, method),
(RouteMethod::Get, zenith_api::Method::Get)
| (RouteMethod::Post, zenith_api::Method::Post)
| (RouteMethod::Put, zenith_api::Method::Put)
| (RouteMethod::Delete, zenith_api::Method::Delete)
| (RouteMethod::Patch, zenith_api::Method::Patch)
| (RouteMethod::Head, zenith_api::Method::Head)
| (RouteMethod::Options, zenith_api::Method::Options)
| (RouteMethod::Trace, zenith_api::Method::Trace)
| (RouteMethod::Connect, zenith_api::Method::Connect)
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseRouteMethodError(pub String);
impl std::fmt::Display for ParseRouteMethodError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "unknown route method: {}", self.0)
}
}
impl std::error::Error for ParseRouteMethodError {}
impl std::str::FromStr for RouteMethod {
type Err = ParseRouteMethodError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_uppercase().as_str() {
"GET" => Ok(RouteMethod::Get),
"POST" => Ok(RouteMethod::Post),
"PUT" => Ok(RouteMethod::Put),
"DELETE" => Ok(RouteMethod::Delete),
"PATCH" => Ok(RouteMethod::Patch),
"HEAD" => Ok(RouteMethod::Head),
"OPTIONS" => Ok(RouteMethod::Options),
"TRACE" => Ok(RouteMethod::Trace),
"CONNECT" => Ok(RouteMethod::Connect),
other => Err(ParseRouteMethodError(other.to_string())),
}
}
}
const MAX_PATH_SEGMENTS: usize = 16;
#[derive(Debug, Clone)]
struct PathSegments<'a> {
segments: [&'a [u8]; MAX_PATH_SEGMENTS],
count: usize,
truncated: bool,
}
impl<'a> PathSegments<'a> {
fn new() -> Self {
Self {
segments: [&[]; MAX_PATH_SEGMENTS],
count: 0,
truncated: false,
}
}
fn push(&mut self, segment: &'a [u8]) {
if self.count < MAX_PATH_SEGMENTS {
self.segments[self.count] = segment;
self.count += 1;
}
}
fn as_slice(&self) -> &[&'a [u8]] {
&self.segments[..self.count]
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum SegmentType {
Static(String),
Param(String),
Wildcard(Option<String>),
}
#[derive(Debug, Clone)]
struct RouteNode {
segment_type: SegmentType,
children: Vec<RouteNode>,
handlers: FxHashMap<RouteMethod, usize>,
}
impl Default for RouteNode {
fn default() -> Self {
Self {
segment_type: SegmentType::Static(String::new()),
children: Vec::new(),
handlers: FxHashMap::default(),
}
}
}
impl RouteNode {
fn new(segment_type: SegmentType) -> Self {
Self {
segment_type,
children: Vec::new(),
handlers: FxHashMap::default(),
}
}
}
#[derive(Debug, Clone)]
pub struct RouteEntry {
pub method: RouteMethod,
pub path: String,
pub handler_id: usize,
}
const MAX_PARAMS: usize = 8;
const MAX_PARAM_NAME_LEN: usize = 32;
const MAX_PARAM_VALUE_LEN: usize = 256;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RouteParam {
name: [u8; MAX_PARAM_NAME_LEN],
name_len: usize,
value: [u8; MAX_PARAM_VALUE_LEN],
value_len: usize,
}
impl RouteParam {
#[inline]
pub fn new(name: &[u8], value: &[u8]) -> Self {
let mut result = Self {
name: [0u8; MAX_PARAM_NAME_LEN],
name_len: name.len().min(MAX_PARAM_NAME_LEN),
value: [0u8; MAX_PARAM_VALUE_LEN],
value_len: value.len().min(MAX_PARAM_VALUE_LEN),
};
result.name[..result.name_len].copy_from_slice(&name[..result.name_len]);
result.value[..result.value_len].copy_from_slice(&value[..result.value_len]);
result
}
#[inline]
pub fn name(&self) -> &str {
std::str::from_utf8(&self.name[..self.name_len]).unwrap_or("")
}
#[inline]
pub fn value(&self) -> &str {
std::str::from_utf8(&self.value[..self.value_len]).unwrap_or("")
}
}
#[derive(Debug, Clone)]
pub struct RouteMatch {
pub handler_id: usize,
params: [RouteParam; MAX_PARAMS],
param_count: usize,
}
impl RouteMatch {
#[inline]
pub fn new(handler_id: usize) -> Self {
Self {
handler_id,
params: std::array::from_fn(|_| RouteParam {
name: [0u8; MAX_PARAM_NAME_LEN],
name_len: 0,
value: [0u8; MAX_PARAM_VALUE_LEN],
value_len: 0,
}),
param_count: 0,
}
}
#[inline]
pub fn add_param(&mut self, name: &[u8], value: &[u8]) {
if self.param_count < MAX_PARAMS {
self.params[self.param_count] = RouteParam::new(name, value);
self.param_count += 1;
}
}
#[inline]
pub fn param_count(&self) -> usize {
self.param_count
}
#[inline]
pub fn params(&self) -> &[RouteParam] {
&self.params[..self.param_count]
}
#[inline]
pub fn get(&self, name: &str) -> Option<&str> {
let name_bytes = name.as_bytes();
for i in 0..self.param_count {
let param = &self.params[i];
if param.name_len == name_bytes.len() && param.name[..param.name_len] == *name_bytes {
return Some(param.value());
}
}
None
}
}
#[derive(Debug, Clone, Default)]
pub struct Router {
root: RouteNode,
routes: Vec<RouteEntry>,
}
struct SearchCtx<'a> {
params: &'a mut [RouteParam; MAX_PARAMS],
param_count: &'a mut usize,
any_method: bool,
}
impl<'a> SearchCtx<'a> {
#[inline]
fn new(
params: &'a mut [RouteParam; MAX_PARAMS],
param_count: &'a mut usize,
any_method: bool,
) -> Self {
Self { params, param_count, any_method }
}
#[inline]
fn try_reserve(&mut self, name: &str, value: &[u8]) -> bool {
if *self.param_count < MAX_PARAMS {
self.params[*self.param_count] = RouteParam::new(name.as_bytes(), value);
*self.param_count += 1;
true
} else {
false
}
}
#[inline]
fn rollback(&mut self) {
if *self.param_count > 0 {
*self.param_count -= 1;
}
}
}
impl Router {
pub fn new() -> Self {
Self {
root: RouteNode::new(SegmentType::Static(String::new())),
routes: Vec::new(),
}
}
pub fn add_route(&mut self, method: RouteMethod, path: &str, handler_id: usize) {
let segments = Self::parse_path(path);
Self::insert_segment(&mut self.root, &segments, 0, method, handler_id);
self.routes.push(RouteEntry {
method,
path: path.to_string(),
handler_id,
});
}
fn parse_path(path: &str) -> Vec<SegmentType> {
let trimmed = path.trim_matches('/');
if trimmed.is_empty() {
return vec![SegmentType::Static(String::new())];
}
trimmed
.split('/')
.map(|seg| {
if let Some(name) = seg.strip_prefix(':') {
SegmentType::Param(name.to_string())
} else if seg == "*" || seg == "**" {
SegmentType::Wildcard(None)
} else if let Some(name) = seg.strip_prefix('*') {
SegmentType::Wildcard(Some(name.to_string()))
} else {
SegmentType::Static(seg.to_string())
}
})
.collect()
}
fn insert_segment(
node: &mut RouteNode,
segments: &[SegmentType],
depth: usize,
method: RouteMethod,
handler_id: usize,
) {
if depth == segments.len() {
node.handlers.insert(method, handler_id);
return;
}
let segment = &segments[depth];
let child = node.children.iter_mut().find(|child| {
match (&child.segment_type, segment) {
(SegmentType::Static(a), SegmentType::Static(b)) => a == b,
(SegmentType::Param(a), SegmentType::Param(b)) => a == b,
(SegmentType::Wildcard(_), SegmentType::Wildcard(_)) => true,
_ => false,
}
});
if let Some(child) = child {
Self::insert_segment(child, segments, depth + 1, method, handler_id);
} else {
let mut new_child = RouteNode::new(segment.clone());
Self::insert_segment(&mut new_child, segments, depth + 1, method, handler_id);
node.children.push(new_child);
}
}
pub fn match_route(
&self,
method: zenith_api::Method,
path: &[u8],
) -> Option<RouteMatch> {
let effective_path = if path.is_empty() { b"/" } else { path };
let segments = Self::split_path_bytes(effective_path);
if segments.truncated {
return None;
}
let mut params = [RouteParam {
name: [0u8; MAX_PARAM_NAME_LEN],
name_len: 0,
value: [0u8; MAX_PARAM_VALUE_LEN],
value_len: 0,
}; MAX_PARAMS];
let mut param_count = 0;
let mut ctx = SearchCtx::new(&mut params, &mut param_count, false);
self.search(&self.root, segments.as_slice(), 0, method, &mut ctx)
}
pub fn path_exists(&self, path: &[u8]) -> bool {
let effective_path = if path.is_empty() { b"/" } else { path };
let segments = Self::split_path_bytes(effective_path);
if segments.truncated {
return false;
}
let mut params = [RouteParam {
name: [0u8; MAX_PARAM_NAME_LEN],
name_len: 0,
value: [0u8; MAX_PARAM_VALUE_LEN],
value_len: 0,
}; MAX_PARAMS];
let mut param_count = 0;
let mut ctx = SearchCtx::new(&mut params, &mut param_count, true);
self.search(&self.root, segments.as_slice(), 0, zenith_api::Method::Get, &mut ctx).is_some()
}
fn split_path_bytes(path: &[u8]) -> PathSegments<'_> {
let mut segments = PathSegments::new();
let mut start = 0;
while start < path.len() && path[start] == b'/' {
start += 1;
}
if start >= path.len() {
segments.push(&[]);
return segments;
}
let mut end = start;
while end < path.len() && segments.count < MAX_PATH_SEGMENTS {
if path[end] == b'/' {
if end > start {
segments.push(&path[start..end]);
}
while end < path.len() && path[end] == b'/' {
end += 1;
}
start = end;
} else {
end += 1;
}
}
if end > start && segments.count < MAX_PATH_SEGMENTS {
segments.push(&path[start..end]);
}
if end < path.len() || (end > start && segments.count >= MAX_PATH_SEGMENTS) {
segments.truncated = true;
}
segments
}
fn search(
&self,
node: &RouteNode,
segments: &[&[u8]],
depth: usize,
method: zenith_api::Method,
ctx: &mut SearchCtx<'_>,
) -> Option<RouteMatch> {
if depth == segments.len() {
let handler = if ctx.any_method {
node.handlers.values().next()
} else {
node.handlers.iter().find(|(m, _)| m.matches(method)).map(|(_, id)| id)
};
return handler.map(|handler_id| {
let mut result = RouteMatch::new(*handler_id);
let count = *ctx.param_count;
result.params[..count].copy_from_slice(&ctx.params[..count]);
result.param_count = count;
result
});
}
let segment = segments[depth];
for child in &node.children {
if let SegmentType::Static(expected) = &child.segment_type
&& expected.as_bytes() == segment
&& let Some(result) =
self.search(child, segments, depth + 1, method, ctx)
{
return Some(result);
}
}
for child in &node.children {
match &child.segment_type {
SegmentType::Static(_) => {
}
SegmentType::Param(name) => {
let reserved = ctx.try_reserve(name, segment);
if let Some(result) =
self.search(child, segments, depth + 1, method, ctx)
{
return Some(result);
}
if reserved {
ctx.rollback();
}
}
SegmentType::Wildcard(name_opt) => {
let param_name = name_opt.as_deref().unwrap_or("wildcard");
if *ctx.param_count < MAX_PARAMS {
let mut value_buf = [0u8; MAX_PARAM_VALUE_LEN];
let mut value_len = 0;
for (i, s) in segments[depth..].iter().enumerate() {
if i > 0 && value_len < MAX_PARAM_VALUE_LEN {
value_buf[value_len] = b'/';
value_len += 1;
}
let copy_len = s.len().min(MAX_PARAM_VALUE_LEN - value_len);
value_buf[value_len..value_len + copy_len].copy_from_slice(&s[..copy_len]);
value_len += copy_len;
if value_len >= MAX_PARAM_VALUE_LEN {
break;
}
}
let idx = *ctx.param_count;
ctx.params[idx] = RouteParam {
name: [0u8; MAX_PARAM_NAME_LEN],
name_len: param_name.len().min(MAX_PARAM_NAME_LEN),
value: value_buf,
value_len,
};
ctx.params[idx].name[..ctx.params[idx].name_len]
.copy_from_slice(param_name.as_bytes());
*ctx.param_count += 1;
}
if let Some(result) =
self.search(child, segments, segments.len(), method, ctx)
{
return Some(result);
}
ctx.rollback();
}
}
}
None
}
pub fn routes(&self) -> &[RouteEntry] {
&self.routes
}
pub fn route_count(&self) -> usize {
self.routes.len()
}
pub fn validate(&self) -> Result<(), crate::error::RouterError> {
use rustc_hash::FxHashSet;
let mut seen: FxHashSet<(&str, RouteMethod)> = FxHashSet::default();
for route in &self.routes {
let key = (route.path.as_str(), route.method);
if !seen.insert(key) {
return Err(crate::error::RouterError::Conflict(format!(
"Duplicate route: {} {}",
route.method.as_str(),
route.path
)));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_static_route() {
let mut router = Router::new();
router.add_route(RouteMethod::Get, "/api/health", 1);
let result = router.match_route(zenith_api::Method::Get, b"/api/health");
assert!(result.is_some());
assert_eq!(result.unwrap().handler_id, 1);
let result = router.match_route(zenith_api::Method::Get, b"/api/status");
assert!(result.is_none());
}
#[test]
fn test_static_preferred_over_param_regardless_of_insertion_order() {
let mut router = Router::new();
router.add_route(RouteMethod::Get, "/users/:id", 2);
router.add_route(RouteMethod::Get, "/users/new", 1);
let result = router.match_route(zenith_api::Method::Get, b"/users/new");
assert!(
result.is_some(),
"static route /users/new must resolve"
);
let result = result.unwrap();
assert_eq!(result.handler_id, 1);
assert!(result.get("id").is_none());
let mut router2 = Router::new();
router2.add_route(RouteMethod::Get, "/users/new", 1);
router2.add_route(RouteMethod::Get, "/users/:id", 2);
assert_eq!(
router2
.match_route(zenith_api::Method::Get, b"/users/new")
.unwrap()
.handler_id,
1
);
let result = router
.match_route(zenith_api::Method::Get, b"/users/123")
.unwrap();
assert_eq!(result.handler_id, 2);
assert_eq!(result.get("id").unwrap(), "123");
let mut router3 = Router::new();
router3.add_route(RouteMethod::Get, "/users/:id/profile", 9);
router3.add_route(RouteMethod::Get, "/users/admin", 8);
assert_eq!(
router3
.match_route(zenith_api::Method::Get, b"/users/admin")
.unwrap()
.handler_id,
8
);
assert_eq!(
router3
.match_route(zenith_api::Method::Get, b"/users/bob/profile")
.unwrap()
.handler_id,
9
);
let mut router4 = Router::new();
router4.add_route(RouteMethod::Get, "/files/*", 11);
router4.add_route(RouteMethod::Get, "/files/:name", 10);
assert_eq!(
router4
.match_route(zenith_api::Method::Get, b"/files/a")
.unwrap()
.handler_id,
11,
"wildcard registered first should win in dynamic round"
);
let mut router5 = Router::new();
router5.add_route(RouteMethod::Get, "/files/:name", 10);
router5.add_route(RouteMethod::Get, "/files/*", 11);
let hit = router5
.match_route(zenith_api::Method::Get, b"/files/a")
.unwrap();
assert_eq!(
hit.handler_id, 10,
"param registered first should win in dynamic round"
);
assert_eq!(hit.get("name").unwrap(), "a");
}
#[test]
fn test_param_route() {
let mut router = Router::new();
router.add_route(RouteMethod::Get, "/users/:id", 2);
let result = router.match_route(zenith_api::Method::Get, b"/users/123");
assert!(result.is_some());
let m = result.unwrap();
assert_eq!(m.handler_id, 2);
assert_eq!(m.get("id").unwrap(), "123");
let result = router.match_route(zenith_api::Method::Get, b"/users/abc");
assert!(result.is_some());
assert_eq!(result.unwrap().get("id").unwrap(), "abc");
}
#[test]
fn test_wildcard_route() {
let mut router = Router::new();
router.add_route(RouteMethod::Get, "/files/*", 3);
let result = router.match_route(zenith_api::Method::Get, b"/files/foo/bar.txt");
assert!(result.is_some());
let m = result.unwrap();
assert_eq!(m.handler_id, 3);
assert_eq!(m.get("wildcard").unwrap(), "foo/bar.txt");
}
#[test]
fn test_named_wildcard_route() {
let mut router = Router::new();
router.add_route(RouteMethod::Get, "/assets/*path", 5);
let result = router.match_route(zenith_api::Method::Get, b"/assets/css/main.css");
assert!(result.is_some());
let m = result.unwrap();
assert_eq!(m.handler_id, 5);
assert_eq!(m.get("path").unwrap(), "css/main.css");
assert!(m.get("wildcard").is_none());
}
#[test]
fn test_method_matching() {
let mut router = Router::new();
router.add_route(RouteMethod::Get, "/api", 1);
router.add_route(RouteMethod::Post, "/api", 2);
let result = router.match_route(zenith_api::Method::Get, b"/api");
assert_eq!(result.unwrap().handler_id, 1);
let result = router.match_route(zenith_api::Method::Post, b"/api");
assert_eq!(result.unwrap().handler_id, 2);
let result = router.match_route(zenith_api::Method::Put, b"/api");
assert!(result.is_none());
}
#[test]
fn test_any_method() {
let mut router = Router::new();
router.add_route(RouteMethod::Any, "/catch-all", 1);
for method in [
zenith_api::Method::Get,
zenith_api::Method::Post,
zenith_api::Method::Put,
zenith_api::Method::Delete,
] {
let result = router.match_route(method, b"/catch-all");
assert!(result.is_some(), "Method {:?} should match", method);
}
}
#[test]
fn test_nested_routes() {
let mut router = Router::new();
router.add_route(RouteMethod::Get, "/api/v1/users/:id/posts/:post_id", 1);
let result =
router.match_route(zenith_api::Method::Get, b"/api/v1/users/42/posts/99");
assert!(result.is_some());
let m = result.unwrap();
assert_eq!(m.handler_id, 1);
assert_eq!(m.get("id").unwrap(), "42");
assert_eq!(m.get("post_id").unwrap(), "99");
}
#[test]
fn test_route_validation() {
let mut router = Router::new();
router.add_route(RouteMethod::Get, "/api", 1);
router.add_route(RouteMethod::Get, "/api", 2);
assert!(router.validate().is_err());
let mut clean_router = Router::new();
clean_router.add_route(RouteMethod::Get, "/api", 1);
clean_router.add_route(RouteMethod::Post, "/api", 2);
assert!(clean_router.validate().is_ok());
}
#[test]
fn test_root_path() {
let mut router = Router::new();
router.add_route(RouteMethod::Get, "/", 1);
let result = router.match_route(zenith_api::Method::Get, b"/");
assert!(result.is_some());
assert_eq!(result.unwrap().handler_id, 1);
}
#[test]
fn test_trailing_slash() {
let mut router = Router::new();
router.add_route(RouteMethod::Get, "/api/test", 1);
let result = router.match_route(zenith_api::Method::Get, b"/api/test/");
assert!(result.is_some());
}
#[test]
fn test_path_exists() {
let mut router = Router::new();
router.add_route(RouteMethod::Get, "/api/data", 1);
router.add_route(RouteMethod::Post, "/api/data", 2);
assert!(router.path_exists(b"/api/data"));
assert!(!router.path_exists(b"/api/missing"));
}
#[test]
fn test_path_exists_with_params() {
let mut router = Router::new();
router.add_route(RouteMethod::Get, "/users/:id", 1);
assert!(router.path_exists(b"/users/42"));
assert!(!router.path_exists(b"/users"));
}
#[test]
fn test_path_exists_with_wildcard() {
let mut router = Router::new();
router.add_route(RouteMethod::Get, "/static/*", 1);
assert!(router.path_exists(b"/static/css/style.css"));
assert!(!router.path_exists(b"/dynamic/css/style.css"));
}
}