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