use crate::handler::BoxedHandler;
use crate::route_constraint::RouteConstraints;
use crate::routing::Router;
use crate::{Error, HttpMethod, HttpRequest, HttpResponse};
use lru::LruCache;
use parking_lot::Mutex;
use smallvec::SmallVec;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize;
use std::sync::atomic::{AtomicU64, Ordering};
const INLINE_PATH_SEGMENTS: usize = 8;
#[derive(Clone, Debug, Eq)]
pub struct RouteKey {
method: HttpMethod,
path: String,
}
impl RouteKey {
#[inline]
pub fn new(method: HttpMethod, path: impl Into<String>) -> Self {
Self {
method,
path: path.into(),
}
}
#[inline]
pub fn from_request(req: &HttpRequest) -> Option<Self> {
let path = req
.path
.split_once('?')
.map(|(p, _)| p)
.unwrap_or(&req.path);
Some(Self {
method: HttpMethod::from_str(&req.method)?,
path: path.to_string(),
})
}
}
impl PartialEq for RouteKey {
fn eq(&self, other: &Self) -> bool {
self.method == other.method && self.path == other.path
}
}
impl Hash for RouteKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.method.as_str().hash(state);
self.path.hash(state);
}
}
#[derive(Clone)]
pub struct CachedRoute {
pub route_index: usize,
pub param_indices: Vec<(String, usize)>,
pub is_static: bool,
pub catch_all_index: Option<usize>,
}
impl CachedRoute {
pub fn static_route(route_index: usize) -> Self {
Self {
route_index,
param_indices: Vec::new(),
is_static: true,
catch_all_index: None,
}
}
pub fn with_params(route_index: usize, param_indices: Vec<(String, usize)>) -> Self {
Self {
route_index,
param_indices,
is_static: false,
catch_all_index: None,
}
}
pub fn from_compiled(route_index: usize, compiled: &CompiledRoute) -> Self {
Self {
route_index,
param_indices: compiled.param_indices.clone(),
is_static: compiled.is_static,
catch_all_index: compiled
.has_catch_all
.then(|| compiled.segments.len().saturating_sub(1)),
}
}
#[inline]
pub fn extract_params(&self, path: &str) -> HashMap<String, String> {
if self.is_static {
return HashMap::new();
}
let segments: SmallVec<[&str; INLINE_PATH_SEGMENTS]> =
path.split('/').filter(|s| !s.is_empty()).collect();
let mut params = HashMap::with_capacity(self.param_indices.len());
for (name, idx) in &self.param_indices {
if self.catch_all_index == Some(*idx) {
if let Some(rest) = segments.get(*idx..) {
params.insert(name.clone(), rest.join("/"));
}
} else if let Some(value) = segments.get(*idx) {
params.insert(name.clone(), (*value).to_string());
}
}
params
}
}
pub struct RouteCache {
cache: Mutex<LruCache<RouteKey, CachedRoute>>,
stats: RouteCacheStats,
}
impl RouteCache {
pub fn new() -> Self {
Self::with_capacity(1024)
}
pub fn with_capacity(max_size: usize) -> Self {
let capacity = NonZeroUsize::new(max_size).unwrap_or(NonZeroUsize::MIN);
Self {
cache: Mutex::new(LruCache::new(capacity)),
stats: RouteCacheStats::default(),
}
}
#[inline]
pub fn get(&self, key: &RouteKey) -> Option<CachedRoute> {
let mut cache = self.cache.lock();
let result = cache.get(key).cloned();
if result.is_some() {
self.stats.hits.fetch_add(1, Ordering::Relaxed);
} else {
self.stats.misses.fetch_add(1, Ordering::Relaxed);
}
result
}
pub fn insert(&self, key: RouteKey, route: CachedRoute) {
let mut cache = self.cache.lock();
let len_before = cache.len();
let was_present = cache.peek(&key).is_some();
cache.put(key, route);
if !was_present && len_before >= cache.cap().get() {
self.stats.evictions.fetch_add(1, Ordering::Relaxed);
}
self.stats.insertions.fetch_add(1, Ordering::Relaxed);
}
pub fn clear(&self) {
self.cache.lock().clear();
}
pub fn stats(&self) -> &RouteCacheStats {
&self.stats
}
pub fn len(&self) -> usize {
self.cache.lock().len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl Default for RouteCache {
fn default() -> Self {
Self::new()
}
}
pub struct StaticRoutes {
routes: HashMap<RouteKey, usize>,
stats: StaticRouteStats,
}
impl StaticRoutes {
pub fn new() -> Self {
Self {
routes: HashMap::new(),
stats: StaticRouteStats::default(),
}
}
pub fn add(&mut self, method: HttpMethod, path: impl Into<String>, route_index: usize) {
let key = RouteKey::new(method, path);
self.routes.insert(key, route_index);
}
#[inline]
pub fn get(&self, key: &RouteKey) -> Option<usize> {
let result = self.routes.get(key).copied();
if result.is_some() {
self.stats.hits.fetch_add(1, Ordering::Relaxed);
} else {
self.stats.misses.fetch_add(1, Ordering::Relaxed);
}
result
}
pub fn is_static_path(path: &str) -> bool {
!path.contains(':') && !path.contains('*')
}
pub fn stats(&self) -> &StaticRouteStats {
&self.stats
}
pub fn len(&self) -> usize {
self.routes.len()
}
pub fn is_empty(&self) -> bool {
self.routes.is_empty()
}
}
impl Default for StaticRoutes {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
pub struct CompiledRoute {
pub pattern: String,
pub segments: Vec<RouteSegment>,
pub param_indices: Vec<(String, usize)>,
pub is_static: bool,
pub has_catch_all: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RouteSegment {
Static(String),
Param(String),
CatchAll(String),
}
impl CompiledRoute {
pub fn compile(pattern: &str) -> Self {
let mut segments = Vec::new();
let mut param_indices = Vec::new();
let mut is_static = true;
let mut has_catch_all = false;
for (idx, part) in pattern.split('/').filter(|s| !s.is_empty()).enumerate() {
if let Some(name) = part.strip_prefix(':') {
segments.push(RouteSegment::Param(name.to_string()));
param_indices.push((name.to_string(), idx));
is_static = false;
} else if let Some(name) = part.strip_prefix('*') {
let name = if name.is_empty() { "*" } else { name };
segments.push(RouteSegment::CatchAll(name.to_string()));
param_indices.push((name.to_string(), idx));
is_static = false;
has_catch_all = true;
} else {
segments.push(RouteSegment::Static(part.to_string()));
}
}
Self {
pattern: pattern.to_string(),
segments,
param_indices,
is_static,
has_catch_all,
}
}
#[inline]
pub fn matches(&self, path: &str) -> bool {
let path_segments: SmallVec<[&str; INLINE_PATH_SEGMENTS]> =
path.split('/').filter(|s| !s.is_empty()).collect();
if !self.has_catch_all && path_segments.len() != self.segments.len() {
return false;
}
if self.has_catch_all && path_segments.len() < self.segments.len() - 1 {
return false;
}
for (idx, segment) in self.segments.iter().enumerate() {
match segment {
RouteSegment::Static(s) => {
if path_segments.get(idx) != Some(&s.as_str()) {
return false;
}
}
RouteSegment::Param(_) => {
if idx >= path_segments.len() {
return false;
}
}
RouteSegment::CatchAll(_) => {
break;
}
}
}
true
}
pub fn extract_params(&self, path: &str) -> HashMap<String, String> {
if self.is_static {
return HashMap::new();
}
let path_segments: SmallVec<[&str; INLINE_PATH_SEGMENTS]> =
path.split('/').filter(|s| !s.is_empty()).collect();
let mut params = HashMap::with_capacity(self.param_indices.len());
for (name, idx) in &self.param_indices {
if let Some(segment) = self.segments.get(*idx) {
match segment {
RouteSegment::Param(_) => {
if let Some(value) = path_segments.get(*idx) {
params.insert(name.clone(), (*value).to_string());
}
}
RouteSegment::CatchAll(_) => {
let remaining: String = path_segments[*idx..].join("/");
params.insert(name.clone(), remaining);
}
_ => {}
}
}
}
params
}
}
pub struct OptimizedRoute {
pub method: HttpMethod,
pub compiled: CompiledRoute,
pub handler: BoxedHandler,
pub constraints: Option<RouteConstraints>,
}
pub struct OptimizedRouter {
routes: Vec<OptimizedRoute>,
static_routes: StaticRoutes,
cache: RouteCache,
stats: RouterStats,
}
impl OptimizedRouter {
pub fn new() -> Self {
Self {
routes: Vec::new(),
static_routes: StaticRoutes::new(),
cache: RouteCache::new(),
stats: RouterStats::default(),
}
}
pub fn with_cache_size(cache_size: usize) -> Self {
Self {
routes: Vec::new(),
static_routes: StaticRoutes::new(),
cache: RouteCache::with_capacity(cache_size),
stats: RouterStats::default(),
}
}
pub fn add_route(
&mut self,
method: HttpMethod,
path: impl Into<String>,
handler: BoxedHandler,
) {
let path = path.into();
let compiled = CompiledRoute::compile(&path);
let route_index = self.routes.len();
if compiled.is_static {
self.static_routes.add(method.clone(), &path, route_index);
}
self.routes.push(OptimizedRoute {
method,
compiled,
handler,
constraints: None,
});
}
pub fn from_router(router: &Router) -> Self {
let mut opt = Self::new();
for route in &router.routes {
let compiled = CompiledRoute::compile(&route.path);
let route_index = opt.routes.len();
if compiled.is_static {
let shadowed_by_earlier = opt.routes.iter().any(|earlier| {
earlier.method == route.method && earlier.compiled.matches(&route.path)
});
if !shadowed_by_earlier {
opt.static_routes
.add(route.method.clone(), &route.path, route_index);
}
}
opt.routes.push(OptimizedRoute {
method: route.method.clone(),
compiled,
handler: route.handler.clone(),
constraints: route.constraints.clone(),
});
}
opt
}
pub async fn route(&self, mut request: HttpRequest) -> Result<HttpResponse, Error> {
self.stats.requests.fetch_add(1, Ordering::Relaxed);
let (path, query_string) = request
.path
.split_once('?')
.map(|(p, q)| (p, Some(q)))
.unwrap_or((&request.path, None));
if let Some(query) = query_string {
request.query_params = crate::simd_parser::parse_query_string_decoded(query);
}
let Some(method) = HttpMethod::from_str(&request.method) else {
return Err(Error::RouteNotFound(format!("{} {}", request.method, path)));
};
let key = RouteKey::new(method.clone(), path);
if let Some(route_index) = self.static_routes.get(&key) {
self.stats.static_hits.fetch_add(1, Ordering::Relaxed);
let route = &self.routes[route_index];
if let Some(constraints) = &route.constraints {
constraints.validate(&request.path_params)?;
}
return route.handler.call(request).await;
}
if let Some(cached) = self.cache.get(&key) {
self.stats.cache_hits.fetch_add(1, Ordering::Relaxed);
let route = &self.routes[cached.route_index];
let params = cached.extract_params(path);
if let Some(constraints) = &route.constraints {
constraints.validate(¶ms)?;
}
request.path_params = params;
return route.handler.call(request).await;
}
self.stats.pattern_matches.fetch_add(1, Ordering::Relaxed);
for (route_index, route) in self.routes.iter().enumerate() {
if route.method != method {
continue;
}
if route.compiled.matches(path) {
let cached = CachedRoute::from_compiled(route_index, &route.compiled);
self.cache.insert(key, cached);
let params = route.compiled.extract_params(path);
if let Some(constraints) = &route.constraints {
constraints.validate(¶ms)?;
}
request.path_params = params;
return route.handler.call(request).await;
}
}
Err(Error::RouteNotFound(format!("{} {}", request.method, path)))
}
pub fn stats(&self) -> &RouterStats {
&self.stats
}
pub fn cache_stats(&self) -> &RouteCacheStats {
self.cache.stats()
}
pub fn static_stats(&self) -> &StaticRouteStats {
self.static_routes.stats()
}
pub fn clear_cache(&self) {
self.cache.clear();
}
pub fn len(&self) -> usize {
self.routes.len()
}
pub fn is_empty(&self) -> bool {
self.routes.is_empty()
}
}
impl Default for OptimizedRouter {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Default)]
pub struct RouteCacheStats {
hits: AtomicU64,
misses: AtomicU64,
insertions: AtomicU64,
evictions: AtomicU64,
}
impl RouteCacheStats {
pub fn hits(&self) -> u64 {
self.hits.load(Ordering::Relaxed)
}
pub fn misses(&self) -> u64 {
self.misses.load(Ordering::Relaxed)
}
pub fn insertions(&self) -> u64 {
self.insertions.load(Ordering::Relaxed)
}
pub fn evictions(&self) -> u64 {
self.evictions.load(Ordering::Relaxed)
}
pub fn hit_ratio(&self) -> f64 {
let hits = self.hits() as f64;
let total = hits + self.misses() as f64;
if total > 0.0 { hits / total } else { 0.0 }
}
}
#[derive(Debug, Default)]
pub struct StaticRouteStats {
hits: AtomicU64,
misses: AtomicU64,
}
impl StaticRouteStats {
pub fn hits(&self) -> u64 {
self.hits.load(Ordering::Relaxed)
}
pub fn misses(&self) -> u64 {
self.misses.load(Ordering::Relaxed)
}
pub fn hit_ratio(&self) -> f64 {
let hits = self.hits() as f64;
let total = hits + self.misses() as f64;
if total > 0.0 { hits / total } else { 0.0 }
}
}
#[derive(Debug, Default)]
pub struct RouterStats {
requests: AtomicU64,
static_hits: AtomicU64,
cache_hits: AtomicU64,
pattern_matches: AtomicU64,
}
impl RouterStats {
pub fn requests(&self) -> u64 {
self.requests.load(Ordering::Relaxed)
}
pub fn static_hits(&self) -> u64 {
self.static_hits.load(Ordering::Relaxed)
}
pub fn cache_hits(&self) -> u64 {
self.cache_hits.load(Ordering::Relaxed)
}
pub fn pattern_matches(&self) -> u64 {
self.pattern_matches.load(Ordering::Relaxed)
}
pub fn optimization_ratio(&self) -> f64 {
let optimized = self.static_hits() + self.cache_hits();
let total = self.requests();
if total > 0 {
optimized as f64 / total as f64
} else {
0.0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_route_key_equality() {
let key1 = RouteKey::new(HttpMethod::GET, "/users");
let key2 = RouteKey::new(HttpMethod::GET, "/users");
let key3 = RouteKey::new(HttpMethod::POST, "/users");
assert_eq!(key1, key2);
assert_ne!(key1, key3);
}
#[test]
fn test_cached_route_static() {
let cached = CachedRoute::static_route(0);
assert!(cached.is_static);
let params = cached.extract_params("/users");
assert!(params.is_empty());
}
#[test]
fn test_cached_route_with_params() {
let cached = CachedRoute::with_params(0, vec![("id".to_string(), 1)]);
assert!(!cached.is_static);
let params = cached.extract_params("/users/123");
assert_eq!(params.get("id"), Some(&"123".to_string()));
}
#[test]
fn test_route_key_from_request_unknown_method() {
let req = HttpRequest::new("PROPFIND".to_string(), "/health".to_string());
assert!(RouteKey::from_request(&req).is_none());
let req = HttpRequest::new("GET".to_string(), "/health?x=1".to_string());
let key = RouteKey::from_request(&req).unwrap();
assert_eq!(key, RouteKey::new(HttpMethod::GET, "/health"));
}
#[test]
fn test_cached_route_catch_all_extracts_remaining_segments() {
let compiled = CompiledRoute::compile("/files/*path");
let cached = CachedRoute::from_compiled(0, &compiled);
assert_eq!(cached.catch_all_index, Some(1));
let params = cached.extract_params("/files/docs/readme.md");
assert_eq!(params.get("path"), Some(&"docs/readme.md".to_string()));
let params = cached.extract_params("/files/docs");
assert_eq!(params.get("path"), Some(&"docs".to_string()));
}
#[tokio::test]
async fn test_router_unknown_method_not_routed_to_get() {
let mut router = OptimizedRouter::new();
router.add_route(
HttpMethod::GET,
"/health",
crate::handler::handler(|_req: HttpRequest| async {
Ok::<_, Error>(HttpResponse::ok())
}),
);
let ok = router
.route(HttpRequest::new("GET".to_string(), "/health".to_string()))
.await;
assert!(ok.is_ok());
let err = router
.route(HttpRequest::new(
"PROPFIND".to_string(),
"/health".to_string(),
))
.await;
assert!(matches!(err, Err(Error::RouteNotFound(_))));
}
#[tokio::test]
async fn test_router_cached_catch_all_params() {
let mut router = OptimizedRouter::new();
router.add_route(
HttpMethod::GET,
"/files/*path",
crate::handler::handler(|req: HttpRequest| async move {
let path = req.path_params.get("path").cloned().unwrap_or_default();
let mut response = HttpResponse::ok();
response.body = path.into_bytes();
Ok::<_, Error>(response)
}),
);
let first = router
.route(HttpRequest::new(
"GET".to_string(),
"/files/docs/readme.md".to_string(),
))
.await
.unwrap();
assert_eq!(first.body, b"docs/readme.md");
let second = router
.route(HttpRequest::new(
"GET".to_string(),
"/files/docs/readme.md".to_string(),
))
.await
.unwrap();
assert_eq!(second.body, b"docs/readme.md");
assert!(router.stats().cache_hits() > 0);
}
#[tokio::test]
async fn test_router_decodes_query_params() {
let mut router = OptimizedRouter::new();
router.add_route(
HttpMethod::GET,
"/search",
crate::handler::handler(|req: HttpRequest| async move {
let q = req.query_params.get("q").cloned().unwrap_or_default();
let mut response = HttpResponse::ok();
response.body = q.into_bytes();
Ok::<_, Error>(response)
}),
);
let response = router
.route(HttpRequest::new(
"GET".to_string(),
"/search?q=hello%20world".to_string(),
))
.await
.unwrap();
assert_eq!(response.body, b"hello world");
}
#[test]
fn test_route_cache() {
let cache = RouteCache::new();
let key = RouteKey::new(HttpMethod::GET, "/users");
let route = CachedRoute::static_route(0);
assert!(cache.get(&key).is_none());
assert_eq!(cache.stats().misses(), 1);
cache.insert(key.clone(), route);
assert!(cache.get(&key).is_some());
assert_eq!(cache.stats().hits(), 1);
}
#[test]
fn test_static_routes() {
let mut static_routes = StaticRoutes::new();
static_routes.add(HttpMethod::GET, "/api/health", 0);
static_routes.add(HttpMethod::GET, "/api/users", 1);
let key = RouteKey::new(HttpMethod::GET, "/api/health");
assert_eq!(static_routes.get(&key), Some(0));
let key = RouteKey::new(HttpMethod::GET, "/api/users");
assert_eq!(static_routes.get(&key), Some(1));
let key = RouteKey::new(HttpMethod::GET, "/api/missing");
assert_eq!(static_routes.get(&key), None);
}
#[test]
fn test_is_static_path() {
assert!(StaticRoutes::is_static_path("/api/health"));
assert!(StaticRoutes::is_static_path("/users"));
assert!(!StaticRoutes::is_static_path("/users/:id"));
assert!(!StaticRoutes::is_static_path("/files/*path"));
}
#[test]
fn test_compiled_route_static() {
let compiled = CompiledRoute::compile("/api/health");
assert!(compiled.is_static);
assert!(compiled.param_indices.is_empty());
assert!(compiled.matches("/api/health"));
assert!(!compiled.matches("/api/users"));
}
#[test]
fn test_compiled_route_with_param() {
let compiled = CompiledRoute::compile("/users/:id");
assert!(!compiled.is_static);
assert_eq!(compiled.param_indices.len(), 1);
assert!(compiled.matches("/users/123"));
assert!(compiled.matches("/users/abc"));
assert!(!compiled.matches("/users"));
assert!(!compiled.matches("/users/123/extra"));
let params = compiled.extract_params("/users/123");
assert_eq!(params.get("id"), Some(&"123".to_string()));
}
#[test]
fn test_compiled_route_multiple_params() {
let compiled = CompiledRoute::compile("/users/:user_id/posts/:post_id");
assert!(!compiled.is_static);
assert_eq!(compiled.param_indices.len(), 2);
assert!(compiled.matches("/users/123/posts/456"));
let params = compiled.extract_params("/users/123/posts/456");
assert_eq!(params.get("user_id"), Some(&"123".to_string()));
assert_eq!(params.get("post_id"), Some(&"456".to_string()));
}
#[test]
fn test_compiled_route_catch_all() {
let compiled = CompiledRoute::compile("/files/*path");
assert!(!compiled.is_static);
assert!(compiled.has_catch_all);
assert!(compiled.matches("/files/docs"));
assert!(compiled.matches("/files/docs/readme.md"));
let params = compiled.extract_params("/files/docs/readme.md");
assert_eq!(params.get("path"), Some(&"docs/readme.md".to_string()));
}
#[test]
fn test_router_stats() {
let stats = RouterStats::default();
stats.requests.fetch_add(100, Ordering::Relaxed);
stats.static_hits.fetch_add(50, Ordering::Relaxed);
stats.cache_hits.fetch_add(30, Ordering::Relaxed);
stats.pattern_matches.fetch_add(20, Ordering::Relaxed);
assert_eq!(stats.requests(), 100);
assert_eq!(stats.static_hits(), 50);
assert_eq!(stats.cache_hits(), 30);
assert_eq!(stats.pattern_matches(), 20);
assert!((stats.optimization_ratio() - 0.8).abs() < 0.001);
}
#[tokio::test]
async fn test_from_router_dispatches_static_and_param() {
let mut router = crate::routing::Router::new();
router.add_route(crate::routing::Route::new(
HttpMethod::GET,
"/health",
|_req: HttpRequest| async { Ok::<_, Error>(HttpResponse::ok()) },
));
router.add_route(crate::routing::Route::new(
HttpMethod::GET,
"/users/:id",
|req: HttpRequest| async move {
let id = req.path_params.get("id").cloned().unwrap_or_default();
let mut r = HttpResponse::ok();
r.body = id.into_bytes();
Ok::<_, Error>(r)
},
));
let opt = OptimizedRouter::from_router(&router);
let resp = opt
.route(HttpRequest::new("GET".into(), "/health".into()))
.await
.unwrap();
assert_eq!(resp.status, 200);
assert!(opt.stats().static_hits() >= 1);
let resp = opt
.route(HttpRequest::new("GET".into(), "/users/42".into()))
.await
.unwrap();
assert_eq!(resp.body, b"42");
}
#[tokio::test]
async fn test_from_router_catch_all() {
let mut router = crate::routing::Router::new();
router.add_route(crate::routing::Route::new(
HttpMethod::GET,
"/files/*path",
|req: HttpRequest| async move {
let p = req.path_params.get("path").cloned().unwrap_or_default();
let mut r = HttpResponse::ok();
r.body = p.into_bytes();
Ok::<_, Error>(r)
},
));
let opt = OptimizedRouter::from_router(&router);
let resp = opt
.route(HttpRequest::new(
"GET".into(),
"/files/docs/readme.md".into(),
))
.await
.unwrap();
assert_eq!(resp.body, b"docs/readme.md");
}
#[tokio::test]
async fn test_from_router_validates_constraints() {
let constraints =
RouteConstraints::new().add("id", Box::new(crate::route_constraint::UIntConstraint));
let mut router = crate::routing::Router::new();
router.add_route(
crate::routing::Route::new(HttpMethod::GET, "/users/:id", |_req: HttpRequest| async {
Ok::<_, Error>(HttpResponse::ok())
})
.with_constraints(constraints),
);
let opt = OptimizedRouter::from_router(&router);
let ok = opt
.route(HttpRequest::new("GET".into(), "/users/123".into()))
.await;
assert!(ok.is_ok());
let err = opt
.route(HttpRequest::new("GET".into(), "/users/abc".into()))
.await;
assert!(matches!(err, Err(Error::BadRequest(_))));
let err_again = opt
.route(HttpRequest::new("GET".into(), "/users/abc".into()))
.await;
assert!(matches!(err_again, Err(Error::BadRequest(_))));
}
#[tokio::test]
async fn test_from_router_unknown_method_not_get() {
let mut router = crate::routing::Router::new();
router.add_route(crate::routing::Route::new(
HttpMethod::GET,
"/health",
|_req: HttpRequest| async { Ok::<_, Error>(HttpResponse::ok()) },
));
let opt = OptimizedRouter::from_router(&router);
let err = opt
.route(HttpRequest::new("PROPFIND".into(), "/health".into()))
.await;
assert!(matches!(err, Err(Error::RouteNotFound(_))));
}
#[tokio::test]
async fn test_from_router_query_method_routing() {
let mut router = crate::routing::Router::new();
router.add_route(crate::routing::Route::new(
HttpMethod::QUERY,
"/search",
|req: HttpRequest| async move { Ok::<_, Error>(HttpResponse::ok().with_body(req.body)) },
));
let opt = OptimizedRouter::from_router(&router);
let mut req = HttpRequest::new("QUERY".into(), "/search".into());
req.body = b"name=john".to_vec();
let resp = opt.route(req).await.unwrap();
assert_eq!(resp.into_body_bytes().as_ref(), b"name=john");
let err = opt
.route(HttpRequest::new("GET".into(), "/search".into()))
.await;
assert!(matches!(err, Err(Error::RouteNotFound(_))));
}
#[tokio::test]
async fn test_from_router_preserves_registration_order_precedence() {
let mut router = crate::routing::Router::new();
router.add_route(crate::routing::Route::new(
HttpMethod::GET,
"/users/:id",
|req: HttpRequest| async move {
let id = req.path_params.get("id").cloned().unwrap_or_default();
Ok::<_, Error>(HttpResponse::ok().with_body(format!("param:{id}").into_bytes()))
},
));
router.add_route(crate::routing::Route::new(
HttpMethod::GET,
"/users/me",
|_req: HttpRequest| async {
Ok::<_, Error>(HttpResponse::ok().with_body(b"static".to_vec()))
},
));
let linear = router
.clone()
.route(HttpRequest::new("GET".into(), "/users/me".into()))
.await
.unwrap();
let opt = OptimizedRouter::from_router(&router);
let optimized = opt
.route(HttpRequest::new("GET".into(), "/users/me".into()))
.await
.unwrap();
assert_eq!(optimized.body, linear.body);
assert_eq!(optimized.body, b"param:me");
}
#[tokio::test]
async fn test_from_router_decodes_query_params() {
let mut router = crate::routing::Router::new();
router.add_route(crate::routing::Route::new(
HttpMethod::GET,
"/search",
|req: HttpRequest| async move {
let q = req.query_params.get("q").cloned().unwrap_or_default();
Ok::<_, Error>(HttpResponse::ok().with_body(q.into_bytes()))
},
));
let opt = OptimizedRouter::from_router(&router);
let resp = opt
.route(HttpRequest::new(
"GET".into(),
"/search?q=hello%20world".into(),
))
.await
.unwrap();
assert_eq!(resp.body, b"hello world");
}
#[test]
fn test_from_router_skips_shadowed_static_fast_path() {
let mut router = crate::routing::Router::new();
router.add_route(crate::routing::Route::new(
HttpMethod::GET,
"/users/:id",
|_req: HttpRequest| async { Ok::<_, Error>(HttpResponse::ok()) },
));
router.add_route(crate::routing::Route::new(
HttpMethod::GET,
"/users/me",
|_req: HttpRequest| async { Ok::<_, Error>(HttpResponse::ok()) },
));
let opt = OptimizedRouter::from_router(&router);
assert!(
opt.static_routes
.get(&RouteKey::new(HttpMethod::GET, "/users/me"))
.is_none()
);
let mut router2 = crate::routing::Router::new();
router2.add_route(crate::routing::Route::new(
HttpMethod::GET,
"/health",
|_req: HttpRequest| async { Ok::<_, Error>(HttpResponse::ok()) },
));
let opt2 = OptimizedRouter::from_router(&router2);
assert!(
opt2.static_routes
.get(&RouteKey::new(HttpMethod::GET, "/health"))
.is_some()
);
}
#[test]
fn test_route_cache_eviction() {
let cache = RouteCache::with_capacity(10);
for i in 0..15 {
let key = RouteKey::new(HttpMethod::GET, format!("/route/{}", i));
cache.insert(key, CachedRoute::static_route(i));
}
assert!(cache.len() <= 10);
assert!(cache.stats().evictions() > 0);
}
#[test]
fn test_route_cache_lru_eviction_respects_recency() {
let cache = RouteCache::with_capacity(3);
let key0 = RouteKey::new(HttpMethod::GET, "/route/0");
let key1 = RouteKey::new(HttpMethod::GET, "/route/1");
let key2 = RouteKey::new(HttpMethod::GET, "/route/2");
let key3 = RouteKey::new(HttpMethod::GET, "/route/3");
cache.insert(key0.clone(), CachedRoute::static_route(0));
cache.insert(key1.clone(), CachedRoute::static_route(1));
cache.insert(key2.clone(), CachedRoute::static_route(2));
assert!(cache.get(&key0).is_some());
cache.insert(key3.clone(), CachedRoute::static_route(3));
assert!(
cache.get(&key0).is_some(),
"recently-accessed entry must survive eviction"
);
assert!(
cache.get(&key1).is_none(),
"genuinely-unaccessed entry must be evicted"
);
assert!(cache.get(&key2).is_some());
assert!(cache.get(&key3).is_some());
assert_eq!(cache.len(), 3);
}
}