use crate::handler::{BoxedHandler, IntoHandler};
use crate::logging::{debug, trace};
use crate::route_constraint::RouteConstraints;
use crate::{Error, HttpMethod, HttpRequest, HttpResponse};
use smallvec::SmallVec;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
pub type HandlerFn = Arc<
dyn Fn(HttpRequest) -> Pin<Box<dyn Future<Output = Result<HttpResponse, Error>> + Send>>
+ Send
+ Sync,
>;
pub type OptimizedHandler = BoxedHandler;
#[derive(Clone)]
pub struct Route {
pub method: HttpMethod,
pub path: String,
pub handler: BoxedHandler,
pub constraints: Option<RouteConstraints>,
}
impl Route {
#[inline]
pub fn new<H, Args>(method: HttpMethod, path: impl Into<String>, handler: H) -> Self
where
H: IntoHandler<Args>,
{
Self {
method,
path: path.into(),
handler: BoxedHandler::new(handler.into_handler()),
constraints: None,
}
}
#[inline]
pub fn from_legacy(method: HttpMethod, path: impl Into<String>, handler: HandlerFn) -> Self {
Self {
method,
path: path.into(),
handler: crate::handler::from_legacy_handler(handler),
constraints: None,
}
}
#[inline]
pub fn with_constraints(mut self, constraints: RouteConstraints) -> Self {
self.constraints = Some(constraints);
self
}
}
#[derive(Clone)]
pub struct Router {
pub routes: Vec<Route>,
}
impl Router {
#[inline]
pub fn new() -> Self {
Self { routes: Vec::new() }
}
#[inline]
pub fn add_route(&mut self, route: Route) {
self.routes.push(route);
}
#[inline]
pub fn get<H, Args>(&mut self, path: impl Into<String>, handler: H) -> &mut Self
where
H: IntoHandler<Args>,
{
self.routes.push(Route::new(HttpMethod::GET, path, handler));
self
}
#[inline]
pub fn post<H, Args>(&mut self, path: impl Into<String>, handler: H) -> &mut Self
where
H: IntoHandler<Args>,
{
self.routes
.push(Route::new(HttpMethod::POST, path, handler));
self
}
#[inline]
pub fn put<H, Args>(&mut self, path: impl Into<String>, handler: H) -> &mut Self
where
H: IntoHandler<Args>,
{
self.routes.push(Route::new(HttpMethod::PUT, path, handler));
self
}
#[inline]
pub fn delete<H, Args>(&mut self, path: impl Into<String>, handler: H) -> &mut Self
where
H: IntoHandler<Args>,
{
self.routes
.push(Route::new(HttpMethod::DELETE, path, handler));
self
}
#[inline]
pub fn patch<H, Args>(&mut self, path: impl Into<String>, handler: H) -> &mut Self
where
H: IntoHandler<Args>,
{
self.routes
.push(Route::new(HttpMethod::PATCH, path, handler));
self
}
#[inline]
pub fn options<H, Args>(&mut self, path: impl Into<String>, handler: H) -> &mut Self
where
H: IntoHandler<Args>,
{
self.routes
.push(Route::new(HttpMethod::OPTIONS, path, handler));
self
}
#[inline]
pub fn head<H, Args>(&mut self, path: impl Into<String>, handler: H) -> &mut Self
where
H: IntoHandler<Args>,
{
self.routes
.push(Route::new(HttpMethod::HEAD, path, handler));
self
}
#[inline]
pub fn query<H, Args>(&mut self, path: impl Into<String>, handler: H) -> &mut Self
where
H: IntoHandler<Args>,
{
self.routes
.push(Route::new(HttpMethod::QUERY, path, handler));
self
}
#[inline]
pub fn match_route(
&self,
method: &str,
path: &str,
) -> Option<(BoxedHandler, HashMap<String, String>)> {
let path = path.split('?').next().unwrap_or(path);
let path_parts: SmallVec<[&str; 8]> = split_segments(path).collect();
for route in &self.routes {
if route.method.as_str() != method {
continue;
}
if let Some(params) = match_path(&route.path, &path_parts) {
return Some((route.handler.clone(), params));
}
}
None
}
#[inline]
pub async fn route(&self, mut request: HttpRequest) -> Result<HttpResponse, Error> {
debug!("Routing request: {} {}", request.method, request.path);
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 {
trace!("Parsing query string: {}", query);
request.query_params = parse_query_string(query);
}
let matched: Option<(usize, HashMap<String, String>)> = {
let path_parts: SmallVec<[&str; 8]> = split_segments(path).collect();
let mut found = None;
for (idx, route) in self.routes.iter().enumerate() {
if route.method.as_str() != request.method {
continue;
}
if let Some(params) = match_path(&route.path, &path_parts) {
debug!(
"Route matched: {} {} -> {}",
request.method, path, route.path
);
found = Some((idx, params));
break;
}
}
found
};
if let Some((idx, params)) = matched {
let route = &self.routes[idx];
if let Some(constraints) = &route.constraints {
trace!("Validating route constraints");
constraints.validate(¶ms)?;
}
request.path_params = params;
trace!("Dispatching handler");
return route.handler.call(request).await;
}
debug!("No route found for {} {}", request.method, path);
Err(Error::RouteNotFound(format!("{} {}", request.method, path)))
}
}
impl Default for Router {
fn default() -> Self {
Self::new()
}
}
#[inline]
fn split_segments(path: &str) -> impl Iterator<Item = &str> {
path.split('/').filter(|s| !s.is_empty())
}
fn match_path(pattern: &str, path_parts: &[&str]) -> Option<HashMap<String, String>> {
let mut seen = 0usize;
let mut param_count = 0usize;
let mut catch_all_at: Option<usize> = None;
for (i, pattern_part) in split_segments(pattern).enumerate() {
if pattern_part.starts_with('*') {
catch_all_at = Some(i);
param_count += 1;
break;
}
let path_part = path_parts.get(i)?;
if pattern_part.starts_with(':') {
param_count += 1;
} else if pattern_part != *path_part {
return None;
}
seen = i + 1;
}
if let Some(idx) = catch_all_at {
debug_assert!(
path_parts.len() >= idx,
"catch-all index validated during first pass"
);
} else if seen != path_parts.len() {
return None;
}
let mut params = HashMap::with_capacity(param_count);
if param_count > 0 {
for (i, pattern_part) in split_segments(pattern).enumerate() {
if let Some(name) = pattern_part.strip_prefix('*') {
let name = if name.is_empty() { "*" } else { name };
params.insert(name.to_string(), path_parts[i..].join("/"));
break;
} else if let Some(param_name) = pattern_part.strip_prefix(':') {
params.insert(param_name.to_string(), path_parts[i].to_string());
}
}
}
Some(params)
}
#[inline]
fn parse_query_string(query: &str) -> HashMap<String, String> {
crate::simd_parser::parse_query_string_decoded(query)
}
#[cfg(test)]
mod tests {
use super::*;
async fn test_handler(_req: HttpRequest) -> Result<HttpResponse, Error> {
Ok(HttpResponse::ok())
}
fn match_path_str(pattern: &str, path: &str) -> Option<HashMap<String, String>> {
let parts: Vec<&str> = super::split_segments(path).collect();
match_path(pattern, &parts)
}
#[test]
fn test_match_path_static() {
let pattern = "/users";
let path = "/users";
let result = match_path_str(pattern, path);
assert!(result.is_some());
assert_eq!(result.unwrap().len(), 0);
}
#[test]
fn test_match_path_with_param() {
let pattern = "/users/:id";
let path = "/users/123";
let result = match_path_str(pattern, path);
assert!(result.is_some());
let params = result.unwrap();
assert_eq!(params.get("id"), Some(&"123".to_string()));
}
#[test]
fn test_match_path_no_match() {
let pattern = "/users/:id";
let path = "/posts/123";
let result = match_path_str(pattern, path);
assert!(result.is_none());
}
#[test]
fn test_parse_query_string() {
let query = "name=john&age=30";
let params = parse_query_string(query);
assert_eq!(params.get("name"), Some(&"john".to_string()));
assert_eq!(params.get("age"), Some(&"30".to_string()));
}
#[test]
fn test_parse_query_string_decodes_values() {
let query = "name=john%20doe&x=a%26b";
let params = parse_query_string(query);
assert_eq!(params.get("name"), Some(&"john doe".to_string()));
assert_eq!(params.get("x"), Some(&"a&b".to_string()));
}
#[test]
fn test_query_method_round_trip() {
assert_eq!(HttpMethod::from_str("QUERY"), Some(HttpMethod::QUERY));
assert_eq!(HttpMethod::from_str("query"), Some(HttpMethod::QUERY));
assert_eq!(HttpMethod::QUERY.as_str(), "QUERY");
}
#[tokio::test]
async fn test_query_route_dispatch() {
async fn echo_body(req: HttpRequest) -> Result<HttpResponse, Error> {
Ok(HttpResponse::ok().with_body(req.body.clone()))
}
let mut router = Router::new();
router.query("/search", echo_body);
let mut request = HttpRequest::new("QUERY".to_string(), "/search".to_string());
request.body = b"name=john".to_vec();
let response = router.route(request).await.unwrap();
assert_eq!(response.status, 200);
assert_eq!(response.into_body_bytes().as_ref(), b"name=john");
let request = HttpRequest::new("GET".to_string(), "/search".to_string());
assert!(router.route(request).await.is_err());
}
#[test]
fn test_match_path_multiple_params() {
let pattern = "/users/:user_id/posts/:post_id";
let path = "/users/123/posts/456";
let result = match_path_str(pattern, path);
assert!(result.is_some());
let params = result.unwrap();
assert_eq!(params.get("user_id"), Some(&"123".to_string()));
assert_eq!(params.get("post_id"), Some(&"456".to_string()));
}
#[test]
fn test_match_path_trailing_slash() {
let pattern = "/users";
let path = "/users/";
let result = match_path_str(pattern, path);
assert!(result.is_some() || result.is_none());
}
#[test]
fn test_match_path_nested() {
let pattern = "/api/v1/users/:id";
let path = "/api/v1/users/123";
let result = match_path_str(pattern, path);
assert!(result.is_some());
let params = result.unwrap();
assert_eq!(params.get("id"), Some(&"123".to_string()));
}
#[test]
fn test_match_path_empty() {
let pattern = "/";
let path = "/";
let result = match_path_str(pattern, path);
assert!(result.is_some());
}
#[test]
fn test_parse_query_string_empty() {
let query = "";
let params = parse_query_string(query);
assert!(params.is_empty() || params.len() == 1);
}
#[test]
fn test_parse_query_string_special_chars() {
let query = "name=john%20doe&email=test%40example.com";
let params = parse_query_string(query);
assert!(params.contains_key("name"));
assert!(params.contains_key("email"));
}
#[test]
fn test_parse_query_string_no_value() {
let query = "flag&debug=true";
let params = parse_query_string(query);
assert!(params.contains_key("debug"));
assert_eq!(params.get("debug"), Some(&"true".to_string()));
}
#[test]
fn test_match_path_catch_all() {
let pattern = "/files/*path";
assert!(match_path_str(pattern, "/files/docs").is_some());
let result = match_path_str(pattern, "/files/docs/readme.md");
assert!(result.is_some());
let params = result.unwrap();
assert_eq!(params.get("path"), Some(&"docs/readme.md".to_string()));
let result = match_path_str(pattern, "/files");
assert!(result.is_some());
assert_eq!(result.unwrap().get("path"), Some(&String::new()));
assert!(match_path_str(pattern, "/other").is_none());
}
#[test]
fn test_match_path_catch_all_with_preceding_param() {
let pattern = "/users/:id/files/*path";
let result = match_path_str(pattern, "/users/42/files/a/b");
assert!(result.is_some());
let params = result.unwrap();
assert_eq!(params.get("id"), Some(&"42".to_string()));
assert_eq!(params.get("path"), Some(&"a/b".to_string()));
let pattern = "/files/*";
let result = match_path_str(pattern, "/files/a/b/c");
assert!(result.is_some());
let params = result.unwrap();
assert_eq!(params.get("*"), Some(&"a/b/c".to_string()));
}
#[tokio::test]
async fn test_router_route_catch_all() {
async fn echo_path(req: HttpRequest) -> Result<HttpResponse, Error> {
let p = req.path_params.get("path").cloned().unwrap_or_default();
Ok(HttpResponse::ok().with_body(p.into_bytes()))
}
let mut router = Router::new();
router.get("/files/*path", echo_path);
let req = HttpRequest::new("GET".to_string(), "/files/docs/readme.md".to_string());
let response = router.route(req).await.unwrap();
assert_eq!(response.status, 200);
assert_eq!(response.into_body_bytes().as_ref(), b"docs/readme.md");
let (_, params) = router
.match_route("GET", "/files/docs/readme.md")
.expect("catch-all route should match via match_route");
assert_eq!(params.get("path"), Some(&"docs/readme.md".to_string()));
}
#[test]
fn test_match_path_param_with_special_chars() {
let pattern = "/users/:id";
let path = "/users/abc-123";
let result = match_path_str(pattern, path);
assert!(result.is_some());
let params = result.unwrap();
assert_eq!(params.get("id"), Some(&"abc-123".to_string()));
}
#[test]
fn test_route_creation_optimized() {
let route = Route::new(HttpMethod::GET, "/users", test_handler);
assert_eq!(route.method, HttpMethod::GET);
assert_eq!(route.path, "/users");
}
#[test]
fn test_route_creation_legacy() {
let legacy_handler: HandlerFn =
Arc::new(|_req| Box::pin(async move { Ok(HttpResponse::ok()) }));
let route = Route::from_legacy(HttpMethod::GET, "/users", legacy_handler);
assert_eq!(route.method, HttpMethod::GET);
assert_eq!(route.path, "/users");
}
#[test]
fn test_router_fluent_api() {
let mut router = Router::new();
router
.get("/users", test_handler)
.post("/users", test_handler)
.put("/users/:id", test_handler)
.delete("/users/:id", test_handler)
.patch("/users/:id", test_handler)
.options("/users", test_handler)
.head("/users/:id", test_handler);
assert_eq!(router.routes.len(), 7);
}
#[test]
fn test_router_options_route() {
let mut router = Router::new();
router.options("/api/resource", test_handler);
assert_eq!(router.routes.len(), 1);
assert_eq!(router.routes[0].method, HttpMethod::OPTIONS);
assert_eq!(router.routes[0].path, "/api/resource");
}
#[test]
fn test_router_head_route() {
let mut router = Router::new();
router.head("/api/resource/:id", test_handler);
assert_eq!(router.routes.len(), 1);
assert_eq!(router.routes[0].method, HttpMethod::HEAD);
assert_eq!(router.routes[0].path, "/api/resource/:id");
}
#[test]
fn test_router_add_route() {
let mut router = Router::new();
let route = Route::new(HttpMethod::GET, "/test", test_handler);
router.add_route(route);
assert_eq!(router.routes.len(), 1);
}
#[test]
fn test_router_multiple_routes() {
let mut router = Router::new();
for i in 0..5 {
router.get(format!("/test{}", i), test_handler);
}
assert_eq!(router.routes.len(), 5);
}
#[test]
fn test_parse_query_string_multiple_same_key() {
let query = "tag=rust&tag=web&tag=framework";
let params = parse_query_string(query);
assert!(params.contains_key("tag"));
}
#[test]
fn test_route_with_constraints() {
let constraints =
RouteConstraints::new().add("id", Box::new(crate::route_constraint::IntConstraint));
let route =
Route::new(HttpMethod::GET, "/users/:id", test_handler).with_constraints(constraints);
assert!(route.constraints.is_some());
}
#[tokio::test]
async fn test_router_dispatch() {
let mut router = Router::new();
router.get("/test", test_handler);
let req = HttpRequest::new("GET".to_string(), "/test".to_string());
let response = router.route(req).await.unwrap();
assert_eq!(response.status, 200);
}
#[tokio::test]
async fn test_router_dispatch_with_params() {
async fn param_handler(req: HttpRequest) -> Result<HttpResponse, Error> {
let id = req.param("id").unwrap();
Ok(HttpResponse::ok().with_body(id.as_bytes().to_vec()))
}
let mut router = Router::new();
router.get("/users/:id", param_handler);
let req = HttpRequest::new("GET".to_string(), "/users/123".to_string());
let response = router.route(req).await.unwrap();
assert_eq!(response.status, 200);
assert_eq!(String::from_utf8(response.body).unwrap(), "123");
}
#[tokio::test]
async fn test_router_404() {
let router = Router::new();
let req = HttpRequest::new("GET".to_string(), "/nonexistent".to_string());
let result = router.route(req).await;
assert!(matches!(result, Err(Error::RouteNotFound(_))));
}
}