rustapi_core/
router.rs

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