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