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}