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