Skip to main content

rustapi_core/
router.rs

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