Skip to main content

ferro_rs/routing/
router.rs

1use crate::http::{Request, Response};
2use crate::middleware::{into_boxed, BoxedMiddleware, Middleware};
3use matchit::Router as MatchitRouter;
4use serde::Serialize;
5use std::collections::HashMap;
6use std::future::Future;
7use std::pin::Pin;
8use std::sync::{Arc, OnceLock, RwLock};
9
10/// Global registry mapping route names to path patterns
11static ROUTE_REGISTRY: OnceLock<RwLock<HashMap<String, String>>> = OnceLock::new();
12
13/// Global registry of all registered routes for introspection
14static REGISTERED_ROUTES: OnceLock<RwLock<Vec<RouteInfo>>> = OnceLock::new();
15
16/// Information about a registered route for introspection
17#[derive(Debug, Clone, Default, Serialize)]
18pub struct RouteInfo {
19    /// HTTP method (GET, POST, PUT, DELETE)
20    pub method: String,
21    /// Route path pattern (e.g., "/users/{id}")
22    pub path: String,
23    /// Optional route name (e.g., "users.show")
24    pub name: Option<String>,
25    /// Middleware applied to this route
26    pub middleware: Vec<String>,
27    /// Override for auto-generated MCP tool name
28    pub mcp_tool_name: Option<String>,
29    /// Override for auto-generated MCP description
30    pub mcp_description: Option<String>,
31    /// Hint text appended to MCP description for AI agent guidance
32    pub mcp_hint: Option<String>,
33    /// When true, route is hidden from MCP tool discovery
34    pub mcp_hidden: bool,
35}
36
37/// Register a route for introspection
38fn register_route(method: &str, path: &str) {
39    let registry = REGISTERED_ROUTES.get_or_init(|| RwLock::new(Vec::new()));
40    if let Ok(mut routes) = registry.write() {
41        routes.push(RouteInfo {
42            method: method.to_string(),
43            path: path.to_string(),
44            name: None,
45            middleware: Vec::new(),
46            mcp_tool_name: None,
47            mcp_description: None,
48            mcp_hint: None,
49            mcp_hidden: false,
50        });
51    }
52}
53
54/// Update the most recently registered route with its name
55fn update_route_name(path: &str, name: &str) {
56    let registry = REGISTERED_ROUTES.get_or_init(|| RwLock::new(Vec::new()));
57    if let Ok(mut routes) = registry.write() {
58        // Find the most recent route with this path and update its name
59        if let Some(route) = routes.iter_mut().rev().find(|r| r.path == path) {
60            route.name = Some(name.to_string());
61        }
62    }
63}
64
65/// Update a route with middleware name
66fn update_route_middleware(path: &str, middleware_name: &str) {
67    let registry = REGISTERED_ROUTES.get_or_init(|| RwLock::new(Vec::new()));
68    if let Ok(mut routes) = registry.write() {
69        // Find the most recent route with this path and add middleware
70        if let Some(route) = routes.iter_mut().rev().find(|r| r.path == path) {
71            route.middleware.push(middleware_name.to_string());
72        }
73    }
74}
75
76/// Update a route with MCP metadata overrides
77pub(crate) fn update_route_mcp(
78    path: &str,
79    tool_name: Option<String>,
80    description: Option<String>,
81    hint: Option<String>,
82    hidden: bool,
83) {
84    let registry = REGISTERED_ROUTES.get_or_init(|| RwLock::new(Vec::new()));
85    if let Ok(mut routes) = registry.write() {
86        if let Some(route) = routes.iter_mut().rev().find(|r| r.path == path) {
87            route.mcp_tool_name = tool_name;
88            route.mcp_description = description;
89            route.mcp_hint = hint;
90            route.mcp_hidden = hidden;
91        }
92    }
93}
94
95/// Get all registered routes for introspection
96pub fn get_registered_routes() -> Vec<RouteInfo> {
97    REGISTERED_ROUTES
98        .get()
99        .and_then(|r| r.read().ok())
100        .map(|routes| routes.clone())
101        .unwrap_or_default()
102}
103
104/// Register a route name -> path mapping
105pub fn register_route_name(name: &str, path: &str) {
106    let registry = ROUTE_REGISTRY.get_or_init(|| RwLock::new(HashMap::new()));
107    if let Ok(mut map) = registry.write() {
108        map.insert(name.to_string(), path.to_string());
109    }
110    // Also update the introspection registry
111    update_route_name(path, name);
112}
113
114/// Generate a URL for a named route with parameters
115///
116/// # Arguments
117/// * `name` - The route name (e.g., "users.show")
118/// * `params` - Slice of (key, value) tuples for path parameters
119///
120/// # Returns
121/// * `Some(String)` - The generated URL with parameters substituted
122/// * `None` - If the route name is not found
123///
124/// # Example
125/// ```ignore
126/// let url = route("users.show", &[("id", "123")]);
127/// assert_eq!(url, Some("/users/123".to_string()));
128/// ```
129pub fn route(name: &str, params: &[(&str, &str)]) -> Option<String> {
130    let registry = ROUTE_REGISTRY.get()?.read().ok()?;
131    let path_pattern = registry.get(name)?;
132
133    let mut url = path_pattern.clone();
134    for (key, value) in params {
135        url = url.replace(&format!("{{{key}}}"), value);
136    }
137    Some(url)
138}
139
140/// Generate URL with HashMap parameters (used internally by Redirect)
141pub fn route_with_params(name: &str, params: &HashMap<String, String>) -> Option<String> {
142    let registry = ROUTE_REGISTRY.get()?.read().ok()?;
143    let path_pattern = registry.get(name)?;
144
145    let mut url = path_pattern.clone();
146    for (key, value) in params {
147        url = url.replace(&format!("{{{key}}}"), value);
148    }
149    Some(url)
150}
151
152/// HTTP method for tracking the last registered route
153#[derive(Clone, Copy)]
154enum Method {
155    Get,
156    Post,
157    Put,
158    Patch,
159    Delete,
160}
161
162/// Type alias for route handlers
163pub type BoxedHandler =
164    Box<dyn Fn(Request) -> Pin<Box<dyn Future<Output = Response> + Send>> + Send + Sync>;
165
166/// Value stored in the router: handler + pattern for metrics
167type RouteValue = (Arc<BoxedHandler>, String);
168
169/// HTTP Router with Laravel-like route registration
170pub struct Router {
171    get_routes: MatchitRouter<RouteValue>,
172    post_routes: MatchitRouter<RouteValue>,
173    put_routes: MatchitRouter<RouteValue>,
174    patch_routes: MatchitRouter<RouteValue>,
175    delete_routes: MatchitRouter<RouteValue>,
176    /// Middleware assignments: path -> boxed middleware instances
177    route_middleware: HashMap<String, Vec<BoxedMiddleware>>,
178    /// Fallback handler for when no routes match (overrides default 404)
179    fallback_handler: Option<Arc<BoxedHandler>>,
180    /// Middleware for the fallback route
181    fallback_middleware: Vec<BoxedMiddleware>,
182}
183
184impl Router {
185    /// Create an empty router with no routes registered.
186    pub fn new() -> Self {
187        Self {
188            get_routes: MatchitRouter::new(),
189            post_routes: MatchitRouter::new(),
190            put_routes: MatchitRouter::new(),
191            patch_routes: MatchitRouter::new(),
192            delete_routes: MatchitRouter::new(),
193            route_middleware: HashMap::new(),
194            fallback_handler: None,
195            fallback_middleware: Vec::new(),
196        }
197    }
198
199    /// Get middleware for a specific route path
200    pub fn get_route_middleware(&self, path: &str) -> Vec<BoxedMiddleware> {
201        self.route_middleware.get(path).cloned().unwrap_or_default()
202    }
203
204    /// Register middleware for a path (internal use)
205    pub(crate) fn add_middleware(&mut self, path: &str, middleware: BoxedMiddleware) {
206        self.route_middleware
207            .entry(path.to_string())
208            .or_default()
209            .push(middleware);
210    }
211
212    /// Set the fallback handler for when no routes match
213    pub(crate) fn set_fallback(&mut self, handler: Arc<BoxedHandler>) {
214        self.fallback_handler = Some(handler);
215    }
216
217    /// Add middleware to the fallback route
218    pub(crate) fn add_fallback_middleware(&mut self, middleware: BoxedMiddleware) {
219        self.fallback_middleware.push(middleware);
220    }
221
222    /// Get the fallback handler and its middleware
223    pub fn get_fallback(&self) -> Option<(Arc<BoxedHandler>, Vec<BoxedMiddleware>)> {
224        self.fallback_handler
225            .as_ref()
226            .map(|h| (h.clone(), self.fallback_middleware.clone()))
227    }
228
229    /// Insert a GET route with a pre-boxed handler (internal use for groups)
230    pub(crate) fn insert_get(&mut self, path: &str, handler: Arc<BoxedHandler>) {
231        self.get_routes
232            .insert(path, (handler, path.to_string()))
233            .ok();
234        register_route("GET", path);
235    }
236
237    /// Insert a POST route with a pre-boxed handler (internal use for groups)
238    pub(crate) fn insert_post(&mut self, path: &str, handler: Arc<BoxedHandler>) {
239        self.post_routes
240            .insert(path, (handler, path.to_string()))
241            .ok();
242        register_route("POST", path);
243    }
244
245    /// Insert a PUT route with a pre-boxed handler (internal use for groups)
246    pub(crate) fn insert_put(&mut self, path: &str, handler: Arc<BoxedHandler>) {
247        self.put_routes
248            .insert(path, (handler, path.to_string()))
249            .ok();
250        register_route("PUT", path);
251    }
252
253    /// Insert a PATCH route with a pre-boxed handler (internal use for groups)
254    pub(crate) fn insert_patch(&mut self, path: &str, handler: Arc<BoxedHandler>) {
255        self.patch_routes
256            .insert(path, (handler, path.to_string()))
257            .ok();
258        register_route("PATCH", path);
259    }
260
261    /// Insert a DELETE route with a pre-boxed handler (internal use for groups)
262    pub(crate) fn insert_delete(&mut self, path: &str, handler: Arc<BoxedHandler>) {
263        self.delete_routes
264            .insert(path, (handler, path.to_string()))
265            .ok();
266        register_route("DELETE", path);
267    }
268
269    /// Insert a GET route alias pointing at the same handler as a previously
270    /// registered canonical route. Skips `register_route` so `RouteInfo` and
271    /// `get_registered_routes()` stay canonical (D-07). The stored matchit
272    /// value carries the CANONICAL pattern string so middleware lookup in
273    /// `server.rs` (keyed by `route_pattern`) resolves to the canonical
274    /// `add_middleware` key regardless of which variant matched.
275    pub(crate) fn insert_get_alias(
276        &mut self,
277        alias_path: &str,
278        handler: Arc<BoxedHandler>,
279        canonical_path: &str,
280    ) {
281        self.get_routes
282            .insert(alias_path, (handler, canonical_path.to_string()))
283            .ok();
284    }
285
286    /// Insert a POST route alias pointing at the same handler as a previously
287    /// registered canonical route. See `insert_get_alias` for the invariants.
288    pub(crate) fn insert_post_alias(
289        &mut self,
290        alias_path: &str,
291        handler: Arc<BoxedHandler>,
292        canonical_path: &str,
293    ) {
294        self.post_routes
295            .insert(alias_path, (handler, canonical_path.to_string()))
296            .ok();
297    }
298
299    /// Insert a PUT route alias pointing at the same handler as a previously
300    /// registered canonical route. See `insert_get_alias` for the invariants.
301    pub(crate) fn insert_put_alias(
302        &mut self,
303        alias_path: &str,
304        handler: Arc<BoxedHandler>,
305        canonical_path: &str,
306    ) {
307        self.put_routes
308            .insert(alias_path, (handler, canonical_path.to_string()))
309            .ok();
310    }
311
312    /// Insert a PATCH route alias pointing at the same handler as a previously
313    /// registered canonical route. See `insert_get_alias` for the invariants.
314    pub(crate) fn insert_patch_alias(
315        &mut self,
316        alias_path: &str,
317        handler: Arc<BoxedHandler>,
318        canonical_path: &str,
319    ) {
320        self.patch_routes
321            .insert(alias_path, (handler, canonical_path.to_string()))
322            .ok();
323    }
324
325    /// Insert a DELETE route alias pointing at the same handler as a previously
326    /// registered canonical route. See `insert_get_alias` for the invariants.
327    pub(crate) fn insert_delete_alias(
328        &mut self,
329        alias_path: &str,
330        handler: Arc<BoxedHandler>,
331        canonical_path: &str,
332    ) {
333        self.delete_routes
334            .insert(alias_path, (handler, canonical_path.to_string()))
335            .ok();
336    }
337
338    /// Register a GET route
339    pub fn get<H, Fut>(mut self, path: &str, handler: H) -> RouteBuilder
340    where
341        H: Fn(Request) -> Fut + Send + Sync + 'static,
342        Fut: Future<Output = Response> + Send + 'static,
343    {
344        let handler: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
345        self.get_routes
346            .insert(path, (Arc::new(handler), path.to_string()))
347            .ok();
348        register_route("GET", path);
349        RouteBuilder {
350            router: self,
351            last_path: path.to_string(),
352            _last_method: Method::Get,
353        }
354    }
355
356    /// Register a POST route
357    pub fn post<H, Fut>(mut self, path: &str, handler: H) -> RouteBuilder
358    where
359        H: Fn(Request) -> Fut + Send + Sync + 'static,
360        Fut: Future<Output = Response> + Send + 'static,
361    {
362        let handler: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
363        self.post_routes
364            .insert(path, (Arc::new(handler), path.to_string()))
365            .ok();
366        register_route("POST", path);
367        RouteBuilder {
368            router: self,
369            last_path: path.to_string(),
370            _last_method: Method::Post,
371        }
372    }
373
374    /// Register a PUT route
375    pub fn put<H, Fut>(mut self, path: &str, handler: H) -> RouteBuilder
376    where
377        H: Fn(Request) -> Fut + Send + Sync + 'static,
378        Fut: Future<Output = Response> + Send + 'static,
379    {
380        let handler: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
381        self.put_routes
382            .insert(path, (Arc::new(handler), path.to_string()))
383            .ok();
384        register_route("PUT", path);
385        RouteBuilder {
386            router: self,
387            last_path: path.to_string(),
388            _last_method: Method::Put,
389        }
390    }
391
392    /// Register a PATCH route
393    pub fn patch<H, Fut>(mut self, path: &str, handler: H) -> RouteBuilder
394    where
395        H: Fn(Request) -> Fut + Send + Sync + 'static,
396        Fut: Future<Output = Response> + Send + 'static,
397    {
398        let handler: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
399        self.patch_routes
400            .insert(path, (Arc::new(handler), path.to_string()))
401            .ok();
402        register_route("PATCH", path);
403        RouteBuilder {
404            router: self,
405            last_path: path.to_string(),
406            _last_method: Method::Patch,
407        }
408    }
409
410    /// Register a DELETE route
411    pub fn delete<H, Fut>(mut self, path: &str, handler: H) -> RouteBuilder
412    where
413        H: Fn(Request) -> Fut + Send + Sync + 'static,
414        Fut: Future<Output = Response> + Send + 'static,
415    {
416        let handler: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
417        self.delete_routes
418            .insert(path, (Arc::new(handler), path.to_string()))
419            .ok();
420        register_route("DELETE", path);
421        RouteBuilder {
422            router: self,
423            last_path: path.to_string(),
424            _last_method: Method::Delete,
425        }
426    }
427
428    /// Match a request and return the handler with extracted params and route pattern
429    ///
430    /// Returns (handler, params, route_pattern) where route_pattern is the original
431    /// pattern like "/users/{id}" for metrics grouping.
432    pub fn match_route(
433        &self,
434        method: &hyper::Method,
435        path: &str,
436    ) -> Option<(Arc<BoxedHandler>, HashMap<String, String>, String)> {
437        let router = match *method {
438            hyper::Method::GET => &self.get_routes,
439            hyper::Method::POST => &self.post_routes,
440            hyper::Method::PUT => &self.put_routes,
441            hyper::Method::PATCH => &self.patch_routes,
442            hyper::Method::DELETE => &self.delete_routes,
443            _ => return None,
444        };
445
446        router.at(path).ok().map(|matched| {
447            let params: HashMap<String, String> = matched
448                .params
449                .iter()
450                .map(|(k, v)| (k.to_string(), v.to_string()))
451                .collect();
452            let (handler, pattern) = matched.value.clone();
453            (handler, params, pattern)
454        })
455    }
456}
457
458impl Default for Router {
459    fn default() -> Self {
460        Self::new()
461    }
462}
463
464/// Builder returned after registering a route, enabling .name() chaining
465pub struct RouteBuilder {
466    pub(crate) router: Router,
467    last_path: String,
468    #[allow(dead_code)]
469    _last_method: Method,
470}
471
472impl RouteBuilder {
473    /// Name the most recently registered route
474    pub fn name(self, name: &str) -> Router {
475        register_route_name(name, &self.last_path);
476        self.router
477    }
478
479    /// Apply middleware to the most recently registered route
480    ///
481    /// # Example
482    ///
483    /// ```rust,ignore
484    /// Router::new()
485    ///     .get("/admin", admin_handler).middleware(AuthMiddleware)
486    ///     .get("/api/users", users_handler).middleware(CorsMiddleware)
487    /// ```
488    pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> RouteBuilder {
489        // Track middleware name for introspection
490        let type_name = std::any::type_name::<M>();
491        let short_name = type_name.rsplit("::").next().unwrap_or(type_name);
492        update_route_middleware(&self.last_path, short_name);
493
494        self.router
495            .add_middleware(&self.last_path, into_boxed(middleware));
496        self
497    }
498
499    /// Apply pre-boxed middleware to the most recently registered route
500    /// (Used internally by route macros)
501    pub fn middleware_boxed(mut self, middleware: BoxedMiddleware) -> RouteBuilder {
502        // Track middleware (name not available for boxed middleware)
503        update_route_middleware(&self.last_path, "BoxedMiddleware");
504
505        self.router
506            .route_middleware
507            .entry(self.last_path.clone())
508            .or_default()
509            .push(middleware);
510        self
511    }
512
513    /// Register a GET route (for chaining without .name())
514    pub fn get<H, Fut>(self, path: &str, handler: H) -> RouteBuilder
515    where
516        H: Fn(Request) -> Fut + Send + Sync + 'static,
517        Fut: Future<Output = Response> + Send + 'static,
518    {
519        self.router.get(path, handler)
520    }
521
522    /// Register a POST route (for chaining without .name())
523    pub fn post<H, Fut>(self, path: &str, handler: H) -> RouteBuilder
524    where
525        H: Fn(Request) -> Fut + Send + Sync + 'static,
526        Fut: Future<Output = Response> + Send + 'static,
527    {
528        self.router.post(path, handler)
529    }
530
531    /// Register a PUT route (for chaining without .name())
532    pub fn put<H, Fut>(self, path: &str, handler: H) -> RouteBuilder
533    where
534        H: Fn(Request) -> Fut + Send + Sync + 'static,
535        Fut: Future<Output = Response> + Send + 'static,
536    {
537        self.router.put(path, handler)
538    }
539
540    /// Register a PATCH route (for chaining without .name())
541    pub fn patch<H, Fut>(self, path: &str, handler: H) -> RouteBuilder
542    where
543        H: Fn(Request) -> Fut + Send + Sync + 'static,
544        Fut: Future<Output = Response> + Send + 'static,
545    {
546        self.router.patch(path, handler)
547    }
548
549    /// Register a DELETE route (for chaining without .name())
550    pub fn delete<H, Fut>(self, path: &str, handler: H) -> RouteBuilder
551    where
552        H: Fn(Request) -> Fut + Send + Sync + 'static,
553        Fut: Future<Output = Response> + Send + 'static,
554    {
555        self.router.delete(path, handler)
556    }
557}
558
559impl From<RouteBuilder> for Router {
560    fn from(builder: RouteBuilder) -> Self {
561        builder.router
562    }
563}