torch_web/
router.rs

1//! # HTTP Router
2//!
3//! Fast, lightweight HTTP request routing with support for path parameters and wildcards.
4//! The router efficiently matches incoming requests to registered handlers based on
5//! HTTP method and URL path patterns.
6
7use std::collections::HashMap;
8use http::Method;
9use crate::{Request, Response, HandlerFn};
10
11/// A fast, lightweight HTTP router that matches requests to handlers.
12///
13/// The router supports:
14/// - Path parameters (`:name` syntax)
15/// - Wildcard matching (`*` syntax)
16/// - Multiple HTTP methods
17/// - Custom 404 handlers
18/// - Efficient O(1) method lookup with linear path matching
19///
20/// # Examples
21///
22/// ## Basic Usage
23///
24/// ```rust
25/// use torch_web::{Router, Request, Response, Method};
26///
27/// let mut router = Router::new();
28///
29/// router.get("/", |_req: Request| async {
30///     Response::ok().body("Home page")
31/// });
32///
33/// router.get("/users/:id", |req: Request| async move {
34///     let id = req.param("id").unwrap();
35///     Response::ok().body(format!("User: {}", id))
36/// });
37/// ```
38///
39/// ## With Parameters and Wildcards
40///
41/// ```rust
42/// use torch_web::{Router, Request, Response};
43///
44/// let mut router = Router::new();
45///
46/// // Path parameters
47/// router.get("/users/:id/posts/:post_id", |req: Request| async move {
48///     let user_id = req.param("id").unwrap();
49///     let post_id = req.param("post_id").unwrap();
50///     Response::ok().body(format!("User {} Post {}", user_id, post_id))
51/// });
52///
53/// // Wildcard for static files
54/// router.get("/static/*", |req: Request| async move {
55///     let path = req.path();
56///     Response::ok().body(format!("Serving static file: {}", path))
57/// });
58/// ```
59pub struct Router {
60    routes: HashMap<Method, Vec<Route>>,
61    not_found_handler: Option<HandlerFn>,
62}
63
64/// Represents a single route with its pattern and handler.
65///
66/// This is an internal structure that pairs a route pattern with its handler function.
67/// Routes are stored in the router and matched against incoming requests.
68#[derive(Clone)]
69struct Route {
70    pattern: RoutePattern,
71    handler: HandlerFn,
72}
73
74/// Pattern matching engine for route paths.
75///
76/// Parses route patterns into segments that can be efficiently matched against
77/// incoming request paths. Supports static segments, named parameters, and wildcards.
78#[derive(Debug, Clone)]
79struct RoutePattern {
80    segments: Vec<Segment>,
81}
82
83/// A single segment of a route pattern.
84///
85/// Route patterns are broken down into segments separated by `/`. Each segment
86/// can be one of three types:
87/// - `Static`: Exact string match (e.g., "users", "api")
88/// - `Param`: Named parameter that captures the segment value (e.g., ":id", ":name")
89/// - `Wildcard`: Matches any remaining path segments (e.g., "*")
90#[derive(Debug, Clone, PartialEq)]
91enum Segment {
92    /// A static segment that must match exactly
93    Static(String),
94    /// A parameter segment that captures the value with the given name
95    Param(String),
96    /// A wildcard that matches any remaining path
97    Wildcard,
98}
99
100impl Router {
101    /// Creates a new empty router.
102    ///
103    /// The router starts with no routes registered and uses the default 404 handler
104    /// for unmatched requests.
105    ///
106    /// # Examples
107    ///
108    /// ```rust
109    /// use torch_web::Router;
110    ///
111    /// let router = Router::new();
112    /// ```
113    pub fn new() -> Self {
114        Self {
115            routes: HashMap::new(),
116            not_found_handler: None,
117        }
118    }
119
120    /// Registers a route for the specified HTTP method and path pattern.
121    ///
122    /// This is the core method for route registration. All other route methods
123    /// (get, post, etc.) delegate to this method.
124    ///
125    /// # Parameters
126    ///
127    /// * `method` - The HTTP method to match (GET, POST, PUT, DELETE, etc.)
128    /// * `path` - The path pattern to match, supporting parameters and wildcards
129    /// * `handler` - The handler function to execute when the route matches
130    ///
131    /// # Path Pattern Syntax
132    ///
133    /// - Static segments: `/users`, `/api/v1`
134    /// - Parameters: `/users/:id`, `/posts/:id/comments/:comment_id`
135    /// - Wildcards: `/static/*` (matches any remaining path)
136    ///
137    /// # Examples
138    ///
139    /// ```rust
140    /// use torch_web::{Router, Request, Response, Method};
141    ///
142    /// let mut router = Router::new();
143    ///
144    /// router.route(Method::GET, "/", |_req: Request| async {
145    ///     Response::ok().body("Home")
146    /// });
147    ///
148    /// router.route(Method::POST, "/users", |_req: Request| async {
149    ///     Response::created().body("User created")
150    /// });
151    ///
152    /// router.route(Method::GET, "/users/:id", |req: Request| async move {
153    ///     let id = req.param("id").unwrap();
154    ///     Response::ok().body(format!("User: {}", id))
155    /// });
156    /// ```
157    pub fn route(&mut self, method: Method, path: &str, handler: HandlerFn) {
158        let pattern = RoutePattern::parse(path);
159        let route = Route { pattern, handler };
160
161        self.routes
162            .entry(method)
163            .or_insert_with(Vec::new)
164            .push(route);
165    }
166
167    /// Registers a GET route handler.
168    ///
169    /// Convenience method for registering GET routes. GET requests should be
170    /// idempotent and safe (no side effects).
171    ///
172    /// # Parameters
173    ///
174    /// * `path` - The path pattern to match
175    /// * `handler` - The handler function to execute
176    ///
177    /// # Examples
178    ///
179    /// ```rust
180    /// use torch_web::{Router, Request, Response};
181    ///
182    /// let mut router = Router::new();
183    ///
184    /// router.get("/", |_req: Request| async {
185    ///     Response::ok().body("Welcome!")
186    /// });
187    ///
188    /// router.get("/users/:id", |req: Request| async move {
189    ///     let id = req.param("id").unwrap();
190    ///     Response::ok().body(format!("User: {}", id))
191    /// });
192    /// ```
193    pub fn get(&mut self, path: &str, handler: HandlerFn) {
194        self.route(Method::GET, path, handler);
195    }
196
197    /// Registers a POST route handler.
198    ///
199    /// Convenience method for registering POST routes. POST requests are typically
200    /// used for creating resources or submitting data.
201    ///
202    /// # Parameters
203    ///
204    /// * `path` - The path pattern to match
205    /// * `handler` - The handler function to execute
206    ///
207    /// # Examples
208    ///
209    /// ```rust
210    /// use torch_web::{Router, Request, Response};
211    ///
212    /// let mut router = Router::new();
213    ///
214    /// router.post("/users", |_req: Request| async {
215    ///     Response::created().body("User created")
216    /// });
217    ///
218    /// router.post("/login", |_req: Request| async {
219    ///     Response::ok().body("Login successful")
220    /// });
221    /// ```
222    pub fn post(&mut self, path: &str, handler: HandlerFn) {
223        self.route(Method::POST, path, handler);
224    }
225
226    /// Registers a PUT route handler.
227    ///
228    /// Convenience method for registering PUT routes. PUT requests are typically
229    /// used for updating entire resources or creating resources with specific IDs.
230    ///
231    /// # Parameters
232    ///
233    /// * `path` - The path pattern to match
234    /// * `handler` - The handler function to execute
235    ///
236    /// # Examples
237    ///
238    /// ```rust
239    /// use torch_web::{Router, Request, Response};
240    ///
241    /// let mut router = Router::new();
242    ///
243    /// router.put("/users/:id", |req: Request| async move {
244    ///     let id = req.param("id").unwrap();
245    ///     Response::ok().body(format!("Updated user: {}", id))
246    /// });
247    /// ```
248    pub fn put(&mut self, path: &str, handler: HandlerFn) {
249        self.route(Method::PUT, path, handler);
250    }
251
252    /// Registers a DELETE route handler.
253    ///
254    /// Convenience method for registering DELETE routes. DELETE requests are
255    /// used for removing resources.
256    ///
257    /// # Parameters
258    ///
259    /// * `path` - The path pattern to match
260    /// * `handler` - The handler function to execute
261    ///
262    /// # Examples
263    ///
264    /// ```rust
265    /// use torch_web::{Router, Request, Response};
266    ///
267    /// let mut router = Router::new();
268    ///
269    /// router.delete("/users/:id", |req: Request| async move {
270    ///     let id = req.param("id").unwrap();
271    ///     Response::ok().body(format!("Deleted user: {}", id))
272    /// });
273    /// ```
274    pub fn delete(&mut self, path: &str, handler: HandlerFn) {
275        self.route(Method::DELETE, path, handler);
276    }
277
278    /// Registers a PATCH route handler.
279    ///
280    /// Convenience method for registering PATCH routes. PATCH requests are
281    /// used for partial updates to resources.
282    ///
283    /// # Parameters
284    ///
285    /// * `path` - The path pattern to match
286    /// * `handler` - The handler function to execute
287    ///
288    /// # Examples
289    ///
290    /// ```rust
291    /// use torch_web::{Router, Request, Response};
292    ///
293    /// let mut router = Router::new();
294    ///
295    /// router.patch("/users/:id", |req: Request| async move {
296    ///     let id = req.param("id").unwrap();
297    ///     Response::ok().body(format!("Patched user: {}", id))
298    /// });
299    /// ```
300    pub fn patch(&mut self, path: &str, handler: HandlerFn) {
301        self.route(Method::PATCH, path, handler);
302    }
303
304    /// Sets a custom handler for requests that don't match any registered route.
305    ///
306    /// By default, unmatched requests return a 404 Not Found response. This method
307    /// allows you to customize that behavior.
308    ///
309    /// # Parameters
310    ///
311    /// * `handler` - The handler function to execute for unmatched routes
312    ///
313    /// # Examples
314    ///
315    /// ```rust
316    /// use torch_web::{Router, Request, Response};
317    ///
318    /// let mut router = Router::new();
319    ///
320    /// router.not_found(|req: Request| async move {
321    ///     Response::not_found()
322    ///         .body(format!("Sorry, {} was not found", req.path()))
323    /// });
324    /// ```
325    pub fn not_found(&mut self, handler: HandlerFn) {
326        self.not_found_handler = Some(handler);
327    }
328
329    /// Get all routes for mounting (internal use)
330    pub(crate) fn get_all_routes(&self) -> Vec<(Method, String, HandlerFn)> {
331        let mut all_routes = Vec::new();
332
333        for (method, routes) in &self.routes {
334            for route in routes {
335                // Convert the pattern back to a string representation
336                let path = route.pattern.to_string();
337                all_routes.push((method.clone(), path, route.handler.clone()));
338            }
339        }
340
341        all_routes
342    }
343
344    /// Route a request to the appropriate handler
345    pub async fn route_request(&self, mut req: Request) -> Response {
346        if let Some(routes) = self.routes.get(req.method()) {
347            for route in routes {
348                if let Some(params) = route.pattern.matches(req.path()) {
349                    // Set path parameters in the request
350                    for (name, value) in params {
351                        req.set_param(name, value);
352                    }
353                    return (route.handler)(req).await;
354                }
355            }
356        }
357
358        // No route found, use 404 handler or default
359        if let Some(handler) = &self.not_found_handler {
360            handler(req).await
361        } else {
362            Response::not_found()
363        }
364    }
365}
366
367impl Default for Router {
368    fn default() -> Self {
369        Self::new()
370    }
371}
372
373impl Clone for Router {
374    fn clone(&self) -> Self {
375        Self {
376            routes: self.routes.clone(),
377            not_found_handler: self.not_found_handler.clone(),
378        }
379    }
380}
381
382impl RoutePattern {
383    /// Convert pattern back to string representation
384    fn to_string(&self) -> String {
385        let mut result = String::from("/");
386        for segment in &self.segments {
387            match segment {
388                Segment::Static(s) => {
389                    result.push_str(s);
390                    result.push('/');
391                }
392                Segment::Param(name) => {
393                    result.push(':');
394                    result.push_str(name);
395                    result.push('/');
396                }
397                Segment::Wildcard => {
398                    result.push('*');
399                    result.push('/');
400                }
401            }
402        }
403        // Remove trailing slash unless it's the root path
404        if result.len() > 1 && result.ends_with('/') {
405            result.pop();
406        }
407        result
408    }
409
410    /// Parse a route pattern string into segments
411    fn parse(pattern: &str) -> Self {
412        let mut segments = Vec::new();
413
414        for segment in pattern.split('/').filter(|s| !s.is_empty()) {
415            if segment.starts_with(':') {
416                let param_name = segment[1..].to_string();
417                segments.push(Segment::Param(param_name));
418            } else if segment == "*" {
419                segments.push(Segment::Wildcard);
420            } else {
421                segments.push(Segment::Static(segment.to_string()));
422            }
423        }
424
425        Self { segments }
426    }
427
428    /// Check if this pattern matches the given path and extract parameters
429    fn matches(&self, path: &str) -> Option<HashMap<String, String>> {
430        let path_segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
431        
432        // Handle root path
433        if path == "/" && self.segments.is_empty() {
434            return Some(HashMap::new());
435        }
436
437        let mut params = HashMap::new();
438        let mut path_idx = 0;
439        let mut pattern_idx = 0;
440
441        while pattern_idx < self.segments.len() && path_idx < path_segments.len() {
442            match &self.segments[pattern_idx] {
443                Segment::Static(expected) => {
444                    if path_segments[path_idx] != expected {
445                        return None;
446                    }
447                    path_idx += 1;
448                    pattern_idx += 1;
449                }
450                Segment::Param(name) => {
451                    params.insert(name.clone(), path_segments[path_idx].to_string());
452                    path_idx += 1;
453                    pattern_idx += 1;
454                }
455                Segment::Wildcard => {
456                    // Wildcard matches everything remaining
457                    return Some(params);
458                }
459            }
460        }
461
462        // Check if we consumed all segments
463        if pattern_idx == self.segments.len() && path_idx == path_segments.len() {
464            Some(params)
465        } else if pattern_idx < self.segments.len() 
466            && matches!(self.segments[pattern_idx], Segment::Wildcard) {
467            Some(params)
468        } else {
469            None
470        }
471    }
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477    use crate::Response;
478
479    #[test]
480    fn test_route_pattern_parsing() {
481        let pattern = RoutePattern::parse("/users/:id/posts/:post_id");
482        assert_eq!(pattern.segments.len(), 4);
483        assert_eq!(pattern.segments[0], Segment::Static("users".to_string()));
484        assert_eq!(pattern.segments[1], Segment::Param("id".to_string()));
485        assert_eq!(pattern.segments[2], Segment::Static("posts".to_string()));
486        assert_eq!(pattern.segments[3], Segment::Param("post_id".to_string()));
487    }
488
489    #[test]
490    fn test_route_pattern_matching() {
491        let pattern = RoutePattern::parse("/users/:id");
492        let params = pattern.matches("/users/123").unwrap();
493        assert_eq!(params.get("id"), Some(&"123".to_string()));
494
495        assert!(pattern.matches("/users").is_none());
496        assert!(pattern.matches("/users/123/extra").is_none());
497    }
498
499    #[test]
500    fn test_wildcard_matching() {
501        let pattern = RoutePattern::parse("/files/*");
502        let params = pattern.matches("/files/path/to/file.txt");
503        assert!(params.is_some());
504    }
505
506    #[tokio::test]
507    async fn test_router_basic_routing() {
508        let mut router = Router::new();
509        
510        router.get("/", std::sync::Arc::new(|_| Box::pin(async {
511            Response::ok().body("Home")
512        })));
513
514        router.get("/users/:id", std::sync::Arc::new(|req| Box::pin(async move {
515            let id = req.param("id").unwrap_or("unknown");
516            Response::ok().body(format!("User: {}", id))
517        })));
518
519        // Test root route
520        let req = Request::from_hyper(
521            http::Request::builder()
522                .method("GET")
523                .uri("/")
524                .body(())
525                .unwrap()
526                .into_parts()
527                .0,
528            Vec::new(),
529        )
530        .await
531        .unwrap();
532
533        let response = router.route_request(req).await;
534        assert_eq!(response.body_bytes(), b"Home");
535    }
536}