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    ///
433    /// OPTIONS requests are dispatched through [`match_preflight`](Self::match_preflight):
434    /// any path registered under any other verb returns a synthetic 204 handler so
435    /// route-level middleware (CORS in particular) still runs. The CORS middleware
436    /// then short-circuits the preflight with the configured ACAO / ACAH / ACAM
437    /// headers. Without this, OPTIONS would 404 before the middleware chain ran.
438    pub fn match_route(
439        &self,
440        method: &hyper::Method,
441        path: &str,
442    ) -> Option<(Arc<BoxedHandler>, HashMap<String, String>, String)> {
443        let router = match *method {
444            hyper::Method::GET => &self.get_routes,
445            hyper::Method::POST => &self.post_routes,
446            hyper::Method::PUT => &self.put_routes,
447            hyper::Method::PATCH => &self.patch_routes,
448            hyper::Method::DELETE => &self.delete_routes,
449            hyper::Method::OPTIONS => return self.match_preflight(path),
450            _ => return None,
451        };
452
453        router.at(path).ok().map(|matched| {
454            let params: HashMap<String, String> = matched
455                .params
456                .iter()
457                .map(|(k, v)| (k.to_string(), v.to_string()))
458                .collect();
459            let (handler, pattern) = matched.value.clone();
460            (handler, params, pattern)
461        })
462    }
463
464    /// Synthesize a 204 handler for an OPTIONS preflight when any verb matches the path.
465    ///
466    /// Scans every method table for the path; the first match wins. The returned
467    /// pattern is the canonical pattern that match_route would have returned for the
468    /// matching verb, so server.rs resolves the same route-level middleware (CORS,
469    /// auth, etc.) as the live verb would. The synthetic handler returns 204 No
470    /// Content with an empty body — when CORS middleware sits in the chain it
471    /// short-circuits before the handler runs and applies its preflight headers.
472    fn match_preflight(
473        &self,
474        path: &str,
475    ) -> Option<(Arc<BoxedHandler>, HashMap<String, String>, String)> {
476        let tables = [
477            &self.get_routes,
478            &self.post_routes,
479            &self.put_routes,
480            &self.patch_routes,
481            &self.delete_routes,
482        ];
483        for table in tables {
484            if let Ok(matched) = table.at(path) {
485                let params: HashMap<String, String> = matched
486                    .params
487                    .iter()
488                    .map(|(k, v)| (k.to_string(), v.to_string()))
489                    .collect();
490                let (_, pattern) = matched.value.clone();
491                let handler: BoxedHandler = Box::new(|_req| {
492                    Box::pin(async move { Ok(crate::http::HttpResponse::new().status(204)) })
493                });
494                return Some((Arc::new(handler), params, pattern));
495            }
496        }
497        None
498    }
499}
500
501impl Default for Router {
502    fn default() -> Self {
503        Self::new()
504    }
505}
506
507/// Builder returned after registering a route, enabling .name() chaining
508pub struct RouteBuilder {
509    pub(crate) router: Router,
510    last_path: String,
511    #[allow(dead_code)]
512    _last_method: Method,
513}
514
515impl RouteBuilder {
516    /// Name the most recently registered route
517    pub fn name(self, name: &str) -> Router {
518        register_route_name(name, &self.last_path);
519        self.router
520    }
521
522    /// Apply middleware to the most recently registered route
523    ///
524    /// # Example
525    ///
526    /// ```rust,ignore
527    /// Router::new()
528    ///     .get("/admin", admin_handler).middleware(AuthMiddleware)
529    ///     .get("/api/users", users_handler).middleware(CorsMiddleware)
530    /// ```
531    pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> RouteBuilder {
532        // Track middleware name for introspection
533        let type_name = std::any::type_name::<M>();
534        let short_name = type_name.rsplit("::").next().unwrap_or(type_name);
535        update_route_middleware(&self.last_path, short_name);
536
537        self.router
538            .add_middleware(&self.last_path, into_boxed(middleware));
539        self
540    }
541
542    /// Apply pre-boxed middleware to the most recently registered route
543    /// (Used internally by route macros)
544    pub fn middleware_boxed(mut self, middleware: BoxedMiddleware) -> RouteBuilder {
545        // Track middleware (name not available for boxed middleware)
546        update_route_middleware(&self.last_path, "BoxedMiddleware");
547
548        self.router
549            .route_middleware
550            .entry(self.last_path.clone())
551            .or_default()
552            .push(middleware);
553        self
554    }
555
556    /// Register a GET route (for chaining without .name())
557    pub fn get<H, Fut>(self, path: &str, handler: H) -> RouteBuilder
558    where
559        H: Fn(Request) -> Fut + Send + Sync + 'static,
560        Fut: Future<Output = Response> + Send + 'static,
561    {
562        self.router.get(path, handler)
563    }
564
565    /// Register a POST route (for chaining without .name())
566    pub fn post<H, Fut>(self, path: &str, handler: H) -> RouteBuilder
567    where
568        H: Fn(Request) -> Fut + Send + Sync + 'static,
569        Fut: Future<Output = Response> + Send + 'static,
570    {
571        self.router.post(path, handler)
572    }
573
574    /// Register a PUT route (for chaining without .name())
575    pub fn put<H, Fut>(self, path: &str, handler: H) -> RouteBuilder
576    where
577        H: Fn(Request) -> Fut + Send + Sync + 'static,
578        Fut: Future<Output = Response> + Send + 'static,
579    {
580        self.router.put(path, handler)
581    }
582
583    /// Register a PATCH route (for chaining without .name())
584    pub fn patch<H, Fut>(self, path: &str, handler: H) -> RouteBuilder
585    where
586        H: Fn(Request) -> Fut + Send + Sync + 'static,
587        Fut: Future<Output = Response> + Send + 'static,
588    {
589        self.router.patch(path, handler)
590    }
591
592    /// Register a DELETE route (for chaining without .name())
593    pub fn delete<H, Fut>(self, path: &str, handler: H) -> RouteBuilder
594    where
595        H: Fn(Request) -> Fut + Send + Sync + 'static,
596        Fut: Future<Output = Response> + Send + 'static,
597    {
598        self.router.delete(path, handler)
599    }
600}
601
602impl From<RouteBuilder> for Router {
603    fn from(builder: RouteBuilder) -> Self {
604        builder.router
605    }
606}