use super::pattern::{CompiledRoute, RouteId, RouteMatch, RoutePattern, RoutePatternError};
use super::HttpMethod;
use std::collections::HashMap;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum RouteMatchError {
#[error("No matching route found")]
NoMatch,
#[error("Route pattern error: {0}")]
PatternError(#[from] RoutePatternError),
#[error("Conflicting routes: {0} conflicts with {1}")]
RouteConflict(String, String),
}
#[derive(Debug, Clone)]
pub struct RouteDefinition {
pub id: RouteId,
pub method: HttpMethod,
pub path: String,
}
#[derive(Debug)]
pub struct RouteMatcher {
static_routes: HashMap<HttpMethod, HashMap<String, RouteId>>,
dynamic_routes: Vec<CompiledRoute>,
route_definitions: HashMap<RouteId, RouteDefinition>,
}
impl RouteMatcher {
pub fn new() -> Self {
Self {
static_routes: HashMap::new(),
dynamic_routes: Vec::new(),
route_definitions: HashMap::new(),
}
}
pub fn add_route(&mut self, definition: RouteDefinition) -> Result<(), RouteMatchError> {
let pattern = RoutePattern::parse(&definition.path)?;
self.check_conflicts(&definition, &pattern)?;
self.route_definitions
.insert(definition.id.clone(), definition.clone());
if pattern.is_static() {
self.static_routes
.entry(definition.method.clone())
.or_default()
.insert(definition.path.clone(), definition.id);
} else {
let compiled_route = CompiledRoute::new(definition.id, definition.method, pattern);
let insert_pos = self
.dynamic_routes
.binary_search_by_key(&compiled_route.priority, |r| r.priority)
.unwrap_or_else(|pos| pos);
self.dynamic_routes.insert(insert_pos, compiled_route);
}
Ok(())
}
pub fn resolve(&self, method: &HttpMethod, path: &str) -> Option<RouteMatch> {
if let Some(method_routes) = self.static_routes.get(method) {
if let Some(route_id) = method_routes.get(path) {
return Some(RouteMatch {
route_id: route_id.clone(),
params: HashMap::new(),
});
}
}
for compiled_route in &self.dynamic_routes {
if compiled_route.matches(method, path) {
let params = compiled_route.extract_params(path);
return Some(RouteMatch {
route_id: compiled_route.id.clone(),
params,
});
}
}
None
}
fn check_conflicts(
&self,
new_route: &RouteDefinition,
new_pattern: &RoutePattern,
) -> Result<(), RouteMatchError> {
if new_pattern.is_static() {
if let Some(method_routes) = self.static_routes.get(&new_route.method) {
if let Some(existing_id) = method_routes.get(&new_route.path) {
return Err(RouteMatchError::RouteConflict(
new_route.id.clone(),
existing_id.clone(),
));
}
}
}
for existing_route in &self.dynamic_routes {
if existing_route.method == new_route.method
&& self.patterns_conflict(new_pattern, &existing_route.pattern)
{
return Err(RouteMatchError::RouteConflict(
new_route.id.clone(),
existing_route.id.clone(),
));
}
}
Ok(())
}
fn patterns_conflict(&self, pattern1: &RoutePattern, pattern2: &RoutePattern) -> bool {
if pattern1.segments.len() != pattern2.segments.len() {
return false;
}
for (seg1, seg2) in pattern1.segments.iter().zip(pattern2.segments.iter()) {
match (seg1, seg2) {
(
super::pattern::PathSegment::Static(s1),
super::pattern::PathSegment::Static(s2),
) if s1 == s2 => continue,
(
super::pattern::PathSegment::Parameter { constraint: c1, .. },
super::pattern::PathSegment::Parameter { constraint: c2, .. },
) if c1 == c2 => continue,
(
super::pattern::PathSegment::CatchAll { .. },
super::pattern::PathSegment::CatchAll { .. },
) => continue,
_ => return false, }
}
true }
pub fn all_routes(&self) -> &HashMap<RouteId, RouteDefinition> {
&self.route_definitions
}
pub fn get_route(&self, route_id: &RouteId) -> Option<&RouteDefinition> {
self.route_definitions.get(route_id)
}
pub fn stats(&self) -> MatcherStats {
let static_routes_count = self
.static_routes
.values()
.map(|method_routes| method_routes.len())
.sum();
MatcherStats {
static_routes: static_routes_count,
dynamic_routes: self.dynamic_routes.len(),
total_routes: self.route_definitions.len(),
}
}
pub fn clear(&mut self) {
self.static_routes.clear();
self.dynamic_routes.clear();
self.route_definitions.clear();
}
}
impl Default for RouteMatcher {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct MatcherStats {
pub static_routes: usize,
pub dynamic_routes: usize,
pub total_routes: usize,
}
pub struct RouteMatcherBuilder {
routes: Vec<RouteDefinition>,
}
impl RouteMatcherBuilder {
pub fn new() -> Self {
Self { routes: Vec::new() }
}
pub fn route(mut self, id: String, method: HttpMethod, path: String) -> Self {
self.routes.push(RouteDefinition { id, method, path });
self
}
pub fn get(self, id: String, path: String) -> Self {
self.route(id, HttpMethod::GET, path)
}
pub fn post(self, id: String, path: String) -> Self {
self.route(id, HttpMethod::POST, path)
}
pub fn put(self, id: String, path: String) -> Self {
self.route(id, HttpMethod::PUT, path)
}
pub fn delete(self, id: String, path: String) -> Self {
self.route(id, HttpMethod::DELETE, path)
}
pub fn patch(self, id: String, path: String) -> Self {
self.route(id, HttpMethod::PATCH, path)
}
pub fn build(self) -> Result<RouteMatcher, RouteMatchError> {
let mut matcher = RouteMatcher::new();
for route_def in self.routes {
matcher.add_route(route_def)?;
}
Ok(matcher)
}
}
impl Default for RouteMatcherBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_static_route_matching() {
let mut matcher = RouteMatcher::new();
let route_def = RouteDefinition {
id: "home".to_string(),
method: HttpMethod::GET,
path: "/".to_string(),
};
matcher.add_route(route_def).unwrap();
let result = matcher.resolve(&HttpMethod::GET, "/");
assert!(result.is_some());
let route_match = result.unwrap();
assert_eq!(route_match.route_id, "home");
assert!(route_match.params.is_empty());
assert!(matcher.resolve(&HttpMethod::POST, "/").is_none());
assert!(matcher.resolve(&HttpMethod::GET, "/users").is_none());
}
#[test]
fn test_dynamic_route_matching() {
let mut matcher = RouteMatcher::new();
let route_def = RouteDefinition {
id: "user_show".to_string(),
method: HttpMethod::GET,
path: "/users/{id}".to_string(),
};
matcher.add_route(route_def).unwrap();
let result = matcher.resolve(&HttpMethod::GET, "/users/123");
assert!(result.is_some());
let route_match = result.unwrap();
assert_eq!(route_match.route_id, "user_show");
assert_eq!(route_match.params.get("id"), Some(&"123".to_string()));
}
#[test]
fn test_route_priority() {
let mut matcher = RouteMatcher::new();
matcher
.add_route(RouteDefinition {
id: "catch_all".to_string(),
method: HttpMethod::GET,
path: "/files/*path".to_string(),
})
.unwrap();
matcher
.add_route(RouteDefinition {
id: "specific".to_string(),
method: HttpMethod::GET,
path: "/files/config.json".to_string(),
})
.unwrap();
matcher
.add_route(RouteDefinition {
id: "param".to_string(),
method: HttpMethod::GET,
path: "/files/{name}".to_string(),
})
.unwrap();
let result = matcher.resolve(&HttpMethod::GET, "/files/config.json");
assert_eq!(result.unwrap().route_id, "specific");
let result = matcher.resolve(&HttpMethod::GET, "/files/other.txt");
assert_eq!(result.unwrap().route_id, "param");
let result = matcher.resolve(&HttpMethod::GET, "/files/docs/readme.md");
assert_eq!(result.unwrap().route_id, "catch_all");
}
#[test]
fn test_route_conflict_detection() {
let mut matcher = RouteMatcher::new();
matcher
.add_route(RouteDefinition {
id: "route1".to_string(),
method: HttpMethod::GET,
path: "/users".to_string(),
})
.unwrap();
let result = matcher.add_route(RouteDefinition {
id: "route2".to_string(),
method: HttpMethod::GET,
path: "/users".to_string(),
});
assert!(result.is_err());
assert!(matches!(result, Err(RouteMatchError::RouteConflict(_, _))));
}
#[test]
fn test_advanced_conflict_detection() {
let mut matcher = RouteMatcher::new();
matcher
.add_route(RouteDefinition {
id: "users_by_id".to_string(),
method: HttpMethod::GET,
path: "/users/{id}".to_string(),
})
.unwrap();
let result = matcher.add_route(RouteDefinition {
id: "users_by_name".to_string(),
method: HttpMethod::GET,
path: "/users/{name}".to_string(),
});
assert!(
result.is_err(),
"Parameters with different names should conflict"
);
let result = matcher.add_route(RouteDefinition {
id: "users_post".to_string(),
method: HttpMethod::POST,
path: "/users/{id}".to_string(),
});
assert!(result.is_ok(), "Different methods should not conflict");
let result = matcher.add_route(RouteDefinition {
id: "posts_by_id".to_string(),
method: HttpMethod::GET,
path: "/posts/{id}".to_string(),
});
assert!(
result.is_ok(),
"Different static segments should not conflict"
);
matcher
.add_route(RouteDefinition {
id: "files_serve".to_string(),
method: HttpMethod::GET,
path: "/files/*path".to_string(),
})
.unwrap();
let result = matcher.add_route(RouteDefinition {
id: "files_download".to_string(),
method: HttpMethod::GET,
path: "/files/*file_path".to_string(),
});
assert!(
result.is_err(),
"Catch-all routes with same structure should conflict"
);
let result = matcher.add_route(RouteDefinition {
id: "admin_static".to_string(),
method: HttpMethod::GET,
path: "/admin/dashboard".to_string(),
});
assert!(
result.is_ok(),
"Static vs parameter segments should not conflict"
);
}
#[test]
fn test_constraint_based_conflicts() {
let mut matcher = RouteMatcher::new();
matcher
.add_route(RouteDefinition {
id: "user_by_int_id".to_string(),
method: HttpMethod::GET,
path: "/users/{id:int}".to_string(),
})
.unwrap();
let result = matcher.add_route(RouteDefinition {
id: "user_by_int_uid".to_string(),
method: HttpMethod::GET,
path: "/users/{uid:int}".to_string(),
});
assert!(result.is_err(), "Same constraints should conflict");
let result = matcher.add_route(RouteDefinition {
id: "user_by_uuid".to_string(),
method: HttpMethod::GET,
path: "/users/{id:uuid}".to_string(),
});
assert!(result.is_ok(), "Different constraints should not conflict");
let result = matcher.add_route(RouteDefinition {
id: "user_by_string".to_string(),
method: HttpMethod::GET,
path: "/users/{name}".to_string(),
});
assert!(
result.is_ok(),
"Constrained vs unconstrained should not conflict"
);
}
#[test]
fn test_complex_pattern_conflicts() {
let mut matcher = RouteMatcher::new();
matcher
.add_route(RouteDefinition {
id: "api_user_posts".to_string(),
method: HttpMethod::GET,
path: "/api/v1/users/{user_id}/posts/{post_id}".to_string(),
})
.unwrap();
let result = matcher.add_route(RouteDefinition {
id: "api_member_articles".to_string(),
method: HttpMethod::GET,
path: "/api/v1/users/{member_id}/posts/{article_id}".to_string(),
});
assert!(
result.is_err(),
"Structurally identical complex patterns should conflict"
);
let result = matcher.add_route(RouteDefinition {
id: "api_user_comments".to_string(),
method: HttpMethod::GET,
path: "/api/v1/users/{user_id}/comments/{comment_id}".to_string(),
});
assert!(
result.is_ok(),
"Different static segments should not conflict"
);
let result = matcher.add_route(RouteDefinition {
id: "api_user_profile".to_string(),
method: HttpMethod::GET,
path: "/api/v1/users/{user_id}/profile".to_string(),
});
assert!(
result.is_ok(),
"Different segment count should not conflict"
);
}
#[test]
fn test_matcher_builder() {
let matcher = RouteMatcherBuilder::new()
.get("home".to_string(), "/".to_string())
.post("users_create".to_string(), "/users".to_string())
.get("users_show".to_string(), "/users/{id}".to_string())
.build()
.unwrap();
let stats = matcher.stats();
assert_eq!(stats.total_routes, 3);
assert_eq!(stats.static_routes, 2); assert_eq!(stats.dynamic_routes, 1);
assert!(matcher.resolve(&HttpMethod::GET, "/").is_some());
assert!(matcher.resolve(&HttpMethod::POST, "/users").is_some());
assert!(matcher.resolve(&HttpMethod::GET, "/users/123").is_some());
}
#[test]
fn test_constraint_validation_in_matching() {
let mut matcher = RouteMatcher::new();
matcher
.add_route(RouteDefinition {
id: "user_by_id".to_string(),
method: HttpMethod::GET,
path: "/users/{id:int}".to_string(),
})
.unwrap();
let result = matcher.resolve(&HttpMethod::GET, "/users/123");
assert!(result.is_some());
assert_eq!(result.unwrap().route_id, "user_by_id");
let result = matcher.resolve(&HttpMethod::GET, "/users/abc");
assert!(result.is_none());
}
#[test]
fn test_mixed_static_and_dynamic_routes() {
let mut matcher = RouteMatcher::new();
matcher
.add_route(RouteDefinition {
id: "api_status".to_string(),
method: HttpMethod::GET,
path: "/api/status".to_string(),
})
.unwrap();
matcher
.add_route(RouteDefinition {
id: "api_user".to_string(),
method: HttpMethod::GET,
path: "/api/users/{id}".to_string(),
})
.unwrap();
matcher
.add_route(RouteDefinition {
id: "root".to_string(),
method: HttpMethod::GET,
path: "/".to_string(),
})
.unwrap();
let result = matcher.resolve(&HttpMethod::GET, "/api/status");
assert_eq!(result.unwrap().route_id, "api_status");
let result = matcher.resolve(&HttpMethod::GET, "/");
assert_eq!(result.unwrap().route_id, "root");
let result = matcher.resolve(&HttpMethod::GET, "/api/users/456");
let route_match = result.unwrap();
assert_eq!(route_match.route_id, "api_user");
assert_eq!(route_match.params.get("id"), Some(&"456".to_string()));
}
#[test]
fn test_no_match() {
let matcher = RouteMatcherBuilder::new()
.get("home".to_string(), "/".to_string())
.build()
.unwrap();
assert!(matcher.resolve(&HttpMethod::GET, "/nonexistent").is_none());
assert!(matcher.resolve(&HttpMethod::POST, "/").is_none());
}
#[test]
fn test_static_route_lookup_performance() {
let mut builder = RouteMatcherBuilder::new();
for i in 0..100 {
builder = builder
.get(format!("get_{}", i), format!("/static/path/{}", i))
.post(format!("post_{}", i), format!("/api/v1/{}", i))
.put(format!("put_{}", i), format!("/resource/{}", i));
}
let matcher = builder.build().unwrap();
let stats = matcher.stats();
assert_eq!(stats.static_routes, 300); assert_eq!(stats.dynamic_routes, 0);
let start = std::time::Instant::now();
for i in 0..1000 {
let test_index = i % 100;
let result = matcher.resolve(&HttpMethod::GET, &format!("/static/path/{}", test_index));
assert!(result.is_some());
let result = matcher.resolve(&HttpMethod::POST, &format!("/api/v1/{}", test_index));
assert!(result.is_some());
let result = matcher.resolve(&HttpMethod::PUT, &format!("/resource/{}", test_index));
assert!(result.is_some());
let result = matcher.resolve(&HttpMethod::GET, "/nonexistent/path");
assert!(result.is_none());
}
let elapsed = start.elapsed();
assert!(
elapsed.as_millis() < 100,
"Static route lookups took too long: {}ms",
elapsed.as_millis()
);
println!(
"3000 static route lookups completed in {}μs",
elapsed.as_micros()
);
}
}