armature_core/
routing.rs

1// Routing system for HTTP requests
2//
3// This module provides an optimized routing system that leverages:
4// - Monomorphization: Handlers are specialized at compile time
5// - Inline dispatch: Hot paths use #[inline(always)]
6// - Zero-cost abstractions: Minimal runtime overhead
7
8use crate::handler::{BoxedHandler, IntoHandler};
9use crate::logging::{debug, trace};
10use crate::route_constraint::RouteConstraints;
11use crate::{Error, HttpMethod, HttpRequest, HttpResponse};
12use std::collections::HashMap;
13use std::future::Future;
14use std::pin::Pin;
15use std::sync::Arc;
16
17/// A route handler function type (legacy - for backwards compatibility)
18///
19/// **Deprecated**: Use `BoxedHandler` for better performance via monomorphization.
20/// This type uses double dynamic dispatch (dyn Fn + Box<dyn Future>) which
21/// prevents the compiler from inlining handler code.
22///
23/// Prefer using the optimized handler system:
24/// ```ignore
25/// use armature_core::handler::handler;
26///
27/// let h = handler(my_async_fn);
28/// ```
29pub type HandlerFn = Arc<
30    dyn Fn(HttpRequest) -> Pin<Box<dyn Future<Output = Result<HttpResponse, Error>> + Send>>
31        + Send
32        + Sync,
33>;
34
35/// Optimized route handler that enables inlining via monomorphization.
36///
37/// This type wraps handlers in a way that allows the compiler to see through
38/// to the actual handler implementation and inline it.
39pub type OptimizedHandler = BoxedHandler;
40
41/// Route definition with handler
42#[derive(Clone)]
43pub struct Route {
44    pub method: HttpMethod,
45    pub path: String,
46    /// The route handler - uses optimized dispatch
47    pub handler: BoxedHandler,
48    /// Optional route constraints for parameter validation
49    pub constraints: Option<RouteConstraints>,
50}
51
52impl Route {
53    /// Create a new route with an optimized handler.
54    ///
55    /// This method accepts any handler type that implements `IntoHandler`,
56    /// enabling compile-time specialization.
57    #[inline]
58    pub fn new<H, Args>(method: HttpMethod, path: impl Into<String>, handler: H) -> Self
59    where
60        H: IntoHandler<Args>,
61    {
62        Self {
63            method,
64            path: path.into(),
65            handler: BoxedHandler::new(handler.into_handler()),
66            constraints: None,
67        }
68    }
69
70    /// Create a route from a legacy HandlerFn for backwards compatibility.
71    #[inline]
72    pub fn from_legacy(method: HttpMethod, path: impl Into<String>, handler: HandlerFn) -> Self {
73        Self {
74            method,
75            path: path.into(),
76            handler: crate::handler::from_legacy_handler(handler),
77            constraints: None,
78        }
79    }
80
81    /// Add route constraints.
82    #[inline]
83    pub fn with_constraints(mut self, constraints: RouteConstraints) -> Self {
84        self.constraints = Some(constraints);
85        self
86    }
87}
88
89/// Router for managing routes and dispatching requests.
90///
91/// The router uses optimized handler dispatch that enables:
92/// - Monomorphization of handler code
93/// - Inlining of handler bodies
94/// - Minimal allocation in the hot path
95#[derive(Clone)]
96pub struct Router {
97    pub routes: Vec<Route>,
98}
99
100impl Router {
101    /// Create a new empty router.
102    #[inline]
103    pub fn new() -> Self {
104        Self { routes: Vec::new() }
105    }
106
107    /// Add a route to the router.
108    #[inline]
109    pub fn add_route(&mut self, route: Route) {
110        self.routes.push(route);
111    }
112
113    /// Add a GET route with an optimized handler.
114    #[inline]
115    pub fn get<H, Args>(&mut self, path: impl Into<String>, handler: H) -> &mut Self
116    where
117        H: IntoHandler<Args>,
118    {
119        self.routes.push(Route::new(HttpMethod::GET, path, handler));
120        self
121    }
122
123    /// Add a POST route with an optimized handler.
124    #[inline]
125    pub fn post<H, Args>(&mut self, path: impl Into<String>, handler: H) -> &mut Self
126    where
127        H: IntoHandler<Args>,
128    {
129        self.routes
130            .push(Route::new(HttpMethod::POST, path, handler));
131        self
132    }
133
134    /// Add a PUT route with an optimized handler.
135    #[inline]
136    pub fn put<H, Args>(&mut self, path: impl Into<String>, handler: H) -> &mut Self
137    where
138        H: IntoHandler<Args>,
139    {
140        self.routes.push(Route::new(HttpMethod::PUT, path, handler));
141        self
142    }
143
144    /// Add a DELETE route with an optimized handler.
145    #[inline]
146    pub fn delete<H, Args>(&mut self, path: impl Into<String>, handler: H) -> &mut Self
147    where
148        H: IntoHandler<Args>,
149    {
150        self.routes
151            .push(Route::new(HttpMethod::DELETE, path, handler));
152        self
153    }
154
155    /// Add a PATCH route with an optimized handler.
156    #[inline]
157    pub fn patch<H, Args>(&mut self, path: impl Into<String>, handler: H) -> &mut Self
158    where
159        H: IntoHandler<Args>,
160    {
161        self.routes
162            .push(Route::new(HttpMethod::PATCH, path, handler));
163        self
164    }
165
166    /// Match a route without executing the handler.
167    /// Returns the handler and path parameters if a route matches.
168    /// Useful for route lookup benchmarking and inspection.
169    #[inline]
170    pub fn match_route(
171        &self,
172        method: &str,
173        path: &str,
174    ) -> Option<(BoxedHandler, HashMap<String, String>)> {
175        // Strip query string if present
176        let path = path.split('?').next().unwrap_or(path);
177
178        for route in &self.routes {
179            if route.method.as_str() != method {
180                continue;
181            }
182
183            if let Some(params) = match_path(&route.path, path) {
184                return Some((route.handler.clone(), params));
185            }
186        }
187
188        None
189    }
190
191    /// Find a route that matches the request and execute the handler.
192    ///
193    /// This is the main hot path for request handling. The handler dispatch
194    /// is optimized via monomorphization - the actual handler code can be
195    /// inlined by the compiler.
196    #[inline]
197    pub async fn route(&self, mut request: HttpRequest) -> Result<HttpResponse, Error> {
198        debug!("Routing request: {} {}", request.method, request.path);
199
200        // Parse query parameters from path
201        let (path, query_string) = request
202            .path
203            .split_once('?')
204            .map(|(p, q)| (p, Some(q)))
205            .unwrap_or((&request.path, None));
206
207        if let Some(query) = query_string {
208            trace!("Parsing query string: {}", query);
209            request.query_params = parse_query_string(query);
210        }
211
212        // Find matching route - this is the route matching hot path
213        for route in &self.routes {
214            if route.method.as_str() != request.method {
215                continue;
216            }
217
218            if let Some(params) = match_path(&route.path, path) {
219                debug!(
220                    "Route matched: {} {} -> {}",
221                    request.method, path, route.path
222                );
223
224                // Validate route constraints if present
225                if let Some(constraints) = &route.constraints {
226                    trace!("Validating route constraints");
227                    constraints.validate(&params)?;
228                }
229
230                request.path_params = params;
231
232                // Handler dispatch - the BoxedHandler.call() is optimized
233                // to allow the compiler to inline the actual handler body
234                trace!("Dispatching handler");
235                return route.handler.call(request).await;
236            }
237        }
238
239        debug!("No route found for {} {}", request.method, path);
240        Err(Error::RouteNotFound(format!("{} {}", request.method, path)))
241    }
242}
243
244impl Default for Router {
245    fn default() -> Self {
246        Self::new()
247    }
248}
249
250/// Match a route path pattern against a request path
251/// Returns Some(params) if matched, None otherwise
252fn match_path(pattern: &str, path: &str) -> Option<HashMap<String, String>> {
253    let pattern_parts: Vec<&str> = pattern.split('/').filter(|s| !s.is_empty()).collect();
254    let path_parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
255
256    if pattern_parts.len() != path_parts.len() {
257        return None;
258    }
259
260    let mut params = HashMap::new();
261
262    for (pattern_part, path_part) in pattern_parts.iter().zip(path_parts.iter()) {
263        if let Some(param_name) = pattern_part.strip_prefix(':') {
264            // This is a parameter
265            params.insert(param_name.to_string(), path_part.to_string());
266        } else if pattern_part != path_part {
267            // Static part doesn't match
268            return None;
269        }
270    }
271
272    Some(params)
273}
274
275/// Parse a query string into a map of parameters
276///
277/// Uses SIMD-optimized byte searching via memchr for faster parsing.
278#[inline]
279fn parse_query_string(query: &str) -> HashMap<String, String> {
280    // Use the SIMD-optimized parser
281    crate::simd_parser::parse_query_string_fast(query)
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    // Test helper handler
289    async fn test_handler(_req: HttpRequest) -> Result<HttpResponse, Error> {
290        Ok(HttpResponse::ok())
291    }
292
293    #[test]
294    fn test_match_path_static() {
295        let pattern = "/users";
296        let path = "/users";
297        let result = match_path(pattern, path);
298        assert!(result.is_some());
299        assert_eq!(result.unwrap().len(), 0);
300    }
301
302    #[test]
303    fn test_match_path_with_param() {
304        let pattern = "/users/:id";
305        let path = "/users/123";
306        let result = match_path(pattern, path);
307        assert!(result.is_some());
308        let params = result.unwrap();
309        assert_eq!(params.get("id"), Some(&"123".to_string()));
310    }
311
312    #[test]
313    fn test_match_path_no_match() {
314        let pattern = "/users/:id";
315        let path = "/posts/123";
316        let result = match_path(pattern, path);
317        assert!(result.is_none());
318    }
319
320    #[test]
321    fn test_parse_query_string() {
322        let query = "name=john&age=30";
323        let params = parse_query_string(query);
324        assert_eq!(params.get("name"), Some(&"john".to_string()));
325        assert_eq!(params.get("age"), Some(&"30".to_string()));
326    }
327
328    #[test]
329    fn test_match_path_multiple_params() {
330        let pattern = "/users/:user_id/posts/:post_id";
331        let path = "/users/123/posts/456";
332        let result = match_path(pattern, path);
333        assert!(result.is_some());
334        let params = result.unwrap();
335        assert_eq!(params.get("user_id"), Some(&"123".to_string()));
336        assert_eq!(params.get("post_id"), Some(&"456".to_string()));
337    }
338
339    #[test]
340    fn test_match_path_trailing_slash() {
341        let pattern = "/users";
342        let path = "/users/";
343        let result = match_path(pattern, path);
344        // Should handle trailing slash gracefully
345        assert!(result.is_some() || result.is_none());
346    }
347
348    #[test]
349    fn test_match_path_nested() {
350        let pattern = "/api/v1/users/:id";
351        let path = "/api/v1/users/123";
352        let result = match_path(pattern, path);
353        assert!(result.is_some());
354        let params = result.unwrap();
355        assert_eq!(params.get("id"), Some(&"123".to_string()));
356    }
357
358    #[test]
359    fn test_match_path_empty() {
360        let pattern = "/";
361        let path = "/";
362        let result = match_path(pattern, path);
363        assert!(result.is_some());
364    }
365
366    #[test]
367    fn test_parse_query_string_empty() {
368        let query = "";
369        let params = parse_query_string(query);
370        // Empty string may return one empty entry, which is fine
371        assert!(params.is_empty() || params.len() == 1);
372    }
373
374    #[test]
375    fn test_parse_query_string_special_chars() {
376        let query = "name=john%20doe&email=test%40example.com";
377        let params = parse_query_string(query);
378        assert!(params.contains_key("name"));
379        assert!(params.contains_key("email"));
380    }
381
382    #[test]
383    fn test_parse_query_string_no_value() {
384        let query = "flag&debug=true";
385        let params = parse_query_string(query);
386        assert!(params.contains_key("debug"));
387        assert_eq!(params.get("debug"), Some(&"true".to_string()));
388    }
389
390    #[test]
391    fn test_match_path_param_with_special_chars() {
392        let pattern = "/users/:id";
393        let path = "/users/abc-123";
394        let result = match_path(pattern, path);
395        assert!(result.is_some());
396        let params = result.unwrap();
397        assert_eq!(params.get("id"), Some(&"abc-123".to_string()));
398    }
399
400    #[test]
401    fn test_route_creation_optimized() {
402        // Test the new optimized route creation
403        let route = Route::new(HttpMethod::GET, "/users", test_handler);
404        assert_eq!(route.method, HttpMethod::GET);
405        assert_eq!(route.path, "/users");
406    }
407
408    #[test]
409    fn test_route_creation_legacy() {
410        // Test legacy handler compatibility
411        let legacy_handler: HandlerFn =
412            Arc::new(|_req| Box::pin(async move { Ok(HttpResponse::ok()) }));
413        let route = Route::from_legacy(HttpMethod::GET, "/users", legacy_handler);
414        assert_eq!(route.method, HttpMethod::GET);
415        assert_eq!(route.path, "/users");
416    }
417
418    #[test]
419    fn test_router_fluent_api() {
420        let mut router = Router::new();
421        router
422            .get("/users", test_handler)
423            .post("/users", test_handler)
424            .put("/users/:id", test_handler)
425            .delete("/users/:id", test_handler);
426
427        assert_eq!(router.routes.len(), 4);
428    }
429
430    #[test]
431    fn test_router_add_route() {
432        let mut router = Router::new();
433        let route = Route::new(HttpMethod::GET, "/test", test_handler);
434        router.add_route(route);
435        assert_eq!(router.routes.len(), 1);
436    }
437
438    #[test]
439    fn test_router_multiple_routes() {
440        let mut router = Router::new();
441
442        for i in 0..5 {
443            router.get(format!("/test{}", i), test_handler);
444        }
445
446        assert_eq!(router.routes.len(), 5);
447    }
448
449    #[test]
450    fn test_parse_query_string_multiple_same_key() {
451        let query = "tag=rust&tag=web&tag=framework";
452        let params = parse_query_string(query);
453        // Should contain at least one tag
454        assert!(params.contains_key("tag"));
455    }
456
457    #[test]
458    fn test_route_with_constraints() {
459        let constraints =
460            RouteConstraints::new().add("id", Box::new(crate::route_constraint::IntConstraint));
461
462        let route =
463            Route::new(HttpMethod::GET, "/users/:id", test_handler).with_constraints(constraints);
464
465        assert!(route.constraints.is_some());
466    }
467
468    #[tokio::test]
469    async fn test_router_dispatch() {
470        let mut router = Router::new();
471        router.get("/test", test_handler);
472
473        let req = HttpRequest::new("GET".to_string(), "/test".to_string());
474        let response = router.route(req).await.unwrap();
475        assert_eq!(response.status, 200);
476    }
477
478    #[tokio::test]
479    async fn test_router_dispatch_with_params() {
480        async fn param_handler(req: HttpRequest) -> Result<HttpResponse, Error> {
481            let id = req.param("id").unwrap();
482            Ok(HttpResponse::ok().with_body(id.as_bytes().to_vec()))
483        }
484
485        let mut router = Router::new();
486        router.get("/users/:id", param_handler);
487
488        let req = HttpRequest::new("GET".to_string(), "/users/123".to_string());
489        let response = router.route(req).await.unwrap();
490        assert_eq!(response.status, 200);
491        assert_eq!(String::from_utf8(response.body).unwrap(), "123");
492    }
493
494    #[tokio::test]
495    async fn test_router_404() {
496        let router = Router::new();
497        let req = HttpRequest::new("GET".to_string(), "/nonexistent".to_string());
498        let result = router.route(req).await;
499        assert!(matches!(result, Err(Error::RouteNotFound(_))));
500    }
501}