rustapi_core/
router.rs

1//! Router implementation using radix tree (matchit)
2//!
3//! This module provides HTTP routing functionality for RustAPI. Routes are
4//! registered using path patterns and HTTP method handlers.
5//!
6//! # Path Patterns
7//!
8//! Routes support dynamic path parameters using `{param}` syntax:
9//!
10//! - `/users` - Static path
11//! - `/users/{id}` - Single parameter
12//! - `/users/{user_id}/posts/{post_id}` - Multiple parameters
13//!
14//! # Example
15//!
16//! ```rust,ignore
17//! use rustapi_core::{Router, get, post, put, delete};
18//!
19//! async fn list_users() -> &'static str { "List users" }
20//! async fn get_user() -> &'static str { "Get user" }
21//! async fn create_user() -> &'static str { "Create user" }
22//! async fn update_user() -> &'static str { "Update user" }
23//! async fn delete_user() -> &'static str { "Delete user" }
24//!
25//! let router = Router::new()
26//!     .route("/users", get(list_users).post(create_user))
27//!     .route("/users/{id}", get(get_user).put(update_user).delete(delete_user));
28//! ```
29//!
30//! # Method Chaining
31//!
32//! Multiple HTTP methods can be registered for the same path using method chaining:
33//!
34//! ```rust,ignore
35//! .route("/users", get(list).post(create))
36//! .route("/users/{id}", get(show).put(update).delete(destroy))
37//! ```
38//!
39//! # Route Conflict Detection
40//!
41//! The router detects conflicting routes at registration time and provides
42//! helpful error messages with resolution guidance.
43
44use crate::handler::{into_boxed_handler, BoxedHandler, Handler};
45use crate::path_params::PathParams;
46use crate::typed_path::TypedPath;
47use http::{Extensions, Method};
48use matchit::Router as MatchitRouter;
49use rustapi_openapi::Operation;
50use std::collections::HashMap;
51use std::sync::Arc;
52
53/// Information about a registered route for conflict detection
54#[derive(Debug, Clone)]
55pub struct RouteInfo {
56    /// The original path pattern (e.g., "/users/{id}")
57    pub path: String,
58    /// The HTTP methods registered for this path
59    pub methods: Vec<Method>,
60}
61
62/// Error returned when a route conflict is detected
63#[derive(Debug, Clone)]
64pub struct RouteConflictError {
65    /// The path that was being registered
66    pub new_path: String,
67    /// The HTTP method that conflicts
68    pub method: Option<Method>,
69    /// The existing path that conflicts
70    pub existing_path: String,
71    /// Detailed error message from the underlying router
72    pub details: String,
73}
74
75impl std::fmt::Display for RouteConflictError {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        writeln!(
78            f,
79            "\n╭─────────────────────────────────────────────────────────────╮"
80        )?;
81        writeln!(
82            f,
83            "│                    ROUTE CONFLICT DETECTED                   │"
84        )?;
85        writeln!(
86            f,
87            "╰─────────────────────────────────────────────────────────────╯"
88        )?;
89        writeln!(f)?;
90        writeln!(f, "  Conflicting routes:")?;
91        writeln!(f, "    → Existing: {}", self.existing_path)?;
92        writeln!(f, "    → New:      {}", self.new_path)?;
93        writeln!(f)?;
94        if let Some(ref method) = self.method {
95            writeln!(f, "  HTTP Method: {}", method)?;
96            writeln!(f)?;
97        }
98        writeln!(f, "  Details: {}", self.details)?;
99        writeln!(f)?;
100        writeln!(f, "  How to resolve:")?;
101        writeln!(f, "    1. Use different path patterns for each route")?;
102        writeln!(
103            f,
104            "    2. If paths must be similar, ensure parameter names differ"
105        )?;
106        writeln!(
107            f,
108            "    3. Consider using different HTTP methods if appropriate"
109        )?;
110        writeln!(f)?;
111        writeln!(f, "  Example:")?;
112        writeln!(f, "    Instead of:")?;
113        writeln!(f, "      .route(\"/users/{{id}}\", get(handler1))")?;
114        writeln!(f, "      .route(\"/users/{{user_id}}\", get(handler2))")?;
115        writeln!(f)?;
116        writeln!(f, "    Use:")?;
117        writeln!(f, "      .route(\"/users/{{id}}\", get(handler1))")?;
118        writeln!(f, "      .route(\"/users/{{id}}/profile\", get(handler2))")?;
119        Ok(())
120    }
121}
122
123impl std::error::Error for RouteConflictError {}
124
125/// HTTP method router for a single path
126pub struct MethodRouter {
127    handlers: HashMap<Method, BoxedHandler>,
128    pub(crate) operations: HashMap<Method, Operation>,
129}
130
131impl Clone for MethodRouter {
132    fn clone(&self) -> Self {
133        Self {
134            handlers: self.handlers.clone(),
135            operations: self.operations.clone(),
136        }
137    }
138}
139
140impl MethodRouter {
141    /// Create a new empty method router
142    pub fn new() -> Self {
143        Self {
144            handlers: HashMap::new(),
145            operations: HashMap::new(),
146        }
147    }
148
149    /// Add a handler for a specific method
150    fn on(mut self, method: Method, handler: BoxedHandler, operation: Operation) -> Self {
151        self.handlers.insert(method.clone(), handler);
152        self.operations.insert(method, operation);
153        self
154    }
155
156    /// Get handler for a method
157    pub(crate) fn get_handler(&self, method: &Method) -> Option<&BoxedHandler> {
158        self.handlers.get(method)
159    }
160
161    /// Get allowed methods for 405 response
162    pub(crate) fn allowed_methods(&self) -> Vec<Method> {
163        self.handlers.keys().cloned().collect()
164    }
165
166    /// Create from pre-boxed handlers (internal use)
167    pub(crate) fn from_boxed(handlers: HashMap<Method, BoxedHandler>) -> Self {
168        Self {
169            handlers,
170            operations: HashMap::new(), // Operations lost when using raw boxed handlers for now
171        }
172    }
173
174    /// Insert a pre-boxed handler and its OpenAPI operation (internal use).
175    ///
176    /// Panics if the same method is inserted twice for the same path.
177    pub(crate) fn insert_boxed_with_operation(
178        &mut self,
179        method: Method,
180        handler: BoxedHandler,
181        operation: Operation,
182    ) {
183        if self.handlers.contains_key(&method) {
184            panic!(
185                "Duplicate handler for method {} on the same path",
186                method.as_str()
187            );
188        }
189
190        self.handlers.insert(method.clone(), handler);
191        self.operations.insert(method, operation);
192    }
193    /// Add a GET handler
194    pub fn get<H, T>(self, handler: H) -> Self
195    where
196        H: Handler<T>,
197        T: 'static,
198    {
199        let mut op = Operation::new();
200        H::update_operation(&mut op);
201        self.on(Method::GET, into_boxed_handler(handler), op)
202    }
203
204    /// Add a POST handler
205    pub fn post<H, T>(self, handler: H) -> Self
206    where
207        H: Handler<T>,
208        T: 'static,
209    {
210        let mut op = Operation::new();
211        H::update_operation(&mut op);
212        self.on(Method::POST, into_boxed_handler(handler), op)
213    }
214
215    /// Add a PUT handler
216    pub fn put<H, T>(self, handler: H) -> Self
217    where
218        H: Handler<T>,
219        T: 'static,
220    {
221        let mut op = Operation::new();
222        H::update_operation(&mut op);
223        self.on(Method::PUT, into_boxed_handler(handler), op)
224    }
225
226    /// Add a PATCH handler
227    pub fn patch<H, T>(self, handler: H) -> Self
228    where
229        H: Handler<T>,
230        T: 'static,
231    {
232        let mut op = Operation::new();
233        H::update_operation(&mut op);
234        self.on(Method::PATCH, into_boxed_handler(handler), op)
235    }
236
237    /// Add a DELETE handler
238    pub fn delete<H, T>(self, handler: H) -> Self
239    where
240        H: Handler<T>,
241        T: 'static,
242    {
243        let mut op = Operation::new();
244        H::update_operation(&mut op);
245        self.on(Method::DELETE, into_boxed_handler(handler), op)
246    }
247}
248
249impl Default for MethodRouter {
250    fn default() -> Self {
251        Self::new()
252    }
253}
254
255/// Create a GET route handler
256pub fn get<H, T>(handler: H) -> MethodRouter
257where
258    H: Handler<T>,
259    T: 'static,
260{
261    let mut op = Operation::new();
262    H::update_operation(&mut op);
263    MethodRouter::new().on(Method::GET, into_boxed_handler(handler), op)
264}
265
266/// Create a POST route handler
267pub fn post<H, T>(handler: H) -> MethodRouter
268where
269    H: Handler<T>,
270    T: 'static,
271{
272    let mut op = Operation::new();
273    H::update_operation(&mut op);
274    MethodRouter::new().on(Method::POST, into_boxed_handler(handler), op)
275}
276
277/// Create a PUT route handler
278pub fn put<H, T>(handler: H) -> MethodRouter
279where
280    H: Handler<T>,
281    T: 'static,
282{
283    let mut op = Operation::new();
284    H::update_operation(&mut op);
285    MethodRouter::new().on(Method::PUT, into_boxed_handler(handler), op)
286}
287
288/// Create a PATCH route handler
289pub fn patch<H, T>(handler: H) -> MethodRouter
290where
291    H: Handler<T>,
292    T: 'static,
293{
294    let mut op = Operation::new();
295    H::update_operation(&mut op);
296    MethodRouter::new().on(Method::PATCH, into_boxed_handler(handler), op)
297}
298
299/// Create a DELETE route handler
300pub fn delete<H, T>(handler: H) -> MethodRouter
301where
302    H: Handler<T>,
303    T: 'static,
304{
305    let mut op = Operation::new();
306    H::update_operation(&mut op);
307    MethodRouter::new().on(Method::DELETE, into_boxed_handler(handler), op)
308}
309
310/// Main router
311pub struct Router {
312    inner: MatchitRouter<MethodRouter>,
313    state: Arc<Extensions>,
314    /// Track registered routes for conflict detection
315    registered_routes: HashMap<String, RouteInfo>,
316    /// Store MethodRouters for nesting support (keyed by matchit path)
317    method_routers: HashMap<String, MethodRouter>,
318    /// Track state type IDs for merging (type name -> whether it's set)
319    /// This is a workaround since Extensions doesn't support iteration
320    state_type_ids: Vec<std::any::TypeId>,
321}
322
323impl Router {
324    /// Create a new router
325    pub fn new() -> Self {
326        Self {
327            inner: MatchitRouter::new(),
328            state: Arc::new(Extensions::new()),
329            registered_routes: HashMap::new(),
330            method_routers: HashMap::new(),
331            state_type_ids: Vec::new(),
332        }
333    }
334
335    /// Add a typed route using a TypedPath
336    pub fn typed<P: TypedPath>(self, method_router: MethodRouter) -> Self {
337        self.route(P::PATH, method_router)
338    }
339
340    /// Add a route
341    pub fn route(mut self, path: &str, method_router: MethodRouter) -> Self {
342        // Convert {param} style to :param for matchit
343        let matchit_path = convert_path_params(path);
344
345        // Get the methods being registered
346        let methods: Vec<Method> = method_router.handlers.keys().cloned().collect();
347
348        // Store a clone of the MethodRouter for nesting support
349        self.method_routers
350            .insert(matchit_path.clone(), method_router.clone());
351
352        match self.inner.insert(matchit_path.clone(), method_router) {
353            Ok(_) => {
354                // Track the registered route
355                self.registered_routes.insert(
356                    matchit_path.clone(),
357                    RouteInfo {
358                        path: path.to_string(),
359                        methods,
360                    },
361                );
362            }
363            Err(e) => {
364                // Remove the method_router we just added since registration failed
365                self.method_routers.remove(&matchit_path);
366
367                // Find the existing conflicting route
368                let existing_path = self
369                    .find_conflicting_route(&matchit_path)
370                    .map(|info| info.path.clone())
371                    .unwrap_or_else(|| "<unknown>".to_string());
372
373                let conflict_error = RouteConflictError {
374                    new_path: path.to_string(),
375                    method: methods.first().cloned(),
376                    existing_path,
377                    details: e.to_string(),
378                };
379
380                panic!("{}", conflict_error);
381            }
382        }
383        self
384    }
385
386    /// Find a conflicting route by checking registered routes
387    fn find_conflicting_route(&self, matchit_path: &str) -> Option<&RouteInfo> {
388        // Try to find an exact match first
389        if let Some(info) = self.registered_routes.get(matchit_path) {
390            return Some(info);
391        }
392
393        // Try to find a route that would conflict (same structure but different param names)
394        let normalized_new = normalize_path_for_comparison(matchit_path);
395
396        for (registered_path, info) in &self.registered_routes {
397            let normalized_existing = normalize_path_for_comparison(registered_path);
398            if normalized_new == normalized_existing {
399                return Some(info);
400            }
401        }
402
403        None
404    }
405
406    /// Add application state
407    pub fn state<S: Clone + Send + Sync + 'static>(mut self, state: S) -> Self {
408        let type_id = std::any::TypeId::of::<S>();
409        let extensions = Arc::make_mut(&mut self.state);
410        extensions.insert(state);
411        if !self.state_type_ids.contains(&type_id) {
412            self.state_type_ids.push(type_id);
413        }
414        self
415    }
416
417    /// Check if state of a given type exists
418    pub fn has_state<S: 'static>(&self) -> bool {
419        self.state_type_ids.contains(&std::any::TypeId::of::<S>())
420    }
421
422    /// Get state type IDs (for testing and debugging)
423    pub fn state_type_ids(&self) -> &[std::any::TypeId] {
424        &self.state_type_ids
425    }
426
427    /// Nest another router under a prefix
428    ///
429    /// All routes from the nested router will be registered with the prefix
430    /// prepended to their paths. State from the nested router is merged into
431    /// the parent router (parent state takes precedence for type conflicts).
432    ///
433    /// # State Merging
434    ///
435    /// When nesting routers with state:
436    /// - If the parent router has state of type T, it is preserved (parent wins)
437    /// - If only the nested router has state of type T, it is added to the parent
438    /// - State type tracking is merged to enable proper conflict detection
439    ///
440    /// Note: Due to limitations of `http::Extensions`, automatic state merging
441    /// requires using the `merge_state` method for specific types.
442    ///
443    /// # Example
444    ///
445    /// ```rust,ignore
446    /// use rustapi_core::{Router, get};
447    ///
448    /// async fn list_users() -> &'static str { "List users" }
449    /// async fn get_user() -> &'static str { "Get user" }
450    ///
451    /// let users_router = Router::new()
452    ///     .route("/", get(list_users))
453    ///     .route("/{id}", get(get_user));
454    ///
455    /// let app = Router::new()
456    ///     .nest("/api/users", users_router);
457    ///
458    /// // Routes are now:
459    /// // GET /api/users/
460    /// // GET /api/users/{id}
461    /// ```
462    pub fn nest(mut self, prefix: &str, router: Router) -> Self {
463        // 1. Normalize the prefix
464        let normalized_prefix = normalize_prefix(prefix);
465
466        // 2. Merge state type IDs from nested router
467        // Parent state takes precedence - we only track types, actual values
468        // are handled by merge_state calls or by the user adding state to parent
469        for type_id in &router.state_type_ids {
470            if !self.state_type_ids.contains(type_id) {
471                self.state_type_ids.push(*type_id);
472            }
473        }
474
475        // 3. Collect routes from the nested router before consuming it
476        // We need to iterate over registered_routes and get the corresponding MethodRouters
477        let nested_routes: Vec<(String, RouteInfo, MethodRouter)> = router
478            .registered_routes
479            .into_iter()
480            .filter_map(|(matchit_path, route_info)| {
481                router
482                    .method_routers
483                    .get(&matchit_path)
484                    .map(|mr| (matchit_path, route_info, mr.clone()))
485            })
486            .collect();
487
488        // 4. Register each nested route with the prefix
489        for (matchit_path, route_info, method_router) in nested_routes {
490            // Build the prefixed path
491            // The matchit_path already has the :param format
492            // The route_info.path has the {param} format
493            let prefixed_matchit_path = if matchit_path == "/" {
494                normalized_prefix.clone()
495            } else {
496                format!("{}{}", normalized_prefix, matchit_path)
497            };
498
499            let prefixed_display_path = if route_info.path == "/" {
500                normalized_prefix.clone()
501            } else {
502                format!("{}{}", normalized_prefix, route_info.path)
503            };
504
505            // Store the MethodRouter for future nesting
506            self.method_routers
507                .insert(prefixed_matchit_path.clone(), method_router.clone());
508
509            // Try to insert into the matchit router
510            match self
511                .inner
512                .insert(prefixed_matchit_path.clone(), method_router)
513            {
514                Ok(_) => {
515                    // Track the registered route
516                    self.registered_routes.insert(
517                        prefixed_matchit_path,
518                        RouteInfo {
519                            path: prefixed_display_path,
520                            methods: route_info.methods,
521                        },
522                    );
523                }
524                Err(e) => {
525                    // Remove the method_router we just added since registration failed
526                    self.method_routers.remove(&prefixed_matchit_path);
527
528                    // Find the existing conflicting route
529                    let existing_path = self
530                        .find_conflicting_route(&prefixed_matchit_path)
531                        .map(|info| info.path.clone())
532                        .unwrap_or_else(|| "<unknown>".to_string());
533
534                    let conflict_error = RouteConflictError {
535                        new_path: prefixed_display_path,
536                        method: route_info.methods.first().cloned(),
537                        existing_path,
538                        details: e.to_string(),
539                    };
540
541                    panic!("{}", conflict_error);
542                }
543            }
544        }
545
546        self
547    }
548
549    /// Merge state from another router into this one
550    ///
551    /// This method allows explicit state merging when nesting routers.
552    /// Parent state takes precedence - if the parent already has state of type S,
553    /// the nested state is ignored.
554    ///
555    /// # Example
556    ///
557    /// ```rust,ignore
558    /// #[derive(Clone)]
559    /// struct DbPool(String);
560    ///
561    /// let nested = Router::new().state(DbPool("nested".to_string()));
562    /// let parent = Router::new()
563    ///     .merge_state::<DbPool>(&nested); // Adds DbPool from nested
564    /// ```
565    pub fn merge_state<S: Clone + Send + Sync + 'static>(mut self, other: &Router) -> Self {
566        let type_id = std::any::TypeId::of::<S>();
567
568        // Parent wins - only merge if parent doesn't have this state type
569        if !self.state_type_ids.contains(&type_id) {
570            // Try to get the state from the other router
571            if let Some(state) = other.state.get::<S>() {
572                let extensions = Arc::make_mut(&mut self.state);
573                extensions.insert(state.clone());
574                self.state_type_ids.push(type_id);
575            }
576        }
577
578        self
579    }
580
581    /// Match a request and return the handler + params
582    pub fn match_route(&self, path: &str, method: &Method) -> RouteMatch<'_> {
583        match self.inner.at(path) {
584            Ok(matched) => {
585                let method_router = matched.value;
586
587                if let Some(handler) = method_router.get_handler(method) {
588                    // Use stack-optimized PathParams (avoids heap allocation for ≤4 params)
589                    let params: PathParams = matched
590                        .params
591                        .iter()
592                        .map(|(k, v)| (k.to_string(), v.to_string()))
593                        .collect();
594
595                    RouteMatch::Found { handler, params }
596                } else {
597                    RouteMatch::MethodNotAllowed {
598                        allowed: method_router.allowed_methods(),
599                    }
600                }
601            }
602            Err(_) => RouteMatch::NotFound,
603        }
604    }
605
606    /// Get shared state
607    pub fn state_ref(&self) -> Arc<Extensions> {
608        self.state.clone()
609    }
610
611    /// Get registered routes (for testing and debugging)
612    pub fn registered_routes(&self) -> &HashMap<String, RouteInfo> {
613        &self.registered_routes
614    }
615
616    /// Get method routers (for OpenAPI integration during nesting)
617    pub fn method_routers(&self) -> &HashMap<String, MethodRouter> {
618        &self.method_routers
619    }
620}
621
622impl Default for Router {
623    fn default() -> Self {
624        Self::new()
625    }
626}
627
628/// Result of route matching
629pub enum RouteMatch<'a> {
630    Found {
631        handler: &'a BoxedHandler,
632        params: PathParams,
633    },
634    NotFound,
635    MethodNotAllowed {
636        allowed: Vec<Method>,
637    },
638}
639
640/// Convert {param} style to :param for matchit
641fn convert_path_params(path: &str) -> String {
642    let mut result = String::with_capacity(path.len());
643
644    for ch in path.chars() {
645        match ch {
646            '{' => {
647                result.push(':');
648            }
649            '}' => {
650                // Skip closing brace
651            }
652            _ => {
653                result.push(ch);
654            }
655        }
656    }
657
658    result
659}
660
661/// Normalize a path for conflict comparison by replacing parameter names with a placeholder
662fn normalize_path_for_comparison(path: &str) -> String {
663    let mut result = String::with_capacity(path.len());
664    let mut in_param = false;
665
666    for ch in path.chars() {
667        match ch {
668            ':' => {
669                in_param = true;
670                result.push_str(":_");
671            }
672            '/' => {
673                in_param = false;
674                result.push('/');
675            }
676            _ if in_param => {
677                // Skip parameter name characters
678            }
679            _ => {
680                result.push(ch);
681            }
682        }
683    }
684
685    result
686}
687
688/// Normalize a prefix for router nesting.
689///
690/// Ensures the prefix:
691/// - Starts with exactly one leading slash
692/// - Has no trailing slash (unless it's just "/")
693/// - Has no double slashes
694///
695/// # Examples
696///
697/// ```ignore
698/// assert_eq!(normalize_prefix("api"), "/api");
699/// assert_eq!(normalize_prefix("/api"), "/api");
700/// assert_eq!(normalize_prefix("/api/"), "/api");
701/// assert_eq!(normalize_prefix("//api//"), "/api");
702/// assert_eq!(normalize_prefix(""), "/");
703/// ```
704pub(crate) fn normalize_prefix(prefix: &str) -> String {
705    // Handle empty string
706    if prefix.is_empty() {
707        return "/".to_string();
708    }
709
710    // Split by slashes and filter out empty segments (handles multiple slashes)
711    let segments: Vec<&str> = prefix.split('/').filter(|s| !s.is_empty()).collect();
712
713    // If no segments after filtering, return root
714    if segments.is_empty() {
715        return "/".to_string();
716    }
717
718    // Build the normalized prefix with leading slash
719    let mut result = String::with_capacity(prefix.len() + 1);
720    for segment in segments {
721        result.push('/');
722        result.push_str(segment);
723    }
724
725    result
726}
727
728#[cfg(test)]
729mod tests {
730    use super::*;
731
732    #[test]
733    fn test_convert_path_params() {
734        assert_eq!(convert_path_params("/users/{id}"), "/users/:id");
735        assert_eq!(
736            convert_path_params("/users/{user_id}/posts/{post_id}"),
737            "/users/:user_id/posts/:post_id"
738        );
739        assert_eq!(convert_path_params("/static/path"), "/static/path");
740    }
741
742    #[test]
743    fn test_normalize_path_for_comparison() {
744        assert_eq!(normalize_path_for_comparison("/users/:id"), "/users/:_");
745        assert_eq!(
746            normalize_path_for_comparison("/users/:user_id"),
747            "/users/:_"
748        );
749        assert_eq!(
750            normalize_path_for_comparison("/users/:id/posts/:post_id"),
751            "/users/:_/posts/:_"
752        );
753        assert_eq!(
754            normalize_path_for_comparison("/static/path"),
755            "/static/path"
756        );
757    }
758
759    #[test]
760    fn test_normalize_prefix() {
761        // Basic cases
762        assert_eq!(normalize_prefix("api"), "/api");
763        assert_eq!(normalize_prefix("/api"), "/api");
764        assert_eq!(normalize_prefix("/api/"), "/api");
765        assert_eq!(normalize_prefix("api/"), "/api");
766
767        // Multiple segments
768        assert_eq!(normalize_prefix("api/v1"), "/api/v1");
769        assert_eq!(normalize_prefix("/api/v1"), "/api/v1");
770        assert_eq!(normalize_prefix("/api/v1/"), "/api/v1");
771
772        // Edge cases: empty and root
773        assert_eq!(normalize_prefix(""), "/");
774        assert_eq!(normalize_prefix("/"), "/");
775
776        // Multiple slashes
777        assert_eq!(normalize_prefix("//api"), "/api");
778        assert_eq!(normalize_prefix("api//v1"), "/api/v1");
779        assert_eq!(normalize_prefix("//api//v1//"), "/api/v1");
780        assert_eq!(normalize_prefix("///"), "/");
781    }
782
783    #[test]
784    #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
785    fn test_route_conflict_detection() {
786        async fn handler1() -> &'static str {
787            "handler1"
788        }
789        async fn handler2() -> &'static str {
790            "handler2"
791        }
792
793        let _router = Router::new()
794            .route("/users/{id}", get(handler1))
795            .route("/users/{user_id}", get(handler2)); // This should panic
796    }
797
798    #[test]
799    fn test_no_conflict_different_paths() {
800        async fn handler1() -> &'static str {
801            "handler1"
802        }
803        async fn handler2() -> &'static str {
804            "handler2"
805        }
806
807        let router = Router::new()
808            .route("/users/{id}", get(handler1))
809            .route("/users/{id}/profile", get(handler2));
810
811        assert_eq!(router.registered_routes().len(), 2);
812    }
813
814    #[test]
815    fn test_route_info_tracking() {
816        async fn handler() -> &'static str {
817            "handler"
818        }
819
820        let router = Router::new().route("/users/{id}", get(handler));
821
822        let routes = router.registered_routes();
823        assert_eq!(routes.len(), 1);
824
825        let info = routes.get("/users/:id").unwrap();
826        assert_eq!(info.path, "/users/{id}");
827        assert_eq!(info.methods.len(), 1);
828        assert_eq!(info.methods[0], Method::GET);
829    }
830
831    #[test]
832    fn test_basic_router_nesting() {
833        async fn list_users() -> &'static str {
834            "list users"
835        }
836        async fn get_user() -> &'static str {
837            "get user"
838        }
839
840        let users_router = Router::new()
841            .route("/", get(list_users))
842            .route("/{id}", get(get_user));
843
844        let app = Router::new().nest("/api/users", users_router);
845
846        let routes = app.registered_routes();
847        assert_eq!(routes.len(), 2);
848
849        // Check that routes are registered with prefix
850        assert!(routes.contains_key("/api/users"));
851        assert!(routes.contains_key("/api/users/:id"));
852
853        // Check display paths
854        let list_info = routes.get("/api/users").unwrap();
855        assert_eq!(list_info.path, "/api/users");
856
857        let get_info = routes.get("/api/users/:id").unwrap();
858        assert_eq!(get_info.path, "/api/users/{id}");
859    }
860
861    #[test]
862    fn test_nested_route_matching() {
863        async fn handler() -> &'static str {
864            "handler"
865        }
866
867        let users_router = Router::new().route("/{id}", get(handler));
868
869        let app = Router::new().nest("/api/users", users_router);
870
871        // Test that the route can be matched
872        match app.match_route("/api/users/123", &Method::GET) {
873            RouteMatch::Found { params, .. } => {
874                assert_eq!(params.get("id"), Some(&"123".to_string()));
875            }
876            _ => panic!("Route should be found"),
877        }
878    }
879
880    #[test]
881    fn test_nested_route_matching_multiple_params() {
882        async fn handler() -> &'static str {
883            "handler"
884        }
885
886        let posts_router = Router::new().route("/{user_id}/posts/{post_id}", get(handler));
887
888        let app = Router::new().nest("/api", posts_router);
889
890        // Test that multiple parameters are correctly extracted
891        match app.match_route("/api/42/posts/100", &Method::GET) {
892            RouteMatch::Found { params, .. } => {
893                assert_eq!(params.get("user_id"), Some(&"42".to_string()));
894                assert_eq!(params.get("post_id"), Some(&"100".to_string()));
895            }
896            _ => panic!("Route should be found"),
897        }
898    }
899
900    #[test]
901    fn test_nested_route_matching_static_path() {
902        async fn handler() -> &'static str {
903            "handler"
904        }
905
906        let health_router = Router::new().route("/health", get(handler));
907
908        let app = Router::new().nest("/api/v1", health_router);
909
910        // Test that static paths are correctly matched
911        match app.match_route("/api/v1/health", &Method::GET) {
912            RouteMatch::Found { params, .. } => {
913                assert!(params.is_empty(), "Static path should have no params");
914            }
915            _ => panic!("Route should be found"),
916        }
917    }
918
919    #[test]
920    fn test_nested_route_not_found() {
921        async fn handler() -> &'static str {
922            "handler"
923        }
924
925        let users_router = Router::new().route("/users", get(handler));
926
927        let app = Router::new().nest("/api", users_router);
928
929        // Test that non-existent paths return NotFound
930        match app.match_route("/api/posts", &Method::GET) {
931            RouteMatch::NotFound => {
932                // Expected
933            }
934            _ => panic!("Route should not be found"),
935        }
936
937        // Test that wrong prefix returns NotFound
938        match app.match_route("/v2/users", &Method::GET) {
939            RouteMatch::NotFound => {
940                // Expected
941            }
942            _ => panic!("Route with wrong prefix should not be found"),
943        }
944    }
945
946    #[test]
947    fn test_nested_route_method_not_allowed() {
948        async fn handler() -> &'static str {
949            "handler"
950        }
951
952        let users_router = Router::new().route("/users", get(handler));
953
954        let app = Router::new().nest("/api", users_router);
955
956        // Test that wrong method returns MethodNotAllowed
957        match app.match_route("/api/users", &Method::POST) {
958            RouteMatch::MethodNotAllowed { allowed } => {
959                assert!(allowed.contains(&Method::GET));
960                assert!(!allowed.contains(&Method::POST));
961            }
962            _ => panic!("Should return MethodNotAllowed"),
963        }
964    }
965
966    #[test]
967    fn test_nested_route_multiple_methods() {
968        async fn get_handler() -> &'static str {
969            "get"
970        }
971        async fn post_handler() -> &'static str {
972            "post"
973        }
974
975        // Create a method router with both GET and POST
976        let get_router = get(get_handler);
977        let post_router = post(post_handler);
978        let mut combined = MethodRouter::new();
979        for (method, handler) in get_router.handlers {
980            combined.handlers.insert(method, handler);
981        }
982        for (method, handler) in post_router.handlers {
983            combined.handlers.insert(method, handler);
984        }
985
986        let users_router = Router::new().route("/users", combined);
987        let app = Router::new().nest("/api", users_router);
988
989        // Both GET and POST should work
990        match app.match_route("/api/users", &Method::GET) {
991            RouteMatch::Found { .. } => {}
992            _ => panic!("GET should be found"),
993        }
994
995        match app.match_route("/api/users", &Method::POST) {
996            RouteMatch::Found { .. } => {}
997            _ => panic!("POST should be found"),
998        }
999
1000        // DELETE should return MethodNotAllowed with GET and POST in allowed
1001        match app.match_route("/api/users", &Method::DELETE) {
1002            RouteMatch::MethodNotAllowed { allowed } => {
1003                assert!(allowed.contains(&Method::GET));
1004                assert!(allowed.contains(&Method::POST));
1005            }
1006            _ => panic!("DELETE should return MethodNotAllowed"),
1007        }
1008    }
1009
1010    #[test]
1011    fn test_nested_router_prefix_normalization() {
1012        async fn handler() -> &'static str {
1013            "handler"
1014        }
1015
1016        // Test various prefix formats
1017        let router1 = Router::new().route("/test", get(handler));
1018        let app1 = Router::new().nest("api", router1);
1019        assert!(app1.registered_routes().contains_key("/api/test"));
1020
1021        let router2 = Router::new().route("/test", get(handler));
1022        let app2 = Router::new().nest("/api/", router2);
1023        assert!(app2.registered_routes().contains_key("/api/test"));
1024
1025        let router3 = Router::new().route("/test", get(handler));
1026        let app3 = Router::new().nest("//api//", router3);
1027        assert!(app3.registered_routes().contains_key("/api/test"));
1028    }
1029
1030    #[test]
1031    fn test_state_tracking() {
1032        #[derive(Clone)]
1033        struct MyState(#[allow(dead_code)] String);
1034
1035        let router = Router::new().state(MyState("test".to_string()));
1036
1037        assert!(router.has_state::<MyState>());
1038        assert!(!router.has_state::<String>());
1039    }
1040
1041    #[test]
1042    fn test_state_merge_nested_only() {
1043        #[derive(Clone, PartialEq, Debug)]
1044        struct NestedState(String);
1045
1046        async fn handler() -> &'static str {
1047            "handler"
1048        }
1049
1050        // Create a router with state to use as source for merging
1051        let state_source = Router::new().state(NestedState("nested".to_string()));
1052
1053        let nested = Router::new().route("/test", get(handler));
1054
1055        let parent = Router::new()
1056            .nest("/api", nested)
1057            .merge_state::<NestedState>(&state_source);
1058
1059        // Parent should now have the nested state
1060        assert!(parent.has_state::<NestedState>());
1061
1062        // Verify the state value
1063        let state = parent.state.get::<NestedState>().unwrap();
1064        assert_eq!(state.0, "nested");
1065    }
1066
1067    #[test]
1068    fn test_state_merge_parent_wins() {
1069        #[derive(Clone, PartialEq, Debug)]
1070        struct SharedState(String);
1071
1072        async fn handler() -> &'static str {
1073            "handler"
1074        }
1075
1076        // Create a router with state to use as source for merging
1077        let state_source = Router::new().state(SharedState("nested".to_string()));
1078
1079        let nested = Router::new().route("/test", get(handler));
1080
1081        let parent = Router::new()
1082            .state(SharedState("parent".to_string()))
1083            .nest("/api", nested)
1084            .merge_state::<SharedState>(&state_source);
1085
1086        // Parent should still have its own state (parent wins)
1087        assert!(parent.has_state::<SharedState>());
1088
1089        // Verify the state value is from parent
1090        let state = parent.state.get::<SharedState>().unwrap();
1091        assert_eq!(state.0, "parent");
1092    }
1093
1094    #[test]
1095    fn test_state_type_ids_merged_on_nest() {
1096        #[derive(Clone)]
1097        struct NestedState(#[allow(dead_code)] String);
1098
1099        async fn handler() -> &'static str {
1100            "handler"
1101        }
1102
1103        let nested = Router::new()
1104            .route("/test", get(handler))
1105            .state(NestedState("nested".to_string()));
1106
1107        let parent = Router::new().nest("/api", nested);
1108
1109        // Parent should track the nested state type ID
1110        assert!(parent
1111            .state_type_ids()
1112            .contains(&std::any::TypeId::of::<NestedState>()));
1113    }
1114
1115    #[test]
1116    #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
1117    fn test_nested_route_conflict_with_existing_route() {
1118        async fn handler1() -> &'static str {
1119            "handler1"
1120        }
1121        async fn handler2() -> &'static str {
1122            "handler2"
1123        }
1124
1125        // Create a parent router with an existing route
1126        let parent = Router::new().route("/api/users/{id}", get(handler1));
1127
1128        // Create a nested router with a conflicting route
1129        let nested = Router::new().route("/{user_id}", get(handler2));
1130
1131        // This should panic because /api/users/{id} conflicts with /api/users/{user_id}
1132        let _app = parent.nest("/api/users", nested);
1133    }
1134
1135    #[test]
1136    #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
1137    fn test_nested_route_conflict_same_path_different_param_names() {
1138        async fn handler1() -> &'static str {
1139            "handler1"
1140        }
1141        async fn handler2() -> &'static str {
1142            "handler2"
1143        }
1144
1145        // Create two nested routers with same path structure but different param names
1146        let nested1 = Router::new().route("/{id}", get(handler1));
1147        let nested2 = Router::new().route("/{user_id}", get(handler2));
1148
1149        // Nest both under the same prefix - should conflict
1150        let _app = Router::new()
1151            .nest("/api/users", nested1)
1152            .nest("/api/users", nested2);
1153    }
1154
1155    #[test]
1156    fn test_nested_route_conflict_error_contains_both_paths() {
1157        use std::panic::{catch_unwind, AssertUnwindSafe};
1158
1159        async fn handler1() -> &'static str {
1160            "handler1"
1161        }
1162        async fn handler2() -> &'static str {
1163            "handler2"
1164        }
1165
1166        let result = catch_unwind(AssertUnwindSafe(|| {
1167            let parent = Router::new().route("/api/users/{id}", get(handler1));
1168            let nested = Router::new().route("/{user_id}", get(handler2));
1169            let _app = parent.nest("/api/users", nested);
1170        }));
1171
1172        assert!(result.is_err(), "Should have panicked due to conflict");
1173
1174        if let Err(panic_info) = result {
1175            if let Some(msg) = panic_info.downcast_ref::<String>() {
1176                assert!(
1177                    msg.contains("ROUTE CONFLICT DETECTED"),
1178                    "Error should contain 'ROUTE CONFLICT DETECTED'"
1179                );
1180                assert!(
1181                    msg.contains("Existing:") && msg.contains("New:"),
1182                    "Error should contain both 'Existing:' and 'New:' labels"
1183                );
1184                assert!(
1185                    msg.contains("How to resolve:"),
1186                    "Error should contain resolution guidance"
1187                );
1188            }
1189        }
1190    }
1191
1192    #[test]
1193    fn test_nested_routes_no_conflict_different_prefixes() {
1194        async fn handler1() -> &'static str {
1195            "handler1"
1196        }
1197        async fn handler2() -> &'static str {
1198            "handler2"
1199        }
1200
1201        // Create two nested routers with same internal paths but different prefixes
1202        let nested1 = Router::new().route("/{id}", get(handler1));
1203        let nested2 = Router::new().route("/{id}", get(handler2));
1204
1205        // Nest under different prefixes - should NOT conflict
1206        let app = Router::new()
1207            .nest("/api/users", nested1)
1208            .nest("/api/posts", nested2);
1209
1210        assert_eq!(app.registered_routes().len(), 2);
1211        assert!(app.registered_routes().contains_key("/api/users/:id"));
1212        assert!(app.registered_routes().contains_key("/api/posts/:id"));
1213    }
1214
1215    // **Feature: router-nesting, Property 4: Multiple Router Composition**
1216    // Tests for nesting multiple routers under different prefixes
1217    // **Validates: Requirements 1.5**
1218
1219    #[test]
1220    fn test_multiple_router_composition_all_routes_registered() {
1221        async fn users_list() -> &'static str {
1222            "users list"
1223        }
1224        async fn users_get() -> &'static str {
1225            "users get"
1226        }
1227        async fn posts_list() -> &'static str {
1228            "posts list"
1229        }
1230        async fn posts_get() -> &'static str {
1231            "posts get"
1232        }
1233        async fn comments_list() -> &'static str {
1234            "comments list"
1235        }
1236
1237        // Create multiple sub-routers with different routes
1238        let users_router = Router::new()
1239            .route("/", get(users_list))
1240            .route("/{id}", get(users_get));
1241
1242        let posts_router = Router::new()
1243            .route("/", get(posts_list))
1244            .route("/{id}", get(posts_get));
1245
1246        let comments_router = Router::new().route("/", get(comments_list));
1247
1248        // Nest all routers under different prefixes
1249        let app = Router::new()
1250            .nest("/api/users", users_router)
1251            .nest("/api/posts", posts_router)
1252            .nest("/api/comments", comments_router);
1253
1254        // Verify all routes are registered (2 + 2 + 1 = 5 routes)
1255        let routes = app.registered_routes();
1256        assert_eq!(routes.len(), 5, "Should have 5 routes registered");
1257
1258        // Verify users routes
1259        assert!(
1260            routes.contains_key("/api/users"),
1261            "Should have /api/users route"
1262        );
1263        assert!(
1264            routes.contains_key("/api/users/:id"),
1265            "Should have /api/users/:id route"
1266        );
1267
1268        // Verify posts routes
1269        assert!(
1270            routes.contains_key("/api/posts"),
1271            "Should have /api/posts route"
1272        );
1273        assert!(
1274            routes.contains_key("/api/posts/:id"),
1275            "Should have /api/posts/:id route"
1276        );
1277
1278        // Verify comments routes
1279        assert!(
1280            routes.contains_key("/api/comments"),
1281            "Should have /api/comments route"
1282        );
1283    }
1284
1285    #[test]
1286    fn test_multiple_router_composition_no_interference() {
1287        async fn users_handler() -> &'static str {
1288            "users"
1289        }
1290        async fn posts_handler() -> &'static str {
1291            "posts"
1292        }
1293        async fn admin_handler() -> &'static str {
1294            "admin"
1295        }
1296
1297        // Create routers with same internal structure but different prefixes
1298        let users_router = Router::new()
1299            .route("/list", get(users_handler))
1300            .route("/{id}", get(users_handler));
1301
1302        let posts_router = Router::new()
1303            .route("/list", get(posts_handler))
1304            .route("/{id}", get(posts_handler));
1305
1306        let admin_router = Router::new()
1307            .route("/list", get(admin_handler))
1308            .route("/{id}", get(admin_handler));
1309
1310        // Nest all routers
1311        let app = Router::new()
1312            .nest("/api/v1/users", users_router)
1313            .nest("/api/v1/posts", posts_router)
1314            .nest("/admin", admin_router);
1315
1316        // Verify all routes are registered (2 + 2 + 2 = 6 routes)
1317        let routes = app.registered_routes();
1318        assert_eq!(routes.len(), 6, "Should have 6 routes registered");
1319
1320        // Verify each prefix group has its routes
1321        assert!(routes.contains_key("/api/v1/users/list"));
1322        assert!(routes.contains_key("/api/v1/users/:id"));
1323        assert!(routes.contains_key("/api/v1/posts/list"));
1324        assert!(routes.contains_key("/api/v1/posts/:id"));
1325        assert!(routes.contains_key("/admin/list"));
1326        assert!(routes.contains_key("/admin/:id"));
1327
1328        // Verify routes are matchable and don't interfere with each other
1329        match app.match_route("/api/v1/users/list", &Method::GET) {
1330            RouteMatch::Found { params, .. } => {
1331                assert!(params.is_empty(), "Static path should have no params");
1332            }
1333            _ => panic!("Should find /api/v1/users/list"),
1334        }
1335
1336        match app.match_route("/api/v1/posts/123", &Method::GET) {
1337            RouteMatch::Found { params, .. } => {
1338                assert_eq!(params.get("id"), Some(&"123".to_string()));
1339            }
1340            _ => panic!("Should find /api/v1/posts/123"),
1341        }
1342
1343        match app.match_route("/admin/456", &Method::GET) {
1344            RouteMatch::Found { params, .. } => {
1345                assert_eq!(params.get("id"), Some(&"456".to_string()));
1346            }
1347            _ => panic!("Should find /admin/456"),
1348        }
1349    }
1350
1351    #[test]
1352    fn test_multiple_router_composition_with_multiple_methods() {
1353        async fn get_handler() -> &'static str {
1354            "get"
1355        }
1356        async fn post_handler() -> &'static str {
1357            "post"
1358        }
1359        async fn put_handler() -> &'static str {
1360            "put"
1361        }
1362
1363        // Create routers with multiple HTTP methods
1364        // Combine GET and POST for users root
1365        let get_router = get(get_handler);
1366        let post_router = post(post_handler);
1367        let mut users_root_combined = MethodRouter::new();
1368        for (method, handler) in get_router.handlers {
1369            users_root_combined.handlers.insert(method, handler);
1370        }
1371        for (method, handler) in post_router.handlers {
1372            users_root_combined.handlers.insert(method, handler);
1373        }
1374
1375        // Combine GET and PUT for users/{id}
1376        let get_router2 = get(get_handler);
1377        let put_router = put(put_handler);
1378        let mut users_id_combined = MethodRouter::new();
1379        for (method, handler) in get_router2.handlers {
1380            users_id_combined.handlers.insert(method, handler);
1381        }
1382        for (method, handler) in put_router.handlers {
1383            users_id_combined.handlers.insert(method, handler);
1384        }
1385
1386        let users_router = Router::new()
1387            .route("/", users_root_combined)
1388            .route("/{id}", users_id_combined);
1389
1390        // Combine GET and POST for posts root
1391        let get_router3 = get(get_handler);
1392        let post_router2 = post(post_handler);
1393        let mut posts_root_combined = MethodRouter::new();
1394        for (method, handler) in get_router3.handlers {
1395            posts_root_combined.handlers.insert(method, handler);
1396        }
1397        for (method, handler) in post_router2.handlers {
1398            posts_root_combined.handlers.insert(method, handler);
1399        }
1400
1401        let posts_router = Router::new().route("/", posts_root_combined);
1402
1403        // Nest routers
1404        let app = Router::new()
1405            .nest("/users", users_router)
1406            .nest("/posts", posts_router);
1407
1408        // Verify routes are registered
1409        let routes = app.registered_routes();
1410        assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1411
1412        // Verify methods are preserved for users routes
1413        let users_root = routes.get("/users").unwrap();
1414        assert!(users_root.methods.contains(&Method::GET));
1415        assert!(users_root.methods.contains(&Method::POST));
1416
1417        let users_id = routes.get("/users/:id").unwrap();
1418        assert!(users_id.methods.contains(&Method::GET));
1419        assert!(users_id.methods.contains(&Method::PUT));
1420
1421        // Verify methods are preserved for posts routes
1422        let posts_root = routes.get("/posts").unwrap();
1423        assert!(posts_root.methods.contains(&Method::GET));
1424        assert!(posts_root.methods.contains(&Method::POST));
1425
1426        // Verify route matching works for all methods
1427        match app.match_route("/users", &Method::GET) {
1428            RouteMatch::Found { .. } => {}
1429            _ => panic!("GET /users should be found"),
1430        }
1431        match app.match_route("/users", &Method::POST) {
1432            RouteMatch::Found { .. } => {}
1433            _ => panic!("POST /users should be found"),
1434        }
1435        match app.match_route("/users/123", &Method::PUT) {
1436            RouteMatch::Found { .. } => {}
1437            _ => panic!("PUT /users/123 should be found"),
1438        }
1439    }
1440
1441    #[test]
1442    fn test_multiple_router_composition_deep_nesting() {
1443        async fn handler() -> &'static str {
1444            "handler"
1445        }
1446
1447        // Create nested routers at different depth levels
1448        let deep_router = Router::new().route("/action", get(handler));
1449
1450        let mid_router = Router::new().route("/info", get(handler));
1451
1452        let shallow_router = Router::new().route("/status", get(handler));
1453
1454        // Nest at different depths
1455        let app = Router::new()
1456            .nest("/api/v1/resources/items", deep_router)
1457            .nest("/api/v1/resources", mid_router)
1458            .nest("/api", shallow_router);
1459
1460        // Verify all routes are registered
1461        let routes = app.registered_routes();
1462        assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1463
1464        assert!(routes.contains_key("/api/v1/resources/items/action"));
1465        assert!(routes.contains_key("/api/v1/resources/info"));
1466        assert!(routes.contains_key("/api/status"));
1467
1468        // Verify all routes are matchable
1469        match app.match_route("/api/v1/resources/items/action", &Method::GET) {
1470            RouteMatch::Found { .. } => {}
1471            _ => panic!("Should find deep route"),
1472        }
1473        match app.match_route("/api/v1/resources/info", &Method::GET) {
1474            RouteMatch::Found { .. } => {}
1475            _ => panic!("Should find mid route"),
1476        }
1477        match app.match_route("/api/status", &Method::GET) {
1478            RouteMatch::Found { .. } => {}
1479            _ => panic!("Should find shallow route"),
1480        }
1481    }
1482}
1483
1484#[cfg(test)]
1485mod property_tests {
1486    use super::*;
1487    use proptest::prelude::*;
1488    use std::panic::{catch_unwind, AssertUnwindSafe};
1489
1490    // **Feature: router-nesting, Property 2: Prefix Normalization**
1491    //
1492    // For any prefix string (with or without leading/trailing slashes), the normalized
1493    // prefix should start with exactly one slash and have no trailing slash, and all
1494    // nested routes should have properly formed paths without double slashes.
1495    //
1496    // **Validates: Requirements 1.2, 1.3**
1497    proptest! {
1498        #![proptest_config(ProptestConfig::with_cases(100))]
1499
1500        /// Property: Normalized prefix always starts with exactly one slash
1501        ///
1502        /// For any input prefix, the normalized result should always start with
1503        /// exactly one leading slash.
1504        #[test]
1505        fn prop_normalized_prefix_starts_with_single_slash(
1506            // Generate prefix with optional leading slashes
1507            leading_slashes in prop::collection::vec(Just('/'), 0..5),
1508            segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 0..4),
1509            trailing_slashes in prop::collection::vec(Just('/'), 0..5),
1510        ) {
1511            // Build the input prefix
1512            let mut prefix = String::new();
1513            for _ in &leading_slashes {
1514                prefix.push('/');
1515            }
1516            for (i, segment) in segments.iter().enumerate() {
1517                if i > 0 {
1518                    prefix.push('/');
1519                }
1520                prefix.push_str(segment);
1521            }
1522            for _ in &trailing_slashes {
1523                prefix.push('/');
1524            }
1525
1526            let normalized = normalize_prefix(&prefix);
1527
1528            // Property 1: Always starts with exactly one slash
1529            prop_assert!(
1530                normalized.starts_with('/'),
1531                "Normalized prefix '{}' should start with '/', input was '{}'",
1532                normalized, prefix
1533            );
1534
1535            // Property 2: No double slashes at the start
1536            prop_assert!(
1537                !normalized.starts_with("//"),
1538                "Normalized prefix '{}' should not start with '//', input was '{}'",
1539                normalized, prefix
1540            );
1541        }
1542
1543        /// Property: Normalized prefix has no trailing slash (unless root)
1544        ///
1545        /// For any input prefix with non-empty segments, the normalized result
1546        /// should have no trailing slash.
1547        #[test]
1548        fn prop_normalized_prefix_no_trailing_slash(
1549            segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1550            trailing_slashes in prop::collection::vec(Just('/'), 0..5),
1551        ) {
1552            // Build the input prefix with segments
1553            let mut prefix = String::from("/");
1554            for (i, segment) in segments.iter().enumerate() {
1555                if i > 0 {
1556                    prefix.push('/');
1557                }
1558                prefix.push_str(segment);
1559            }
1560            for _ in &trailing_slashes {
1561                prefix.push('/');
1562            }
1563
1564            let normalized = normalize_prefix(&prefix);
1565
1566            // Property: No trailing slash when there are segments
1567            prop_assert!(
1568                !normalized.ends_with('/'),
1569                "Normalized prefix '{}' should not end with '/', input was '{}'",
1570                normalized, prefix
1571            );
1572        }
1573
1574        /// Property: Normalized prefix has no double slashes
1575        ///
1576        /// For any input prefix, the normalized result should never contain
1577        /// consecutive slashes.
1578        #[test]
1579        fn prop_normalized_prefix_no_double_slashes(
1580            // Generate prefix with random slashes between segments
1581            segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1582            extra_slashes in prop::collection::vec(0..4usize, 1..4),
1583        ) {
1584            // Build the input prefix with extra slashes between segments
1585            let mut prefix = String::from("/");
1586            for (i, segment) in segments.iter().enumerate() {
1587                if i > 0 {
1588                    // Add extra slashes between segments
1589                    let num_slashes = extra_slashes.get(i).copied().unwrap_or(1);
1590                    for _ in 0..=num_slashes {
1591                        prefix.push('/');
1592                    }
1593                }
1594                prefix.push_str(segment);
1595            }
1596
1597            let normalized = normalize_prefix(&prefix);
1598
1599            // Property: No consecutive slashes
1600            prop_assert!(
1601                !normalized.contains("//"),
1602                "Normalized prefix '{}' should not contain '//', input was '{}'",
1603                normalized, prefix
1604            );
1605        }
1606
1607        /// Property: Prefix normalization preserves segment content
1608        ///
1609        /// For any input prefix, all non-empty segments should be preserved
1610        /// in the normalized output in the same order.
1611        #[test]
1612        fn prop_normalized_prefix_preserves_segments(
1613            segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..4),
1614        ) {
1615            // Build the input prefix
1616            let prefix = format!("/{}", segments.join("/"));
1617
1618            let normalized = normalize_prefix(&prefix);
1619
1620            // Extract segments from normalized prefix
1621            let normalized_segments: Vec<&str> = normalized
1622                .split('/')
1623                .filter(|s| !s.is_empty())
1624                .collect();
1625
1626            prop_assert_eq!(
1627                segments.len(),
1628                normalized_segments.len(),
1629                "Segment count should be preserved"
1630            );
1631
1632            for (original, normalized_seg) in segments.iter().zip(normalized_segments.iter()) {
1633                prop_assert_eq!(
1634                    original, normalized_seg,
1635                    "Segment content should be preserved"
1636                );
1637            }
1638        }
1639
1640        /// Property: Empty or slash-only input normalizes to root
1641        ///
1642        /// For any input that contains only slashes or is empty, the normalized
1643        /// result should be exactly "/".
1644        #[test]
1645        fn prop_empty_or_slashes_normalize_to_root(
1646            num_slashes in 0..10usize,
1647        ) {
1648            let prefix: String = std::iter::repeat('/').take(num_slashes).collect();
1649
1650            let normalized = normalize_prefix(&prefix);
1651
1652            prop_assert_eq!(
1653                normalized, "/",
1654                "Empty or slash-only prefix '{}' should normalize to '/'",
1655                prefix
1656            );
1657        }
1658    }
1659
1660    // **Feature: router-nesting, Property 3: HTTP Method Preservation**
1661    //
1662    // For any router with routes having multiple HTTP methods, cloning the MethodRouter
1663    // should preserve all method handlers for each route.
1664    //
1665    // **Validates: Requirements 1.4**
1666    proptest! {
1667        #![proptest_config(ProptestConfig::with_cases(100))]
1668
1669        /// Property: Cloning a MethodRouter preserves all HTTP method handlers
1670        ///
1671        /// For any combination of HTTP methods registered on a MethodRouter,
1672        /// cloning should preserve all handlers and their associated methods.
1673        #[test]
1674        fn prop_method_router_clone_preserves_methods(
1675            // Generate a random subset of HTTP methods to register
1676            use_get in any::<bool>(),
1677            use_post in any::<bool>(),
1678            use_put in any::<bool>(),
1679            use_patch in any::<bool>(),
1680            use_delete in any::<bool>(),
1681        ) {
1682            // Ensure at least one method is selected
1683            prop_assume!(use_get || use_post || use_put || use_patch || use_delete);
1684
1685            // Build a MethodRouter with the selected methods
1686            let mut method_router = MethodRouter::new();
1687            let mut expected_methods: Vec<Method> = Vec::new();
1688
1689            async fn handler() -> &'static str { "handler" }
1690
1691            if use_get {
1692                method_router = get(handler);
1693                expected_methods.push(Method::GET);
1694            }
1695
1696            if use_post {
1697                let post_router = post(handler);
1698                for (method, handler) in post_router.handlers {
1699                    method_router.handlers.insert(method.clone(), handler);
1700                    if !expected_methods.contains(&method) {
1701                        expected_methods.push(method);
1702                    }
1703                }
1704            }
1705
1706            if use_put {
1707                let put_router = put(handler);
1708                for (method, handler) in put_router.handlers {
1709                    method_router.handlers.insert(method.clone(), handler);
1710                    if !expected_methods.contains(&method) {
1711                        expected_methods.push(method);
1712                    }
1713                }
1714            }
1715
1716            if use_patch {
1717                let patch_router = patch(handler);
1718                for (method, handler) in patch_router.handlers {
1719                    method_router.handlers.insert(method.clone(), handler);
1720                    if !expected_methods.contains(&method) {
1721                        expected_methods.push(method);
1722                    }
1723                }
1724            }
1725
1726            if use_delete {
1727                let delete_router = delete(handler);
1728                for (method, handler) in delete_router.handlers {
1729                    method_router.handlers.insert(method.clone(), handler);
1730                    if !expected_methods.contains(&method) {
1731                        expected_methods.push(method);
1732                    }
1733                }
1734            }
1735
1736            // Clone the MethodRouter
1737            let cloned_router = method_router.clone();
1738
1739            // Verify all methods are preserved in the clone
1740            let original_methods = method_router.allowed_methods();
1741            let cloned_methods = cloned_router.allowed_methods();
1742
1743            prop_assert_eq!(
1744                original_methods.len(),
1745                cloned_methods.len(),
1746                "Cloned router should have same number of methods"
1747            );
1748
1749            for method in &expected_methods {
1750                prop_assert!(
1751                    cloned_router.get_handler(method).is_some(),
1752                    "Cloned router should have handler for method {:?}",
1753                    method
1754                );
1755            }
1756
1757            // Verify handlers are accessible (not null/invalid)
1758            for method in &cloned_methods {
1759                prop_assert!(
1760                    cloned_router.get_handler(method).is_some(),
1761                    "Handler for {:?} should be accessible after clone",
1762                    method
1763                );
1764            }
1765        }
1766    }
1767
1768    // **Feature: router-nesting, Property 1: Route Registration with Prefix**
1769    //
1770    // For any router with routes and any valid prefix, nesting the router should
1771    // result in all routes being registered with the prefix prepended to their
1772    // original paths.
1773    //
1774    // **Validates: Requirements 1.1**
1775    proptest! {
1776        #![proptest_config(ProptestConfig::with_cases(100))]
1777
1778        /// Property: All nested routes are registered with prefix prepended
1779        ///
1780        /// For any router with routes and any valid prefix, nesting should result
1781        /// in all routes being registered with the prefix prepended.
1782        #[test]
1783        fn prop_nested_routes_have_prefix(
1784            // Generate prefix segments
1785            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1786            // Generate route path segments
1787            route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1788            has_param in any::<bool>(),
1789        ) {
1790            async fn handler() -> &'static str { "handler" }
1791
1792            // Build the prefix
1793            let prefix = format!("/{}", prefix_segments.join("/"));
1794
1795            // Build the route path
1796            let mut route_path = format!("/{}", route_segments.join("/"));
1797            if has_param {
1798                route_path.push_str("/{id}");
1799            }
1800
1801            // Create nested router and nest it
1802            let nested_router = Router::new().route(&route_path, get(handler));
1803            let app = Router::new().nest(&prefix, nested_router);
1804
1805            // Build expected prefixed path (matchit format)
1806            let expected_matchit_path = if has_param {
1807                format!("{}/{}/:id", prefix, route_segments.join("/"))
1808            } else {
1809                format!("{}/{}", prefix, route_segments.join("/"))
1810            };
1811
1812            let routes = app.registered_routes();
1813
1814            // Property: The prefixed route should exist
1815            prop_assert!(
1816                routes.contains_key(&expected_matchit_path),
1817                "Expected route '{}' not found. Available routes: {:?}",
1818                expected_matchit_path,
1819                routes.keys().collect::<Vec<_>>()
1820            );
1821
1822            // Property: The route info should have the correct display path
1823            let route_info = routes.get(&expected_matchit_path).unwrap();
1824            let expected_display_path = format!("{}{}", prefix, route_path);
1825            prop_assert_eq!(
1826                &route_info.path, &expected_display_path,
1827                "Display path should be prefix + original path"
1828            );
1829        }
1830
1831        /// Property: Number of routes is preserved after nesting
1832        ///
1833        /// For any router with N routes, nesting should result in exactly N routes
1834        /// being registered in the parent router (assuming no conflicts).
1835        #[test]
1836        fn prop_route_count_preserved_after_nesting(
1837            // Generate number of routes (1-3 to keep test fast)
1838            num_routes in 1..4usize,
1839            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1840        ) {
1841            async fn handler() -> &'static str { "handler" }
1842
1843            let prefix = format!("/{}", prefix_segments.join("/"));
1844
1845            // Create nested router with multiple routes
1846            let mut nested_router = Router::new();
1847            for i in 0..num_routes {
1848                let path = format!("/route{}", i);
1849                nested_router = nested_router.route(&path, get(handler));
1850            }
1851
1852            let app = Router::new().nest(&prefix, nested_router);
1853
1854            prop_assert_eq!(
1855                app.registered_routes().len(),
1856                num_routes,
1857                "Number of routes should be preserved after nesting"
1858            );
1859        }
1860
1861        /// Property: Nested routes are matchable
1862        ///
1863        /// For any nested route, a request to the prefixed path should match.
1864        #[test]
1865        fn prop_nested_routes_are_matchable(
1866            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1867            route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1868        ) {
1869            async fn handler() -> &'static str { "handler" }
1870
1871            let prefix = format!("/{}", prefix_segments.join("/"));
1872            let route_path = format!("/{}", route_segments.join("/"));
1873
1874            let nested_router = Router::new().route(&route_path, get(handler));
1875            let app = Router::new().nest(&prefix, nested_router);
1876
1877            // Build the full path to match
1878            let full_path = format!("{}{}", prefix, route_path);
1879
1880            // Property: The route should be matchable
1881            match app.match_route(&full_path, &Method::GET) {
1882                RouteMatch::Found { .. } => {
1883                    // Success - route was found
1884                }
1885                RouteMatch::NotFound => {
1886                    prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
1887                }
1888                RouteMatch::MethodNotAllowed { .. } => {
1889                    prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
1890                }
1891            }
1892        }
1893    }
1894
1895    // **Feature: router-nesting, Property 9: State Merging**
1896    //
1897    // For any nested router with state, that state should be accessible via the
1898    // State extractor in handlers after nesting (assuming no type conflict with parent).
1899    //
1900    // **Validates: Requirements 3.1, 3.3**
1901    proptest! {
1902        #![proptest_config(ProptestConfig::with_cases(100))]
1903
1904        /// Property: State type IDs are merged from nested router
1905        ///
1906        /// For any nested router with state, the parent router should track
1907        /// the state type IDs after nesting.
1908        #[test]
1909        fn prop_state_type_ids_merged(
1910            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1911            has_nested_state in any::<bool>(),
1912        ) {
1913            #[derive(Clone)]
1914            struct TestState(#[allow(dead_code)] i32);
1915
1916            async fn handler() -> &'static str { "handler" }
1917
1918            let prefix = format!("/{}", prefix_segments.join("/"));
1919
1920            let mut nested = Router::new().route("/test", get(handler));
1921            if has_nested_state {
1922                nested = nested.state(TestState(42));
1923            }
1924
1925            let parent = Router::new().nest(&prefix, nested);
1926
1927            // Property: If nested had state, parent should track the type ID
1928            if has_nested_state {
1929                prop_assert!(
1930                    parent.state_type_ids().contains(&std::any::TypeId::of::<TestState>()),
1931                    "Parent should track nested state type ID"
1932                );
1933            }
1934        }
1935
1936        /// Property: State merging adds nested state to parent
1937        ///
1938        /// For any nested router with state that the parent doesn't have,
1939        /// merge_state should add that state to the parent.
1940        #[test]
1941        fn prop_merge_state_adds_nested_state(
1942            state_value in any::<i32>(),
1943        ) {
1944            #[derive(Clone, PartialEq, Debug)]
1945            struct UniqueState(i32);
1946
1947            // Create a source router with state
1948            let source = Router::new().state(UniqueState(state_value));
1949
1950            // Create a parent without this state type
1951            let parent = Router::new().merge_state::<UniqueState>(&source);
1952
1953            // Property: Parent should now have the state
1954            prop_assert!(
1955                parent.has_state::<UniqueState>(),
1956                "Parent should have state after merge"
1957            );
1958
1959            // Property: State value should match
1960            let merged_state = parent.state.get::<UniqueState>().unwrap();
1961            prop_assert_eq!(
1962                merged_state.0, state_value,
1963                "Merged state value should match source"
1964            );
1965        }
1966    }
1967
1968    // **Feature: router-nesting, Property 10: State Precedence**
1969    //
1970    // For any parent and nested router both having state of the same type,
1971    // the parent's state value should be preserved after nesting.
1972    //
1973    // **Validates: Requirements 3.2**
1974    proptest! {
1975        #![proptest_config(ProptestConfig::with_cases(100))]
1976
1977        /// Property: Parent state takes precedence over nested state
1978        ///
1979        /// For any parent and nested router both having state of the same type,
1980        /// the parent's state value should be preserved after merge_state.
1981        #[test]
1982        fn prop_parent_state_takes_precedence(
1983            parent_value in any::<i32>(),
1984            nested_value in any::<i32>(),
1985        ) {
1986            // Ensure values are different to make the test meaningful
1987            prop_assume!(parent_value != nested_value);
1988
1989            #[derive(Clone, PartialEq, Debug)]
1990            struct SharedState(i32);
1991
1992            // Create source router with nested state
1993            let source = Router::new().state(SharedState(nested_value));
1994
1995            // Create parent with its own state
1996            let parent = Router::new()
1997                .state(SharedState(parent_value))
1998                .merge_state::<SharedState>(&source);
1999
2000            // Property: Parent should still have state
2001            prop_assert!(
2002                parent.has_state::<SharedState>(),
2003                "Parent should have state"
2004            );
2005
2006            // Property: Parent's state value should be preserved (parent wins)
2007            let final_state = parent.state.get::<SharedState>().unwrap();
2008            prop_assert_eq!(
2009                final_state.0, parent_value,
2010                "Parent state value should be preserved, not overwritten by nested"
2011            );
2012        }
2013
2014        /// Property: State precedence is consistent regardless of merge order
2015        ///
2016        /// For any parent with state, merging from a source with the same type
2017        /// should always preserve the parent's value.
2018        #[test]
2019        fn prop_state_precedence_consistent(
2020            parent_value in any::<i32>(),
2021            source1_value in any::<i32>(),
2022            source2_value in any::<i32>(),
2023        ) {
2024            #[derive(Clone, PartialEq, Debug)]
2025            struct ConsistentState(i32);
2026
2027            // Create multiple source routers
2028            let source1 = Router::new().state(ConsistentState(source1_value));
2029            let source2 = Router::new().state(ConsistentState(source2_value));
2030
2031            // Create parent with its own state and merge from multiple sources
2032            let parent = Router::new()
2033                .state(ConsistentState(parent_value))
2034                .merge_state::<ConsistentState>(&source1)
2035                .merge_state::<ConsistentState>(&source2);
2036
2037            // Property: Parent's original state should be preserved
2038            let final_state = parent.state.get::<ConsistentState>().unwrap();
2039            prop_assert_eq!(
2040                final_state.0, parent_value,
2041                "Parent state should be preserved after multiple merges"
2042            );
2043        }
2044    }
2045
2046    // **Feature: phase4-ergonomics-v1, Property 1: Route Conflict Detection**
2047    //
2048    // For any two routes with the same path and HTTP method registered on the same
2049    // RustApi instance, the system should detect the conflict and report an error
2050    // at startup time.
2051    //
2052    // **Validates: Requirements 1.2**
2053    proptest! {
2054        #![proptest_config(ProptestConfig::with_cases(100))]
2055
2056        /// Property: Routes with identical path structure but different parameter names conflict
2057        ///
2058        /// For any valid path with parameters, registering two routes with the same
2059        /// structure but different parameter names should be detected as a conflict.
2060        #[test]
2061        fn prop_same_structure_different_param_names_conflict(
2062            // Generate valid path segments
2063            segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
2064            // Generate two different parameter names
2065            param1 in "[a-z][a-z0-9]{0,5}",
2066            param2 in "[a-z][a-z0-9]{0,5}",
2067        ) {
2068            // Ensure param names are different
2069            prop_assume!(param1 != param2);
2070
2071            // Build two paths with same structure but different param names
2072            let mut path1 = String::from("/");
2073            let mut path2 = String::from("/");
2074
2075            for segment in &segments {
2076                path1.push_str(segment);
2077                path1.push('/');
2078                path2.push_str(segment);
2079                path2.push('/');
2080            }
2081
2082            path1.push('{');
2083            path1.push_str(&param1);
2084            path1.push('}');
2085
2086            path2.push('{');
2087            path2.push_str(&param2);
2088            path2.push('}');
2089
2090            // Try to register both routes - should panic
2091            let result = catch_unwind(AssertUnwindSafe(|| {
2092                async fn handler1() -> &'static str { "handler1" }
2093                async fn handler2() -> &'static str { "handler2" }
2094
2095                let _router = Router::new()
2096                    .route(&path1, get(handler1))
2097                    .route(&path2, get(handler2));
2098            }));
2099
2100            prop_assert!(
2101                result.is_err(),
2102                "Routes '{}' and '{}' should conflict but didn't",
2103                path1, path2
2104            );
2105        }
2106
2107        /// Property: Routes with different path structures don't conflict
2108        ///
2109        /// For any two paths with different structures (different number of segments
2110        /// or different static segments), they should not conflict.
2111        #[test]
2112        fn prop_different_structures_no_conflict(
2113            // Generate different path segments for two routes
2114            segments1 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2115            segments2 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2116            // Optional parameter at the end
2117            has_param1 in any::<bool>(),
2118            has_param2 in any::<bool>(),
2119        ) {
2120            // Build two paths
2121            let mut path1 = String::from("/");
2122            let mut path2 = String::from("/");
2123
2124            for segment in &segments1 {
2125                path1.push_str(segment);
2126                path1.push('/');
2127            }
2128            path1.pop(); // Remove trailing slash
2129
2130            for segment in &segments2 {
2131                path2.push_str(segment);
2132                path2.push('/');
2133            }
2134            path2.pop(); // Remove trailing slash
2135
2136            if has_param1 {
2137                path1.push_str("/{id}");
2138            }
2139
2140            if has_param2 {
2141                path2.push_str("/{id}");
2142            }
2143
2144            // Normalize paths for comparison
2145            let norm1 = normalize_path_for_comparison(&convert_path_params(&path1));
2146            let norm2 = normalize_path_for_comparison(&convert_path_params(&path2));
2147
2148            // Only test if paths are actually different
2149            prop_assume!(norm1 != norm2);
2150
2151            // Try to register both routes - should succeed
2152            let result = catch_unwind(AssertUnwindSafe(|| {
2153                async fn handler1() -> &'static str { "handler1" }
2154                async fn handler2() -> &'static str { "handler2" }
2155
2156                let router = Router::new()
2157                    .route(&path1, get(handler1))
2158                    .route(&path2, get(handler2));
2159
2160                router.registered_routes().len()
2161            }));
2162
2163            prop_assert!(
2164                result.is_ok(),
2165                "Routes '{}' and '{}' should not conflict but did",
2166                path1, path2
2167            );
2168
2169            if let Ok(count) = result {
2170                prop_assert_eq!(count, 2, "Should have registered 2 routes");
2171            }
2172        }
2173
2174        /// Property: Conflict error message contains both route paths
2175        ///
2176        /// When a conflict is detected, the error message should include both
2177        /// the existing route path and the new conflicting route path.
2178        #[test]
2179        fn prop_conflict_error_contains_both_paths(
2180            // Generate a valid path segment
2181            segment in "[a-z][a-z0-9]{1,5}",
2182            param1 in "[a-z][a-z0-9]{1,5}",
2183            param2 in "[a-z][a-z0-9]{1,5}",
2184        ) {
2185            prop_assume!(param1 != param2);
2186
2187            let path1 = format!("/{}/{{{}}}", segment, param1);
2188            let path2 = format!("/{}/{{{}}}", segment, param2);
2189
2190            let result = catch_unwind(AssertUnwindSafe(|| {
2191                async fn handler1() -> &'static str { "handler1" }
2192                async fn handler2() -> &'static str { "handler2" }
2193
2194                let _router = Router::new()
2195                    .route(&path1, get(handler1))
2196                    .route(&path2, get(handler2));
2197            }));
2198
2199            prop_assert!(result.is_err(), "Should have panicked due to conflict");
2200
2201            // Check that the panic message contains useful information
2202            if let Err(panic_info) = result {
2203                if let Some(msg) = panic_info.downcast_ref::<String>() {
2204                    prop_assert!(
2205                        msg.contains("ROUTE CONFLICT DETECTED"),
2206                        "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
2207                        msg
2208                    );
2209                    prop_assert!(
2210                        msg.contains("Existing:") && msg.contains("New:"),
2211                        "Error should contain both 'Existing:' and 'New:' labels, got: {}",
2212                        msg
2213                    );
2214                    prop_assert!(
2215                        msg.contains("How to resolve:"),
2216                        "Error should contain resolution guidance, got: {}",
2217                        msg
2218                    );
2219                }
2220            }
2221        }
2222
2223        /// Property: Exact duplicate paths conflict
2224        ///
2225        /// Registering the exact same path twice should always be detected as a conflict.
2226        #[test]
2227        fn prop_exact_duplicate_paths_conflict(
2228            // Generate valid path segments
2229            segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
2230            has_param in any::<bool>(),
2231        ) {
2232            // Build a path
2233            let mut path = String::from("/");
2234
2235            for segment in &segments {
2236                path.push_str(segment);
2237                path.push('/');
2238            }
2239            path.pop(); // Remove trailing slash
2240
2241            if has_param {
2242                path.push_str("/{id}");
2243            }
2244
2245            // Try to register the same path twice - should panic
2246            let result = catch_unwind(AssertUnwindSafe(|| {
2247                async fn handler1() -> &'static str { "handler1" }
2248                async fn handler2() -> &'static str { "handler2" }
2249
2250                let _router = Router::new()
2251                    .route(&path, get(handler1))
2252                    .route(&path, get(handler2));
2253            }));
2254
2255            prop_assert!(
2256                result.is_err(),
2257                "Registering path '{}' twice should conflict but didn't",
2258                path
2259            );
2260        }
2261    }
2262
2263    // **Feature: router-nesting, Property 5: Nested Route Matching**
2264    //
2265    // For any nested route and a request with a matching path and method,
2266    // the router should return the correct handler.
2267    //
2268    // **Validates: Requirements 2.1**
2269    proptest! {
2270        #![proptest_config(ProptestConfig::with_cases(100))]
2271
2272        /// Property: Nested routes with path parameters are correctly matched
2273        ///
2274        /// For any nested route with path parameters, a request to the prefixed path
2275        /// with valid parameter values should match and return Found.
2276        #[test]
2277        fn prop_nested_route_with_params_matches(
2278            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2279            route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
2280            param_value in "[a-z0-9]{1,10}",
2281        ) {
2282            async fn handler() -> &'static str { "handler" }
2283
2284            let prefix = format!("/{}", prefix_segments.join("/"));
2285            let route_path = if route_segments.is_empty() {
2286                "/{id}".to_string()
2287            } else {
2288                format!("/{}/{{id}}", route_segments.join("/"))
2289            };
2290
2291            let nested_router = Router::new().route(&route_path, get(handler));
2292            let app = Router::new().nest(&prefix, nested_router);
2293
2294            // Build the full path to match with actual parameter value
2295            let full_path = if route_segments.is_empty() {
2296                format!("{}/{}", prefix, param_value)
2297            } else {
2298                format!("{}/{}/{}", prefix, route_segments.join("/"), param_value)
2299            };
2300
2301            // Property: The route should be matched
2302            match app.match_route(&full_path, &Method::GET) {
2303                RouteMatch::Found { params, .. } => {
2304                    // Verify the parameter was extracted
2305                    prop_assert!(
2306                        params.contains_key("id"),
2307                        "Should have 'id' parameter, got: {:?}",
2308                        params
2309                    );
2310                    prop_assert_eq!(
2311                        params.get("id").unwrap(),
2312                        &param_value,
2313                        "Parameter value should match"
2314                    );
2315                }
2316                RouteMatch::NotFound => {
2317                    prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
2318                }
2319                RouteMatch::MethodNotAllowed { .. } => {
2320                    prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
2321                }
2322            }
2323        }
2324
2325        /// Property: Nested routes match correct HTTP method
2326        ///
2327        /// For any nested route registered with a specific HTTP method, only requests
2328        /// with that method should return Found.
2329        #[test]
2330        fn prop_nested_route_matches_correct_method(
2331            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2332            route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2333            use_get in any::<bool>(),
2334        ) {
2335            async fn handler() -> &'static str { "handler" }
2336
2337            let prefix = format!("/{}", prefix_segments.join("/"));
2338            let route_path = format!("/{}", route_segments.join("/"));
2339
2340            // Register with either GET or POST
2341            let method_router = if use_get { get(handler) } else { post(handler) };
2342            let nested_router = Router::new().route(&route_path, method_router);
2343            let app = Router::new().nest(&prefix, nested_router);
2344
2345            let full_path = format!("{}{}", prefix, route_path);
2346            let registered_method = if use_get { Method::GET } else { Method::POST };
2347            let other_method = if use_get { Method::POST } else { Method::GET };
2348
2349            // Property: Registered method should match
2350            match app.match_route(&full_path, &registered_method) {
2351                RouteMatch::Found { .. } => {
2352                    // Success
2353                }
2354                other => {
2355                    prop_assert!(false, "Route should be found for registered method, got: {:?}",
2356                        match other {
2357                            RouteMatch::NotFound => "NotFound",
2358                            RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
2359                            _ => "Found",
2360                        }
2361                    );
2362                }
2363            }
2364
2365            // Property: Other method should return MethodNotAllowed
2366            match app.match_route(&full_path, &other_method) {
2367                RouteMatch::MethodNotAllowed { allowed } => {
2368                    prop_assert!(
2369                        allowed.contains(&registered_method),
2370                        "Allowed methods should contain {:?}",
2371                        registered_method
2372                    );
2373                }
2374                other => {
2375                    prop_assert!(false, "Route should return MethodNotAllowed for other method, got: {:?}",
2376                        match other {
2377                            RouteMatch::NotFound => "NotFound",
2378                            RouteMatch::Found { .. } => "Found",
2379                            _ => "MethodNotAllowed",
2380                        }
2381                    );
2382                }
2383            }
2384        }
2385    }
2386
2387    // **Feature: router-nesting, Property 6: Path Parameter Extraction**
2388    //
2389    // For any nested route with path parameters and a matching request,
2390    // the extracted parameters should have the correct names and values.
2391    //
2392    // **Validates: Requirements 2.2**
2393    proptest! {
2394        #![proptest_config(ProptestConfig::with_cases(100))]
2395
2396        /// Property: Single path parameter is correctly extracted from nested route
2397        ///
2398        /// For any nested route with a single path parameter, the parameter name
2399        /// and value should be correctly extracted.
2400        #[test]
2401        fn prop_single_param_extraction(
2402            prefix in "[a-z][a-z0-9]{1,5}",
2403            param_name in "[a-z][a-z0-9]{1,5}",
2404            param_value in "[a-z0-9]{1,10}",
2405        ) {
2406            async fn handler() -> &'static str { "handler" }
2407
2408            let prefix = format!("/{}", prefix);
2409            let route_path = format!("/{{{}}}", param_name);
2410
2411            let nested_router = Router::new().route(&route_path, get(handler));
2412            let app = Router::new().nest(&prefix, nested_router);
2413
2414            let full_path = format!("{}/{}", prefix, param_value);
2415
2416            match app.match_route(&full_path, &Method::GET) {
2417                RouteMatch::Found { params, .. } => {
2418                    prop_assert!(
2419                        params.contains_key(&param_name),
2420                        "Should have '{}' parameter, got: {:?}",
2421                        param_name, params
2422                    );
2423                    prop_assert_eq!(
2424                        params.get(&param_name).unwrap(),
2425                        &param_value,
2426                        "Parameter '{}' value should be '{}'",
2427                        param_name, param_value
2428                    );
2429                }
2430                _ => {
2431                    prop_assert!(false, "Route should be found");
2432                }
2433            }
2434        }
2435
2436        /// Property: Multiple path parameters are correctly extracted from nested route
2437        ///
2438        /// For any nested route with multiple path parameters, all parameters
2439        /// should be correctly extracted with their names and values.
2440        #[test]
2441        fn prop_multiple_params_extraction(
2442            prefix in "[a-z][a-z0-9]{1,5}",
2443            param1_name in "[a-z]{1,5}",
2444            param1_value in "[a-z0-9]{1,10}",
2445            param2_name in "[a-z]{1,5}",
2446            param2_value in "[a-z0-9]{1,10}",
2447        ) {
2448            // Ensure param names are different
2449            prop_assume!(param1_name != param2_name);
2450
2451            async fn handler() -> &'static str { "handler" }
2452
2453            let prefix = format!("/{}", prefix);
2454            let route_path = format!("/{{{}}}/items/{{{}}}", param1_name, param2_name);
2455
2456            let nested_router = Router::new().route(&route_path, get(handler));
2457            let app = Router::new().nest(&prefix, nested_router);
2458
2459            let full_path = format!("{}/{}/items/{}", prefix, param1_value, param2_value);
2460
2461            match app.match_route(&full_path, &Method::GET) {
2462                RouteMatch::Found { params, .. } => {
2463                    // Check first parameter
2464                    prop_assert!(
2465                        params.contains_key(&param1_name),
2466                        "Should have '{}' parameter, got: {:?}",
2467                        param1_name, params
2468                    );
2469                    prop_assert_eq!(
2470                        params.get(&param1_name).unwrap(),
2471                        &param1_value,
2472                        "Parameter '{}' value should be '{}'",
2473                        param1_name, param1_value
2474                    );
2475
2476                    // Check second parameter
2477                    prop_assert!(
2478                        params.contains_key(&param2_name),
2479                        "Should have '{}' parameter, got: {:?}",
2480                        param2_name, params
2481                    );
2482                    prop_assert_eq!(
2483                        params.get(&param2_name).unwrap(),
2484                        &param2_value,
2485                        "Parameter '{}' value should be '{}'",
2486                        param2_name, param2_value
2487                    );
2488                }
2489                _ => {
2490                    prop_assert!(false, "Route should be found");
2491                }
2492            }
2493        }
2494
2495        /// Property: Path parameters preserve special characters in values
2496        ///
2497        /// For any nested route with path parameters, parameter values containing
2498        /// URL-safe special characters should be preserved correctly.
2499        #[test]
2500        fn prop_param_value_preservation(
2501            prefix in "[a-z]{1,5}",
2502            // Generate values with alphanumeric and some special chars
2503            param_value in "[a-zA-Z0-9_-]{1,15}",
2504        ) {
2505            async fn handler() -> &'static str { "handler" }
2506
2507            let prefix = format!("/{}", prefix);
2508            let route_path = "/{id}".to_string();
2509
2510            let nested_router = Router::new().route(&route_path, get(handler));
2511            let app = Router::new().nest(&prefix, nested_router);
2512
2513            let full_path = format!("{}/{}", prefix, param_value);
2514
2515            match app.match_route(&full_path, &Method::GET) {
2516                RouteMatch::Found { params, .. } => {
2517                    prop_assert_eq!(
2518                        params.get("id").unwrap(),
2519                        &param_value,
2520                        "Parameter value should be preserved exactly"
2521                    );
2522                }
2523                _ => {
2524                    prop_assert!(false, "Route should be found");
2525                }
2526            }
2527        }
2528    }
2529
2530    // **Feature: router-nesting, Property 7: Not Found Response**
2531    //
2532    // For any request path that doesn't match any registered route (nested or otherwise),
2533    // the router should return NotFound.
2534    //
2535    // **Validates: Requirements 2.3**
2536    proptest! {
2537        #![proptest_config(ProptestConfig::with_cases(100))]
2538
2539        /// Property: Unregistered paths return NotFound
2540        ///
2541        /// For any path that doesn't match any registered route, the router
2542        /// should return NotFound.
2543        #[test]
2544        fn prop_unregistered_path_returns_not_found(
2545            prefix in "[a-z][a-z0-9]{1,5}",
2546            route_segment in "[a-z][a-z0-9]{1,5}",
2547            unregistered_segment in "[a-z][a-z0-9]{6,10}",
2548        ) {
2549            // Ensure segments are different
2550            prop_assume!(route_segment != unregistered_segment);
2551
2552            async fn handler() -> &'static str { "handler" }
2553
2554            let prefix = format!("/{}", prefix);
2555            let route_path = format!("/{}", route_segment);
2556
2557            let nested_router = Router::new().route(&route_path, get(handler));
2558            let app = Router::new().nest(&prefix, nested_router);
2559
2560            // Try to match an unregistered path
2561            let unregistered_path = format!("{}/{}", prefix, unregistered_segment);
2562
2563            match app.match_route(&unregistered_path, &Method::GET) {
2564                RouteMatch::NotFound => {
2565                    // Success - this is expected
2566                }
2567                RouteMatch::Found { .. } => {
2568                    prop_assert!(false, "Path '{}' should not be found", unregistered_path);
2569                }
2570                RouteMatch::MethodNotAllowed { .. } => {
2571                    prop_assert!(false, "Path '{}' should return NotFound, not MethodNotAllowed", unregistered_path);
2572                }
2573            }
2574        }
2575
2576        /// Property: Wrong prefix returns NotFound
2577        ///
2578        /// For any nested route, a request with a different prefix should return NotFound.
2579        #[test]
2580        fn prop_wrong_prefix_returns_not_found(
2581            prefix1 in "[a-z][a-z0-9]{1,5}",
2582            prefix2 in "[a-z][a-z0-9]{6,10}",
2583            route_segment in "[a-z][a-z0-9]{1,5}",
2584        ) {
2585            // Ensure prefixes are different
2586            prop_assume!(prefix1 != prefix2);
2587
2588            async fn handler() -> &'static str { "handler" }
2589
2590            let prefix = format!("/{}", prefix1);
2591            let route_path = format!("/{}", route_segment);
2592
2593            let nested_router = Router::new().route(&route_path, get(handler));
2594            let app = Router::new().nest(&prefix, nested_router);
2595
2596            // Try to match with wrong prefix
2597            let wrong_prefix_path = format!("/{}/{}", prefix2, route_segment);
2598
2599            match app.match_route(&wrong_prefix_path, &Method::GET) {
2600                RouteMatch::NotFound => {
2601                    // Success - this is expected
2602                }
2603                _ => {
2604                    prop_assert!(false, "Path '{}' with wrong prefix should return NotFound", wrong_prefix_path);
2605                }
2606            }
2607        }
2608
2609        /// Property: Partial path match returns NotFound
2610        ///
2611        /// For any nested route with multiple segments, a request matching only
2612        /// part of the path should return NotFound.
2613        #[test]
2614        fn prop_partial_path_returns_not_found(
2615            prefix in "[a-z][a-z0-9]{1,5}",
2616            segment1 in "[a-z][a-z0-9]{1,5}",
2617            segment2 in "[a-z][a-z0-9]{1,5}",
2618        ) {
2619            async fn handler() -> &'static str { "handler" }
2620
2621            let prefix = format!("/{}", prefix);
2622            let route_path = format!("/{}/{}", segment1, segment2);
2623
2624            let nested_router = Router::new().route(&route_path, get(handler));
2625            let app = Router::new().nest(&prefix, nested_router);
2626
2627            // Try to match only the first segment (partial path)
2628            let partial_path = format!("{}/{}", prefix, segment1);
2629
2630            match app.match_route(&partial_path, &Method::GET) {
2631                RouteMatch::NotFound => {
2632                    // Success - partial path should not match
2633                }
2634                _ => {
2635                    prop_assert!(false, "Partial path '{}' should return NotFound", partial_path);
2636                }
2637            }
2638        }
2639    }
2640
2641    // **Feature: router-nesting, Property 8: Method Not Allowed Response**
2642    //
2643    // For any request to a valid path but with an unregistered HTTP method,
2644    // the router should return MethodNotAllowed with the list of allowed methods.
2645    //
2646    // **Validates: Requirements 2.4**
2647    proptest! {
2648        #![proptest_config(ProptestConfig::with_cases(100))]
2649
2650        /// Property: Unregistered method returns MethodNotAllowed with allowed methods
2651        ///
2652        /// For any nested route registered with specific methods, a request with
2653        /// an unregistered method should return MethodNotAllowed with the correct
2654        /// list of allowed methods.
2655        #[test]
2656        fn prop_unregistered_method_returns_method_not_allowed(
2657            prefix in "[a-z][a-z0-9]{1,5}",
2658            route_segment in "[a-z][a-z0-9]{1,5}",
2659        ) {
2660            async fn handler() -> &'static str { "handler" }
2661
2662            let prefix = format!("/{}", prefix);
2663            let route_path = format!("/{}", route_segment);
2664
2665            // Register only GET
2666            let nested_router = Router::new().route(&route_path, get(handler));
2667            let app = Router::new().nest(&prefix, nested_router);
2668
2669            let full_path = format!("{}{}", prefix, route_path);
2670
2671            // Try POST on a GET-only route
2672            match app.match_route(&full_path, &Method::POST) {
2673                RouteMatch::MethodNotAllowed { allowed } => {
2674                    prop_assert!(
2675                        allowed.contains(&Method::GET),
2676                        "Allowed methods should contain GET, got: {:?}",
2677                        allowed
2678                    );
2679                    prop_assert!(
2680                        !allowed.contains(&Method::POST),
2681                        "Allowed methods should not contain POST"
2682                    );
2683                }
2684                RouteMatch::Found { .. } => {
2685                    prop_assert!(false, "POST should not be found on GET-only route");
2686                }
2687                RouteMatch::NotFound => {
2688                    prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
2689                }
2690            }
2691        }
2692
2693        /// Property: Multiple registered methods are all returned in allowed list
2694        ///
2695        /// For any nested route registered with multiple methods, the MethodNotAllowed
2696        /// response should include all registered methods.
2697        #[test]
2698        fn prop_multiple_methods_in_allowed_list(
2699            prefix in "[a-z][a-z0-9]{1,5}",
2700            route_segment in "[a-z][a-z0-9]{1,5}",
2701            use_get in any::<bool>(),
2702            use_post in any::<bool>(),
2703            use_put in any::<bool>(),
2704        ) {
2705            // Ensure at least one method is registered
2706            prop_assume!(use_get || use_post || use_put);
2707
2708            async fn handler() -> &'static str { "handler" }
2709
2710            let prefix = format!("/{}", prefix);
2711            let route_path = format!("/{}", route_segment);
2712
2713            // Build method router with selected methods
2714            let mut method_router = MethodRouter::new();
2715            let mut expected_methods: Vec<Method> = Vec::new();
2716
2717            if use_get {
2718                let get_router = get(handler);
2719                for (method, h) in get_router.handlers {
2720                    method_router.handlers.insert(method.clone(), h);
2721                    expected_methods.push(method);
2722                }
2723            }
2724            if use_post {
2725                let post_router = post(handler);
2726                for (method, h) in post_router.handlers {
2727                    method_router.handlers.insert(method.clone(), h);
2728                    expected_methods.push(method);
2729                }
2730            }
2731            if use_put {
2732                let put_router = put(handler);
2733                for (method, h) in put_router.handlers {
2734                    method_router.handlers.insert(method.clone(), h);
2735                    expected_methods.push(method);
2736                }
2737            }
2738
2739            let nested_router = Router::new().route(&route_path, method_router);
2740            let app = Router::new().nest(&prefix, nested_router);
2741
2742            let full_path = format!("{}{}", prefix, route_path);
2743
2744            // Try DELETE (which we never register)
2745            match app.match_route(&full_path, &Method::DELETE) {
2746                RouteMatch::MethodNotAllowed { allowed } => {
2747                    // All registered methods should be in allowed list
2748                    for method in &expected_methods {
2749                        prop_assert!(
2750                            allowed.contains(method),
2751                            "Allowed methods should contain {:?}, got: {:?}",
2752                            method, allowed
2753                        );
2754                    }
2755                    // DELETE should not be in allowed list
2756                    prop_assert!(
2757                        !allowed.contains(&Method::DELETE),
2758                        "Allowed methods should not contain DELETE"
2759                    );
2760                }
2761                RouteMatch::Found { .. } => {
2762                    prop_assert!(false, "DELETE should not be found");
2763                }
2764                RouteMatch::NotFound => {
2765                    prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
2766                }
2767            }
2768        }
2769    }
2770
2771    // **Feature: router-nesting, Property 12: Conflict Detection**
2772    //
2773    // For any nested route that conflicts with an existing route (same path structure),
2774    // the router should detect and report the conflict with both route paths.
2775    //
2776    // **Validates: Requirements 5.1, 5.3**
2777
2778    // **Feature: router-nesting, Property 4: Multiple Router Composition**
2779    //
2780    // For any set of routers with non-overlapping route structures nested under
2781    // different prefixes, all routes should be registered without conflicts.
2782    //
2783    // **Validates: Requirements 1.5**
2784    proptest! {
2785        #![proptest_config(ProptestConfig::with_cases(100))]
2786
2787        /// Property: Multiple routers nested under different prefixes register all routes
2788        ///
2789        /// For any set of routers with routes nested under different prefixes,
2790        /// all routes should be registered and the total count should equal the
2791        /// sum of routes from all nested routers.
2792        #[test]
2793        fn prop_multiple_routers_all_routes_registered(
2794            // Generate 2-3 different prefixes
2795            prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2796            prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2797            // Generate route counts for each router (1-3 routes each)
2798            num_routes1 in 1..4usize,
2799            num_routes2 in 1..4usize,
2800        ) {
2801            // Build prefixes
2802            let prefix1 = format!("/{}", prefix1_segments.join("/"));
2803            let prefix2 = format!("/{}", prefix2_segments.join("/"));
2804
2805            // Ensure prefixes are different
2806            prop_assume!(prefix1 != prefix2);
2807
2808            async fn handler() -> &'static str { "handler" }
2809
2810            // Create first router with routes
2811            let mut router1 = Router::new();
2812            for i in 0..num_routes1 {
2813                let path = format!("/route1_{}", i);
2814                router1 = router1.route(&path, get(handler));
2815            }
2816
2817            // Create second router with routes
2818            let mut router2 = Router::new();
2819            for i in 0..num_routes2 {
2820                let path = format!("/route2_{}", i);
2821                router2 = router2.route(&path, get(handler));
2822            }
2823
2824            // Nest both routers
2825            let app = Router::new()
2826                .nest(&prefix1, router1)
2827                .nest(&prefix2, router2);
2828
2829            let routes = app.registered_routes();
2830
2831            // Property: Total route count should equal sum of all nested routes
2832            let expected_count = num_routes1 + num_routes2;
2833            prop_assert_eq!(
2834                routes.len(),
2835                expected_count,
2836                "Should have {} routes ({}+{}), got {}",
2837                expected_count, num_routes1, num_routes2, routes.len()
2838            );
2839
2840            // Property: All routes from router1 should be registered with prefix1
2841            for i in 0..num_routes1 {
2842                let expected_path = format!("{}/route1_{}", prefix1, i);
2843                let matchit_path = convert_path_params(&expected_path);
2844                prop_assert!(
2845                    routes.contains_key(&matchit_path),
2846                    "Route '{}' should be registered",
2847                    expected_path
2848                );
2849            }
2850
2851            // Property: All routes from router2 should be registered with prefix2
2852            for i in 0..num_routes2 {
2853                let expected_path = format!("{}/route2_{}", prefix2, i);
2854                let matchit_path = convert_path_params(&expected_path);
2855                prop_assert!(
2856                    routes.contains_key(&matchit_path),
2857                    "Route '{}' should be registered",
2858                    expected_path
2859                );
2860            }
2861        }
2862
2863        /// Property: Multiple routers with same internal routes don't interfere
2864        ///
2865        /// For any set of routers with identical internal route structures nested
2866        /// under different prefixes, all routes should be independently matchable.
2867        #[test]
2868        fn prop_multiple_routers_no_interference(
2869            prefix1 in "[a-z][a-z0-9]{1,5}",
2870            prefix2 in "[a-z][a-z0-9]{1,5}",
2871            route_segment in "[a-z][a-z0-9]{1,5}",
2872            param_value1 in "[a-z0-9]{1,10}",
2873            param_value2 in "[a-z0-9]{1,10}",
2874        ) {
2875            // Ensure prefixes are different
2876            prop_assume!(prefix1 != prefix2);
2877
2878            let prefix1 = format!("/{}", prefix1);
2879            let prefix2 = format!("/{}", prefix2);
2880
2881            async fn handler() -> &'static str { "handler" }
2882
2883            // Create two routers with identical internal structure
2884            let router1 = Router::new()
2885                .route(&format!("/{}", route_segment), get(handler))
2886                .route("/{id}", get(handler));
2887
2888            let router2 = Router::new()
2889                .route(&format!("/{}", route_segment), get(handler))
2890                .route("/{id}", get(handler));
2891
2892            // Nest both routers
2893            let app = Router::new()
2894                .nest(&prefix1, router1)
2895                .nest(&prefix2, router2);
2896
2897            // Property: Routes under prefix1 should be matchable
2898            let path1_static = format!("{}/{}", prefix1, route_segment);
2899            match app.match_route(&path1_static, &Method::GET) {
2900                RouteMatch::Found { params, .. } => {
2901                    prop_assert!(params.is_empty(), "Static path should have no params");
2902                }
2903                _ => {
2904                    prop_assert!(false, "Route '{}' should be found", path1_static);
2905                }
2906            }
2907
2908            let path1_param = format!("{}/{}", prefix1, param_value1);
2909            match app.match_route(&path1_param, &Method::GET) {
2910                RouteMatch::Found { params, .. } => {
2911                    prop_assert_eq!(
2912                        params.get("id"),
2913                        Some(&param_value1.to_string()),
2914                        "Parameter should be extracted correctly"
2915                    );
2916                }
2917                _ => {
2918                    prop_assert!(false, "Route '{}' should be found", path1_param);
2919                }
2920            }
2921
2922            // Property: Routes under prefix2 should be matchable independently
2923            let path2_static = format!("{}/{}", prefix2, route_segment);
2924            match app.match_route(&path2_static, &Method::GET) {
2925                RouteMatch::Found { params, .. } => {
2926                    prop_assert!(params.is_empty(), "Static path should have no params");
2927                }
2928                _ => {
2929                    prop_assert!(false, "Route '{}' should be found", path2_static);
2930                }
2931            }
2932
2933            let path2_param = format!("{}/{}", prefix2, param_value2);
2934            match app.match_route(&path2_param, &Method::GET) {
2935                RouteMatch::Found { params, .. } => {
2936                    prop_assert_eq!(
2937                        params.get("id"),
2938                        Some(&param_value2.to_string()),
2939                        "Parameter should be extracted correctly"
2940                    );
2941                }
2942                _ => {
2943                    prop_assert!(false, "Route '{}' should be found", path2_param);
2944                }
2945            }
2946        }
2947
2948        /// Property: Multiple routers preserve HTTP methods independently
2949        ///
2950        /// For any set of routers with different HTTP methods nested under different
2951        /// prefixes, each route should preserve its own set of allowed methods.
2952        #[test]
2953        fn prop_multiple_routers_preserve_methods(
2954            prefix1 in "[a-z][a-z0-9]{1,5}",
2955            prefix2 in "[a-z][a-z0-9]{1,5}",
2956            route_segment in "[a-z][a-z0-9]{1,5}",
2957            router1_use_get in any::<bool>(),
2958            router1_use_post in any::<bool>(),
2959            router2_use_get in any::<bool>(),
2960            router2_use_put in any::<bool>(),
2961        ) {
2962            // Ensure at least one method per router
2963            prop_assume!(router1_use_get || router1_use_post);
2964            prop_assume!(router2_use_get || router2_use_put);
2965            // Ensure prefixes are different
2966            prop_assume!(prefix1 != prefix2);
2967
2968            let prefix1 = format!("/{}", prefix1);
2969            let prefix2 = format!("/{}", prefix2);
2970            let route_path = format!("/{}", route_segment);
2971
2972            async fn handler() -> &'static str { "handler" }
2973
2974            // Build router1 with selected methods
2975            let mut method_router1 = MethodRouter::new();
2976            let mut expected_methods1: Vec<Method> = Vec::new();
2977            if router1_use_get {
2978                let get_router = get(handler);
2979                for (method, h) in get_router.handlers {
2980                    method_router1.handlers.insert(method.clone(), h);
2981                    expected_methods1.push(method);
2982                }
2983            }
2984            if router1_use_post {
2985                let post_router = post(handler);
2986                for (method, h) in post_router.handlers {
2987                    method_router1.handlers.insert(method.clone(), h);
2988                    expected_methods1.push(method);
2989                }
2990            }
2991
2992            // Build router2 with selected methods
2993            let mut method_router2 = MethodRouter::new();
2994            let mut expected_methods2: Vec<Method> = Vec::new();
2995            if router2_use_get {
2996                let get_router = get(handler);
2997                for (method, h) in get_router.handlers {
2998                    method_router2.handlers.insert(method.clone(), h);
2999                    expected_methods2.push(method);
3000                }
3001            }
3002            if router2_use_put {
3003                let put_router = put(handler);
3004                for (method, h) in put_router.handlers {
3005                    method_router2.handlers.insert(method.clone(), h);
3006                    expected_methods2.push(method);
3007                }
3008            }
3009
3010            let router1 = Router::new().route(&route_path, method_router1);
3011            let router2 = Router::new().route(&route_path, method_router2);
3012
3013            let app = Router::new()
3014                .nest(&prefix1, router1)
3015                .nest(&prefix2, router2);
3016
3017            let full_path1 = format!("{}{}", prefix1, route_path);
3018            let full_path2 = format!("{}{}", prefix2, route_path);
3019
3020            // Property: Router1's methods should be preserved
3021            for method in &expected_methods1 {
3022                match app.match_route(&full_path1, method) {
3023                    RouteMatch::Found { .. } => {}
3024                    _ => {
3025                        prop_assert!(false, "Method {:?} should be found for {}", method, full_path1);
3026                    }
3027                }
3028            }
3029
3030            // Property: Router2's methods should be preserved
3031            for method in &expected_methods2 {
3032                match app.match_route(&full_path2, method) {
3033                    RouteMatch::Found { .. } => {}
3034                    _ => {
3035                        prop_assert!(false, "Method {:?} should be found for {}", method, full_path2);
3036                    }
3037                }
3038            }
3039
3040            // Property: Methods not registered should return MethodNotAllowed
3041            if !expected_methods1.contains(&Method::DELETE) {
3042                match app.match_route(&full_path1, &Method::DELETE) {
3043                    RouteMatch::MethodNotAllowed { allowed } => {
3044                        for method in &expected_methods1 {
3045                            prop_assert!(
3046                                allowed.contains(method),
3047                                "Allowed methods for {} should contain {:?}",
3048                                full_path1, method
3049                            );
3050                        }
3051                    }
3052                    _ => {
3053                        prop_assert!(false, "DELETE should return MethodNotAllowed for {}", full_path1);
3054                    }
3055                }
3056            }
3057        }
3058
3059        /// Property: Three or more routers can be composed without conflicts
3060        ///
3061        /// For any set of three routers nested under different prefixes,
3062        /// all routes should be registered without conflicts.
3063        #[test]
3064        fn prop_three_routers_composition(
3065            prefix1 in "[a-z]{1,3}",
3066            prefix2 in "[a-z]{4,6}",
3067            prefix3 in "[a-z]{7,9}",
3068            num_routes in 1..3usize,
3069        ) {
3070            let prefix1 = format!("/{}", prefix1);
3071            let prefix2 = format!("/{}", prefix2);
3072            let prefix3 = format!("/{}", prefix3);
3073
3074            async fn handler() -> &'static str { "handler" }
3075
3076            // Create three routers with same number of routes
3077            let mut router1 = Router::new();
3078            let mut router2 = Router::new();
3079            let mut router3 = Router::new();
3080
3081            for i in 0..num_routes {
3082                let path = format!("/item{}", i);
3083                router1 = router1.route(&path, get(handler));
3084                router2 = router2.route(&path, get(handler));
3085                router3 = router3.route(&path, get(handler));
3086            }
3087
3088            // Nest all three routers
3089            let app = Router::new()
3090                .nest(&prefix1, router1)
3091                .nest(&prefix2, router2)
3092                .nest(&prefix3, router3);
3093
3094            let routes = app.registered_routes();
3095
3096            // Property: Total route count should be 3 * num_routes
3097            let expected_count = 3 * num_routes;
3098            prop_assert_eq!(
3099                routes.len(),
3100                expected_count,
3101                "Should have {} routes, got {}",
3102                expected_count, routes.len()
3103            );
3104
3105            // Property: All routes should be matchable
3106            for i in 0..num_routes {
3107                let path1 = format!("{}/item{}", prefix1, i);
3108                let path2 = format!("{}/item{}", prefix2, i);
3109                let path3 = format!("{}/item{}", prefix3, i);
3110
3111                match app.match_route(&path1, &Method::GET) {
3112                    RouteMatch::Found { .. } => {}
3113                    _ => prop_assert!(false, "Route '{}' should be found", path1),
3114                }
3115                match app.match_route(&path2, &Method::GET) {
3116                    RouteMatch::Found { .. } => {}
3117                    _ => prop_assert!(false, "Route '{}' should be found", path2),
3118                }
3119                match app.match_route(&path3, &Method::GET) {
3120                    RouteMatch::Found { .. } => {}
3121                    _ => prop_assert!(false, "Route '{}' should be found", path3),
3122                }
3123            }
3124        }
3125    }
3126    proptest! {
3127        #![proptest_config(ProptestConfig::with_cases(100))]
3128
3129        /// Property: Nested routes with same path structure but different param names conflict
3130        ///
3131        /// For any existing route with a parameter and a nested route that would create
3132        /// the same path structure with a different parameter name, the router should
3133        /// detect and report the conflict.
3134        #[test]
3135        fn prop_nested_route_conflict_different_param_names(
3136            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3137            route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
3138            param1 in "[a-z][a-z0-9]{1,5}",
3139            param2 in "[a-z][a-z0-9]{1,5}",
3140        ) {
3141            // Ensure param names are different
3142            prop_assume!(param1 != param2);
3143
3144            async fn handler1() -> &'static str { "handler1" }
3145            async fn handler2() -> &'static str { "handler2" }
3146
3147            let prefix = format!("/{}", prefix_segments.join("/"));
3148
3149            // Build the existing route path (with param1)
3150            let existing_path = if route_segments.is_empty() {
3151                format!("{}/{{{}}}", prefix, param1)
3152            } else {
3153                format!("{}/{}/{{{}}}", prefix, route_segments.join("/"), param1)
3154            };
3155
3156            // Build the nested route path (with param2)
3157            let nested_path = if route_segments.is_empty() {
3158                format!("/{{{}}}", param2)
3159            } else {
3160                format!("/{}/{{{}}}", route_segments.join("/"), param2)
3161            };
3162
3163            // Try to create a conflict
3164            let result = catch_unwind(AssertUnwindSafe(|| {
3165                let parent = Router::new().route(&existing_path, get(handler1));
3166                let nested = Router::new().route(&nested_path, get(handler2));
3167                let _app = parent.nest(&prefix, nested);
3168            }));
3169
3170            // Property: Should detect conflict
3171            prop_assert!(
3172                result.is_err(),
3173                "Nested route '{}{}' should conflict with existing route '{}' but didn't",
3174                prefix, nested_path, existing_path
3175            );
3176
3177            // Property: Error message should contain conflict information
3178            if let Err(panic_info) = result {
3179                if let Some(msg) = panic_info.downcast_ref::<String>() {
3180                    prop_assert!(
3181                        msg.contains("ROUTE CONFLICT DETECTED"),
3182                        "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
3183                        msg
3184                    );
3185                    prop_assert!(
3186                        msg.contains("Existing:") && msg.contains("New:"),
3187                        "Error should contain both 'Existing:' and 'New:' labels, got: {}",
3188                        msg
3189                    );
3190                }
3191            }
3192        }
3193
3194        /// Property: Nested routes with exact same path conflict
3195        ///
3196        /// For any existing route and a nested route that would create the exact
3197        /// same path, the router should detect and report the conflict.
3198        #[test]
3199        fn prop_nested_route_conflict_exact_same_path(
3200            prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3201            route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3202        ) {
3203            async fn handler1() -> &'static str { "handler1" }
3204            async fn handler2() -> &'static str { "handler2" }
3205
3206            let prefix = format!("/{}", prefix_segments.join("/"));
3207            let route_path = format!("/{}", route_segments.join("/"));
3208
3209            // Build the full existing path
3210            let existing_path = format!("{}{}", prefix, route_path);
3211
3212            // Try to create a conflict by nesting a route that creates the same path
3213            let result = catch_unwind(AssertUnwindSafe(|| {
3214                let parent = Router::new().route(&existing_path, get(handler1));
3215                let nested = Router::new().route(&route_path, get(handler2));
3216                let _app = parent.nest(&prefix, nested);
3217            }));
3218
3219            // Property: Should detect conflict
3220            prop_assert!(
3221                result.is_err(),
3222                "Nested route '{}{}' should conflict with existing route '{}' but didn't",
3223                prefix, route_path, existing_path
3224            );
3225        }
3226
3227        /// Property: Nested routes under different prefixes don't conflict
3228        ///
3229        /// For any two nested routers with the same internal routes but different
3230        /// prefixes, they should not conflict.
3231        #[test]
3232        fn prop_nested_routes_different_prefixes_no_conflict(
3233            prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3234            prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3235            route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3236            has_param in any::<bool>(),
3237        ) {
3238            // Build prefixes
3239            let prefix1 = format!("/{}", prefix1_segments.join("/"));
3240            let prefix2 = format!("/{}", prefix2_segments.join("/"));
3241
3242            // Ensure prefixes are different
3243            prop_assume!(prefix1 != prefix2);
3244
3245            async fn handler1() -> &'static str { "handler1" }
3246            async fn handler2() -> &'static str { "handler2" }
3247
3248            // Build the route path
3249            let route_path = if has_param {
3250                format!("/{}/{{id}}", route_segments.join("/"))
3251            } else {
3252                format!("/{}", route_segments.join("/"))
3253            };
3254
3255            // Try to nest both routers - should NOT conflict
3256            let result = catch_unwind(AssertUnwindSafe(|| {
3257                let nested1 = Router::new().route(&route_path, get(handler1));
3258                let nested2 = Router::new().route(&route_path, get(handler2));
3259
3260                let app = Router::new()
3261                    .nest(&prefix1, nested1)
3262                    .nest(&prefix2, nested2);
3263
3264                app.registered_routes().len()
3265            }));
3266
3267            // Property: Should NOT conflict
3268            prop_assert!(
3269                result.is_ok(),
3270                "Routes under different prefixes '{}' and '{}' should not conflict",
3271                prefix1, prefix2
3272            );
3273
3274            if let Ok(count) = result {
3275                prop_assert_eq!(count, 2, "Should have registered 2 routes");
3276            }
3277        }
3278
3279        /// Property: Conflict error message contains resolution guidance
3280        ///
3281        /// When a nested route conflict is detected, the error message should
3282        /// include guidance on how to resolve the conflict.
3283        #[test]
3284        fn prop_nested_conflict_error_contains_guidance(
3285            prefix in "[a-z][a-z0-9]{1,5}",
3286            segment in "[a-z][a-z0-9]{1,5}",
3287            param1 in "[a-z][a-z0-9]{1,5}",
3288            param2 in "[a-z][a-z0-9]{1,5}",
3289        ) {
3290            prop_assume!(param1 != param2);
3291
3292            async fn handler1() -> &'static str { "handler1" }
3293            async fn handler2() -> &'static str { "handler2" }
3294
3295            let prefix = format!("/{}", prefix);
3296            let existing_path = format!("{}/{}/{{{}}}", prefix, segment, param1);
3297            let nested_path = format!("/{}/{{{}}}", segment, param2);
3298
3299            let result = catch_unwind(AssertUnwindSafe(|| {
3300                let parent = Router::new().route(&existing_path, get(handler1));
3301                let nested = Router::new().route(&nested_path, get(handler2));
3302                let _app = parent.nest(&prefix, nested);
3303            }));
3304
3305            prop_assert!(result.is_err(), "Should have detected conflict");
3306
3307            if let Err(panic_info) = result {
3308                if let Some(msg) = panic_info.downcast_ref::<String>() {
3309                    prop_assert!(
3310                        msg.contains("How to resolve:"),
3311                        "Error should contain 'How to resolve:' guidance, got: {}",
3312                        msg
3313                    );
3314                    prop_assert!(
3315                        msg.contains("Use different path patterns") ||
3316                        msg.contains("different path patterns"),
3317                        "Error should suggest using different path patterns, got: {}",
3318                        msg
3319                    );
3320                }
3321            }
3322        }
3323    }
3324}