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/// Insert a route into a matchit tree, surfacing conflicts instead of silently
38/// dropping them (WR-02).
39///
40/// matchit rejects a second route that conflicts at the same tree position
41/// (e.g. two distinct all-param patterns sharing a slot, like `/{a}/{b}` and
42/// `/{c}/{d}`). The previous `.insert(...).ok()` discarded that rejection, so a
43/// conflicting registration produced no route and no diagnostic at boot — a
44/// fail-open-shaped surprise for a security-relevant write surface (the route
45/// vanishes rather than denying).
46///
47/// This keeps the success path byte-for-byte identical (the route is inserted)
48/// and only makes a CONFLICT loud: a `tracing::error!` naming the dropped path
49/// and method. It does not panic, so legitimate idempotent re-registration of
50/// the same pattern (which matchit reports as a conflict) stays non-fatal while
51/// still being visible in logs.
52fn insert_route<V>(router: &mut MatchitRouter<V>, method: &str, path: &str, value: V) {
53    if let Err(err) = router.insert(path, value) {
54        tracing::error!(
55            method = %method,
56            path = %path,
57            error = %err,
58            "route registration conflict: matchit rejected this route; \
59             it will NOT be reachable. A conflicting pattern is already \
60             registered at the same tree position."
61        );
62    }
63}
64
65/// Register a route for introspection
66fn register_route(method: &str, path: &str) {
67    let registry = REGISTERED_ROUTES.get_or_init(|| RwLock::new(Vec::new()));
68    if let Ok(mut routes) = registry.write() {
69        routes.push(RouteInfo {
70            method: method.to_string(),
71            path: path.to_string(),
72            name: None,
73            middleware: Vec::new(),
74            mcp_tool_name: None,
75            mcp_description: None,
76            mcp_hint: None,
77            mcp_hidden: false,
78        });
79    }
80}
81
82/// Update the most recently registered route with its name
83fn update_route_name(path: &str, name: &str) {
84    let registry = REGISTERED_ROUTES.get_or_init(|| RwLock::new(Vec::new()));
85    if let Ok(mut routes) = registry.write() {
86        // Find the most recent route with this path and update its name
87        if let Some(route) = routes.iter_mut().rev().find(|r| r.path == path) {
88            route.name = Some(name.to_string());
89        }
90    }
91}
92
93/// Update a route with middleware name
94fn update_route_middleware(path: &str, middleware_name: &str) {
95    let registry = REGISTERED_ROUTES.get_or_init(|| RwLock::new(Vec::new()));
96    if let Ok(mut routes) = registry.write() {
97        // Find the most recent route with this path and add middleware
98        if let Some(route) = routes.iter_mut().rev().find(|r| r.path == path) {
99            route.middleware.push(middleware_name.to_string());
100        }
101    }
102}
103
104/// Update a route with MCP metadata overrides
105pub(crate) fn update_route_mcp(
106    path: &str,
107    tool_name: Option<String>,
108    description: Option<String>,
109    hint: Option<String>,
110    hidden: bool,
111) {
112    let registry = REGISTERED_ROUTES.get_or_init(|| RwLock::new(Vec::new()));
113    if let Ok(mut routes) = registry.write() {
114        if let Some(route) = routes.iter_mut().rev().find(|r| r.path == path) {
115            route.mcp_tool_name = tool_name;
116            route.mcp_description = description;
117            route.mcp_hint = hint;
118            route.mcp_hidden = hidden;
119        }
120    }
121}
122
123/// Get all registered routes for introspection
124pub fn get_registered_routes() -> Vec<RouteInfo> {
125    REGISTERED_ROUTES
126        .get()
127        .and_then(|r| r.read().ok())
128        .map(|routes| routes.clone())
129        .unwrap_or_default()
130}
131
132/// Register a route name -> path mapping
133pub fn register_route_name(name: &str, path: &str) {
134    let registry = ROUTE_REGISTRY.get_or_init(|| RwLock::new(HashMap::new()));
135    if let Ok(mut map) = registry.write() {
136        map.insert(name.to_string(), path.to_string());
137    }
138    // Also update the introspection registry
139    update_route_name(path, name);
140}
141
142/// Generate a URL for a named route with parameters
143///
144/// # Arguments
145/// * `name` - The route name (e.g., "users.show")
146/// * `params` - Slice of (key, value) tuples for path parameters
147///
148/// # Returns
149/// * `Some(String)` - The generated URL with parameters substituted
150/// * `None` - If the route name is not found
151///
152/// # Example
153/// ```ignore
154/// let url = route("users.show", &[("id", "123")]);
155/// assert_eq!(url, Some("/users/123".to_string()));
156/// ```
157pub fn route(name: &str, params: &[(&str, &str)]) -> Option<String> {
158    let registry = ROUTE_REGISTRY.get()?.read().ok()?;
159    let path_pattern = registry.get(name)?;
160
161    let mut url = path_pattern.clone();
162    for (key, value) in params {
163        url = url.replace(&format!("{{{key}}}"), value);
164    }
165    Some(url)
166}
167
168/// Generate URL with HashMap parameters (used internally by Redirect)
169pub fn route_with_params(name: &str, params: &HashMap<String, String>) -> Option<String> {
170    let registry = ROUTE_REGISTRY.get()?.read().ok()?;
171    let path_pattern = registry.get(name)?;
172
173    let mut url = path_pattern.clone();
174    for (key, value) in params {
175        url = url.replace(&format!("{{{key}}}"), value);
176    }
177    Some(url)
178}
179
180/// HTTP method for tracking the last registered route
181#[derive(Clone, Copy)]
182enum Method {
183    Get,
184    Post,
185    Put,
186    Patch,
187    Delete,
188}
189
190/// Type alias for route handlers
191pub type BoxedHandler =
192    Box<dyn Fn(Request) -> Pin<Box<dyn Future<Output = Response> + Send>> + Send + Sync>;
193
194/// Value stored in the router: handler + pattern for metrics
195type RouteValue = (Arc<BoxedHandler>, String);
196
197/// HTTP Router with Laravel-like route registration
198pub struct Router {
199    get_routes: MatchitRouter<RouteValue>,
200    post_routes: MatchitRouter<RouteValue>,
201    put_routes: MatchitRouter<RouteValue>,
202    patch_routes: MatchitRouter<RouteValue>,
203    delete_routes: MatchitRouter<RouteValue>,
204    /// Middleware assignments: path -> boxed middleware instances
205    route_middleware: HashMap<String, Vec<BoxedMiddleware>>,
206    /// Fallback handler for when no routes match (overrides default 404)
207    fallback_handler: Option<Arc<BoxedHandler>>,
208    /// Middleware for the fallback route
209    fallback_middleware: Vec<BoxedMiddleware>,
210}
211
212impl Router {
213    /// Create an empty router with no routes registered.
214    pub fn new() -> Self {
215        Self {
216            get_routes: MatchitRouter::new(),
217            post_routes: MatchitRouter::new(),
218            put_routes: MatchitRouter::new(),
219            patch_routes: MatchitRouter::new(),
220            delete_routes: MatchitRouter::new(),
221            route_middleware: HashMap::new(),
222            fallback_handler: None,
223            fallback_middleware: Vec::new(),
224        }
225    }
226
227    /// Get middleware for a specific route path
228    pub fn get_route_middleware(&self, path: &str) -> Vec<BoxedMiddleware> {
229        self.route_middleware.get(path).cloned().unwrap_or_default()
230    }
231
232    /// Register middleware for a path (internal use)
233    pub(crate) fn add_middleware(&mut self, path: &str, middleware: BoxedMiddleware) {
234        self.route_middleware
235            .entry(path.to_string())
236            .or_default()
237            .push(middleware);
238    }
239
240    /// Set the fallback handler for when no routes match
241    pub(crate) fn set_fallback(&mut self, handler: Arc<BoxedHandler>) {
242        self.fallback_handler = Some(handler);
243    }
244
245    /// Add middleware to the fallback route
246    pub(crate) fn add_fallback_middleware(&mut self, middleware: BoxedMiddleware) {
247        self.fallback_middleware.push(middleware);
248    }
249
250    /// Get the fallback handler and its middleware
251    pub fn get_fallback(&self) -> Option<(Arc<BoxedHandler>, Vec<BoxedMiddleware>)> {
252        self.fallback_handler
253            .as_ref()
254            .map(|h| (h.clone(), self.fallback_middleware.clone()))
255    }
256
257    /// Insert a GET route with a pre-boxed handler (internal use for groups)
258    pub(crate) fn insert_get(&mut self, path: &str, handler: Arc<BoxedHandler>) {
259        insert_route(
260            &mut self.get_routes,
261            "GET",
262            path,
263            (handler, path.to_string()),
264        );
265        register_route("GET", path);
266    }
267
268    /// Insert a POST route with a pre-boxed handler (internal use for groups)
269    pub(crate) fn insert_post(&mut self, path: &str, handler: Arc<BoxedHandler>) {
270        insert_route(
271            &mut self.post_routes,
272            "POST",
273            path,
274            (handler, path.to_string()),
275        );
276        register_route("POST", path);
277    }
278
279    /// Insert a PUT route with a pre-boxed handler (internal use for groups)
280    pub(crate) fn insert_put(&mut self, path: &str, handler: Arc<BoxedHandler>) {
281        insert_route(
282            &mut self.put_routes,
283            "PUT",
284            path,
285            (handler, path.to_string()),
286        );
287        register_route("PUT", path);
288    }
289
290    /// Insert a PATCH route with a pre-boxed handler (internal use for groups)
291    pub(crate) fn insert_patch(&mut self, path: &str, handler: Arc<BoxedHandler>) {
292        insert_route(
293            &mut self.patch_routes,
294            "PATCH",
295            path,
296            (handler, path.to_string()),
297        );
298        register_route("PATCH", path);
299    }
300
301    /// Insert a DELETE route with a pre-boxed handler (internal use for groups)
302    pub(crate) fn insert_delete(&mut self, path: &str, handler: Arc<BoxedHandler>) {
303        insert_route(
304            &mut self.delete_routes,
305            "DELETE",
306            path,
307            (handler, path.to_string()),
308        );
309        register_route("DELETE", path);
310    }
311
312    /// Insert a GET route alias pointing at the same handler as a previously
313    /// registered canonical route. Skips `register_route` so `RouteInfo` and
314    /// `get_registered_routes()` stay canonical (D-07). The stored matchit
315    /// value carries the CANONICAL pattern string so middleware lookup in
316    /// `server.rs` (keyed by `route_pattern`) resolves to the canonical
317    /// `add_middleware` key regardless of which variant matched.
318    pub(crate) fn insert_get_alias(
319        &mut self,
320        alias_path: &str,
321        handler: Arc<BoxedHandler>,
322        canonical_path: &str,
323    ) {
324        self.get_routes
325            .insert(alias_path, (handler, canonical_path.to_string()))
326            .ok();
327    }
328
329    /// Insert a POST route alias pointing at the same handler as a previously
330    /// registered canonical route. See `insert_get_alias` for the invariants.
331    pub(crate) fn insert_post_alias(
332        &mut self,
333        alias_path: &str,
334        handler: Arc<BoxedHandler>,
335        canonical_path: &str,
336    ) {
337        self.post_routes
338            .insert(alias_path, (handler, canonical_path.to_string()))
339            .ok();
340    }
341
342    /// Insert a PUT route alias pointing at the same handler as a previously
343    /// registered canonical route. See `insert_get_alias` for the invariants.
344    pub(crate) fn insert_put_alias(
345        &mut self,
346        alias_path: &str,
347        handler: Arc<BoxedHandler>,
348        canonical_path: &str,
349    ) {
350        self.put_routes
351            .insert(alias_path, (handler, canonical_path.to_string()))
352            .ok();
353    }
354
355    /// Insert a PATCH route alias pointing at the same handler as a previously
356    /// registered canonical route. See `insert_get_alias` for the invariants.
357    pub(crate) fn insert_patch_alias(
358        &mut self,
359        alias_path: &str,
360        handler: Arc<BoxedHandler>,
361        canonical_path: &str,
362    ) {
363        self.patch_routes
364            .insert(alias_path, (handler, canonical_path.to_string()))
365            .ok();
366    }
367
368    /// Insert a DELETE route alias pointing at the same handler as a previously
369    /// registered canonical route. See `insert_get_alias` for the invariants.
370    pub(crate) fn insert_delete_alias(
371        &mut self,
372        alias_path: &str,
373        handler: Arc<BoxedHandler>,
374        canonical_path: &str,
375    ) {
376        self.delete_routes
377            .insert(alias_path, (handler, canonical_path.to_string()))
378            .ok();
379    }
380
381    /// Register a GET route
382    pub fn get<H, Fut>(mut self, path: &str, handler: H) -> RouteBuilder
383    where
384        H: Fn(Request) -> Fut + Send + Sync + 'static,
385        Fut: Future<Output = Response> + Send + 'static,
386    {
387        let handler: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
388        insert_route(
389            &mut self.get_routes,
390            "GET",
391            path,
392            (Arc::new(handler), path.to_string()),
393        );
394        register_route("GET", path);
395        RouteBuilder {
396            router: self,
397            last_path: path.to_string(),
398            _last_method: Method::Get,
399        }
400    }
401
402    /// Register a POST route
403    pub fn post<H, Fut>(mut self, path: &str, handler: H) -> RouteBuilder
404    where
405        H: Fn(Request) -> Fut + Send + Sync + 'static,
406        Fut: Future<Output = Response> + Send + 'static,
407    {
408        let handler: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
409        insert_route(
410            &mut self.post_routes,
411            "POST",
412            path,
413            (Arc::new(handler), path.to_string()),
414        );
415        register_route("POST", path);
416        RouteBuilder {
417            router: self,
418            last_path: path.to_string(),
419            _last_method: Method::Post,
420        }
421    }
422
423    /// Register a PUT route
424    pub fn put<H, Fut>(mut self, path: &str, handler: H) -> RouteBuilder
425    where
426        H: Fn(Request) -> Fut + Send + Sync + 'static,
427        Fut: Future<Output = Response> + Send + 'static,
428    {
429        let handler: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
430        insert_route(
431            &mut self.put_routes,
432            "PUT",
433            path,
434            (Arc::new(handler), path.to_string()),
435        );
436        register_route("PUT", path);
437        RouteBuilder {
438            router: self,
439            last_path: path.to_string(),
440            _last_method: Method::Put,
441        }
442    }
443
444    /// Register a PATCH route
445    pub fn patch<H, Fut>(mut self, path: &str, handler: H) -> RouteBuilder
446    where
447        H: Fn(Request) -> Fut + Send + Sync + 'static,
448        Fut: Future<Output = Response> + Send + 'static,
449    {
450        let handler: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
451        insert_route(
452            &mut self.patch_routes,
453            "PATCH",
454            path,
455            (Arc::new(handler), path.to_string()),
456        );
457        register_route("PATCH", path);
458        RouteBuilder {
459            router: self,
460            last_path: path.to_string(),
461            _last_method: Method::Patch,
462        }
463    }
464
465    /// Register a DELETE route
466    pub fn delete<H, Fut>(mut self, path: &str, handler: H) -> RouteBuilder
467    where
468        H: Fn(Request) -> Fut + Send + Sync + 'static,
469        Fut: Future<Output = Response> + Send + 'static,
470    {
471        let handler: BoxedHandler = Box::new(move |req| Box::pin(handler(req)));
472        insert_route(
473            &mut self.delete_routes,
474            "DELETE",
475            path,
476            (Arc::new(handler), path.to_string()),
477        );
478        register_route("DELETE", path);
479        RouteBuilder {
480            router: self,
481            last_path: path.to_string(),
482            _last_method: Method::Delete,
483        }
484    }
485
486    /// Match a request and return the handler with extracted params and route pattern
487    ///
488    /// Returns (handler, params, route_pattern) where route_pattern is the original
489    /// pattern like "/users/{id}" for metrics grouping.
490    ///
491    /// OPTIONS requests are dispatched through `match_preflight` (private helper):
492    /// any path registered under any other verb returns a synthetic 204 handler so
493    /// route-level middleware (CORS in particular) still runs. The CORS middleware
494    /// then short-circuits the preflight with the configured ACAO / ACAH / ACAM
495    /// headers. Without this, OPTIONS would 404 before the middleware chain ran.
496    pub fn match_route(
497        &self,
498        method: &hyper::Method,
499        path: &str,
500    ) -> Option<(Arc<BoxedHandler>, HashMap<String, String>, String)> {
501        let router = match *method {
502            hyper::Method::GET => &self.get_routes,
503            hyper::Method::POST => &self.post_routes,
504            hyper::Method::PUT => &self.put_routes,
505            hyper::Method::PATCH => &self.patch_routes,
506            hyper::Method::DELETE => &self.delete_routes,
507            hyper::Method::OPTIONS => return self.match_preflight(path),
508            _ => return None,
509        };
510
511        router.at(path).ok().map(|matched| {
512            let params: HashMap<String, String> = matched
513                .params
514                .iter()
515                .map(|(k, v)| (k.to_string(), v.to_string()))
516                .collect();
517            let (handler, pattern) = matched.value.clone();
518            (handler, params, pattern)
519        })
520    }
521
522    /// Synthesize a 204 handler for an OPTIONS preflight when any verb matches the path.
523    ///
524    /// Scans every method table for the path; the first match wins. The returned
525    /// pattern is the canonical pattern that match_route would have returned for the
526    /// matching verb, so server.rs resolves the same route-level middleware (CORS,
527    /// auth, etc.) as the live verb would. The synthetic handler returns 204 No
528    /// Content with an empty body — when CORS middleware sits in the chain it
529    /// short-circuits before the handler runs and applies its preflight headers.
530    fn match_preflight(
531        &self,
532        path: &str,
533    ) -> Option<(Arc<BoxedHandler>, HashMap<String, String>, String)> {
534        let tables = [
535            &self.get_routes,
536            &self.post_routes,
537            &self.put_routes,
538            &self.patch_routes,
539            &self.delete_routes,
540        ];
541        for table in tables {
542            if let Ok(matched) = table.at(path) {
543                let params: HashMap<String, String> = matched
544                    .params
545                    .iter()
546                    .map(|(k, v)| (k.to_string(), v.to_string()))
547                    .collect();
548                let (_, pattern) = matched.value.clone();
549                let handler: BoxedHandler = Box::new(|_req| {
550                    Box::pin(async move { Ok(crate::http::HttpResponse::new().status(204)) })
551                });
552                return Some((Arc::new(handler), params, pattern));
553            }
554        }
555        None
556    }
557}
558
559impl Default for Router {
560    fn default() -> Self {
561        Self::new()
562    }
563}
564
565/// Builder returned after registering a route, enabling .name() chaining
566pub struct RouteBuilder {
567    pub(crate) router: Router,
568    last_path: String,
569    #[allow(dead_code)]
570    _last_method: Method,
571}
572
573impl RouteBuilder {
574    /// Name the most recently registered route
575    pub fn name(self, name: &str) -> Router {
576        register_route_name(name, &self.last_path);
577        self.router
578    }
579
580    /// Apply middleware to the most recently registered route
581    ///
582    /// # Example
583    ///
584    /// ```rust,ignore
585    /// Router::new()
586    ///     .get("/admin", admin_handler).middleware(AuthMiddleware)
587    ///     .get("/api/users", users_handler).middleware(CorsMiddleware)
588    /// ```
589    pub fn middleware<M: Middleware + 'static>(mut self, middleware: M) -> RouteBuilder {
590        // Track middleware name for introspection
591        let type_name = std::any::type_name::<M>();
592        let short_name = type_name.rsplit("::").next().unwrap_or(type_name);
593        update_route_middleware(&self.last_path, short_name);
594
595        self.router
596            .add_middleware(&self.last_path, into_boxed(middleware));
597        self
598    }
599
600    /// Apply pre-boxed middleware to the most recently registered route
601    /// (Used internally by route macros)
602    pub fn middleware_boxed(mut self, middleware: BoxedMiddleware) -> RouteBuilder {
603        // Track middleware (name not available for boxed middleware)
604        update_route_middleware(&self.last_path, "BoxedMiddleware");
605
606        self.router
607            .route_middleware
608            .entry(self.last_path.clone())
609            .or_default()
610            .push(middleware);
611        self
612    }
613
614    /// Register a GET route (for chaining without .name())
615    pub fn get<H, Fut>(self, path: &str, handler: H) -> RouteBuilder
616    where
617        H: Fn(Request) -> Fut + Send + Sync + 'static,
618        Fut: Future<Output = Response> + Send + 'static,
619    {
620        self.router.get(path, handler)
621    }
622
623    /// Register a POST route (for chaining without .name())
624    pub fn post<H, Fut>(self, path: &str, handler: H) -> RouteBuilder
625    where
626        H: Fn(Request) -> Fut + Send + Sync + 'static,
627        Fut: Future<Output = Response> + Send + 'static,
628    {
629        self.router.post(path, handler)
630    }
631
632    /// Register a PUT route (for chaining without .name())
633    pub fn put<H, Fut>(self, path: &str, handler: H) -> RouteBuilder
634    where
635        H: Fn(Request) -> Fut + Send + Sync + 'static,
636        Fut: Future<Output = Response> + Send + 'static,
637    {
638        self.router.put(path, handler)
639    }
640
641    /// Register a PATCH route (for chaining without .name())
642    pub fn patch<H, Fut>(self, path: &str, handler: H) -> RouteBuilder
643    where
644        H: Fn(Request) -> Fut + Send + Sync + 'static,
645        Fut: Future<Output = Response> + Send + 'static,
646    {
647        self.router.patch(path, handler)
648    }
649
650    /// Register a DELETE route (for chaining without .name())
651    pub fn delete<H, Fut>(self, path: &str, handler: H) -> RouteBuilder
652    where
653        H: Fn(Request) -> Fut + Send + Sync + 'static,
654        Fut: Future<Output = Response> + Send + 'static,
655    {
656        self.router.delete(path, handler)
657    }
658}
659
660impl From<RouteBuilder> for Router {
661    fn from(builder: RouteBuilder) -> Self {
662        builder.router
663    }
664}