Skip to main content

rustapi_core/
router.rs

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