Skip to main content

fastapi_router/
trie.rs

1//! Radix trie router implementation.
2//!
3//! # Route Matching Priority
4//!
5//! Routes are matched according to these priority rules (highest to lowest):
6//!
7//! 1. **Static segments** - Exact literal matches (`/users/me`)
8//! 2. **Named parameters** - Single-segment captures (`/users/{id}`)
9//! 3. **Wildcards** - Multi-segment catch-alls (`/files/{*path}`)
10//!
11//! ## Examples
12//!
13//! Given these routes:
14//! - `/users/me` (static)
15//! - `/users/{id}` (named param)
16//! - `/{*path}` (wildcard)
17//!
18//! Requests match as follows:
19//! - `/users/me` → `/users/me` (static wins over param)
20//! - `/users/123` → `/users/{id}` (param wins over wildcard)
21//! - `/other/path` → `/{*path}` (wildcard catches the rest)
22//!
23//! ## Conflict Detection
24//!
25//! Routes that are ambiguous are rejected at registration:
26//! - `/files/{name}` and `/files/{*path}` conflict (both match `/files/foo`)
27//! - `/api/{a}` and `/api/{b}` conflict (same structure, different names)
28//!
29//! # Wildcard Catch-All Routes
30//!
31//! The router supports catch-all wildcard routes using two equivalent syntaxes:
32//!
33//! - `{*path}` - asterisk prefix syntax (recommended)
34//! - `{path:path}` - converter suffix syntax
35//!
36//! Wildcards capture all remaining path segments including slashes:
37//!
38//! ```text
39//! Route: /files/{*filepath}
40//! Request: /files/css/styles/main.css
41//! Captured: filepath = "css/styles/main.css"
42//! ```
43//!
44//! Wildcards must be the final segment in a route pattern.
45
46use crate::r#match::{AllowedMethods, RouteLookup, RouteMatch};
47use fastapi_types::Method;
48use std::collections::HashMap;
49use std::fmt;
50
51/// Path parameter type converter.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
53pub enum Converter {
54    /// String (default).
55    #[default]
56    Str,
57    /// Integer (i64).
58    Int,
59    /// Float (f64).
60    Float,
61    /// UUID.
62    Uuid,
63    /// Path segment (can contain `/`). Used for catch-all wildcard routes.
64    ///
65    /// Can be specified as `{*name}` or `{name:path}`.
66    Path,
67}
68
69/// A type-converted path parameter value.
70#[derive(Debug, Clone, PartialEq)]
71pub enum ParamValue {
72    /// String value (from `{param}` or `{param:str}`).
73    Str(String),
74    /// Integer value (from `{param:int}`).
75    Int(i64),
76    /// Float value (from `{param:float}`).
77    Float(f64),
78    /// UUID value (from `{param:uuid}`).
79    Uuid(String),
80    /// Path value including slashes (from `{*param}` or `{param:path}`).
81    Path(String),
82}
83
84impl ParamValue {
85    /// Get as string reference. Works for all variants.
86    #[must_use]
87    pub fn as_str(&self) -> &str {
88        match self {
89            Self::Str(s) | Self::Uuid(s) | Self::Path(s) => s,
90            Self::Int(_) | Self::Float(_) => {
91                // For numeric types, this isn't ideal but maintains API consistency
92                // Users should use as_int() or as_float() for those types
93                ""
94            }
95        }
96    }
97
98    /// Get as i64 if this is an Int variant.
99    #[must_use]
100    pub fn as_int(&self) -> Option<i64> {
101        match self {
102            Self::Int(n) => Some(*n),
103            _ => None,
104        }
105    }
106
107    /// Get as f64 if this is a Float variant.
108    #[must_use]
109    pub fn as_float(&self) -> Option<f64> {
110        match self {
111            Self::Float(n) => Some(*n),
112            _ => None,
113        }
114    }
115
116    /// Get the raw string for Str, Uuid, or Path variants.
117    #[must_use]
118    pub fn into_string(self) -> Option<String> {
119        match self {
120            Self::Str(s) | Self::Uuid(s) | Self::Path(s) => Some(s),
121            Self::Int(_) | Self::Float(_) => None,
122        }
123    }
124}
125
126/// Error type for parameter conversion failures.
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub enum ConversionError {
129    /// Failed to parse as integer.
130    InvalidInt {
131        /// The value that failed to parse.
132        value: String,
133        /// The parameter name.
134        param: String,
135    },
136    /// Failed to parse as float.
137    InvalidFloat {
138        /// The value that failed to parse.
139        value: String,
140        /// The parameter name.
141        param: String,
142    },
143    /// Failed to parse as UUID.
144    InvalidUuid {
145        /// The value that failed to parse.
146        value: String,
147        /// The parameter name.
148        param: String,
149    },
150}
151
152impl fmt::Display for ConversionError {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        match self {
155            Self::InvalidInt { value, param } => {
156                write!(
157                    f,
158                    "path parameter '{param}': '{value}' is not a valid integer"
159                )
160            }
161            Self::InvalidFloat { value, param } => {
162                write!(
163                    f,
164                    "path parameter '{param}': '{value}' is not a valid float"
165                )
166            }
167            Self::InvalidUuid { value, param } => {
168                write!(f, "path parameter '{param}': '{value}' is not a valid UUID")
169            }
170        }
171    }
172}
173
174impl std::error::Error for ConversionError {}
175
176impl Converter {
177    /// Check if a value matches this converter.
178    #[must_use]
179    pub fn matches(&self, value: &str) -> bool {
180        match self {
181            Self::Str => true,
182            Self::Int => value.parse::<i64>().is_ok(),
183            Self::Float => value.parse::<f64>().is_ok(),
184            Self::Uuid => is_uuid(value),
185            Self::Path => true,
186        }
187    }
188
189    /// Convert a string value to the appropriate typed value.
190    ///
191    /// # Errors
192    ///
193    /// Returns a `ConversionError` if the value cannot be parsed as the expected type.
194    pub fn convert(&self, value: &str, param_name: &str) -> Result<ParamValue, ConversionError> {
195        match self {
196            Self::Str => Ok(ParamValue::Str(value.to_string())),
197            Self::Int => {
198                value
199                    .parse::<i64>()
200                    .map(ParamValue::Int)
201                    .map_err(|_| ConversionError::InvalidInt {
202                        value: value.to_string(),
203                        param: param_name.to_string(),
204                    })
205            }
206            Self::Float => value.parse::<f64>().map(ParamValue::Float).map_err(|_| {
207                ConversionError::InvalidFloat {
208                    value: value.to_string(),
209                    param: param_name.to_string(),
210                }
211            }),
212            Self::Uuid => {
213                if is_uuid(value) {
214                    Ok(ParamValue::Uuid(value.to_string()))
215                } else {
216                    Err(ConversionError::InvalidUuid {
217                        value: value.to_string(),
218                        param: param_name.to_string(),
219                    })
220                }
221            }
222            Self::Path => Ok(ParamValue::Path(value.to_string())),
223        }
224    }
225
226    /// Returns the type name for error messages.
227    #[must_use]
228    pub fn type_name(&self) -> &'static str {
229        match self {
230            Self::Str => "string",
231            Self::Int => "integer",
232            Self::Float => "float",
233            Self::Uuid => "UUID",
234            Self::Path => "path",
235        }
236    }
237}
238
239/// Check if a string is a valid UUID (8-4-4-4-12 format).
240///
241/// This implementation avoids allocations by using byte-based validation.
242fn is_uuid(s: &str) -> bool {
243    // UUID must be exactly 36 chars: 8-4-4-4-12 = 32 hex + 4 dashes
244    if s.len() != 36 {
245        return false;
246    }
247
248    let bytes = s.as_bytes();
249
250    // Check dashes at positions 8, 13, 18, 23
251    if bytes[8] != b'-' || bytes[13] != b'-' || bytes[18] != b'-' || bytes[23] != b'-' {
252        return false;
253    }
254
255    // Check all hex digits (skip dash positions)
256    bytes.iter().enumerate().all(|(i, &b)| {
257        if i == 8 || i == 13 || i == 18 || i == 23 {
258            true // Already verified dashes
259        } else {
260            b.is_ascii_hexdigit()
261        }
262    })
263}
264
265/// Path parameter information with optional OpenAPI metadata.
266#[derive(Debug, Clone, Default)]
267pub struct ParamInfo {
268    /// Parameter name.
269    pub name: String,
270    /// Type converter.
271    pub converter: Converter,
272    /// Title for display in OpenAPI documentation.
273    pub title: Option<String>,
274    /// Description for OpenAPI documentation.
275    pub description: Option<String>,
276    /// Whether the parameter is deprecated.
277    pub deprecated: bool,
278    /// Example value for OpenAPI documentation.
279    pub example: Option<serde_json::Value>,
280    /// Named examples for OpenAPI documentation.
281    pub examples: Vec<(String, serde_json::Value)>,
282}
283
284impl ParamInfo {
285    /// Create a new parameter info with name and converter.
286    #[must_use]
287    pub fn new(name: impl Into<String>, converter: Converter) -> Self {
288        Self {
289            name: name.into(),
290            converter,
291            title: None,
292            description: None,
293            deprecated: false,
294            example: None,
295            examples: Vec::new(),
296        }
297    }
298
299    /// Set the title for OpenAPI documentation.
300    #[must_use]
301    pub fn with_title(mut self, title: impl Into<String>) -> Self {
302        self.title = Some(title.into());
303        self
304    }
305
306    /// Set the description for OpenAPI documentation.
307    #[must_use]
308    pub fn with_description(mut self, description: impl Into<String>) -> Self {
309        self.description = Some(description.into());
310        self
311    }
312
313    /// Mark the parameter as deprecated.
314    #[must_use]
315    pub fn deprecated(mut self) -> Self {
316        self.deprecated = true;
317        self
318    }
319
320    /// Set an example value for OpenAPI documentation.
321    #[must_use]
322    pub fn with_example(mut self, example: serde_json::Value) -> Self {
323        self.example = Some(example);
324        self
325    }
326
327    /// Add a named example for OpenAPI documentation.
328    #[must_use]
329    pub fn with_named_example(mut self, name: impl Into<String>, value: serde_json::Value) -> Self {
330        self.examples.push((name.into(), value));
331        self
332    }
333}
334
335/// Extract path parameters from a route path pattern.
336///
337/// Parses a path pattern like `/users/{id}/posts/{post_id:int}` and returns
338/// information about each parameter, including its name and type converter.
339///
340/// # Examples
341///
342/// ```ignore
343/// use fastapi_router::{extract_path_params, Converter};
344///
345/// let params = extract_path_params("/users/{id}");
346/// assert_eq!(params.len(), 1);
347/// assert_eq!(params[0].name, "id");
348/// assert!(matches!(params[0].converter, Converter::Str));
349///
350/// // Typed parameters
351/// let params = extract_path_params("/items/{item_id:int}/price/{value:float}");
352/// assert_eq!(params.len(), 2);
353/// assert!(matches!(params[0].converter, Converter::Int));
354/// assert!(matches!(params[1].converter, Converter::Float));
355///
356/// // Wildcard catch-all
357/// let params = extract_path_params("/files/{*path}");
358/// assert_eq!(params.len(), 1);
359/// assert!(matches!(params[0].converter, Converter::Path));
360/// ```
361#[must_use]
362pub fn extract_path_params(path: &str) -> Vec<ParamInfo> {
363    path.split('/')
364        .filter(|s| !s.is_empty())
365        .filter_map(|s| {
366            if s.starts_with('{') && s.ends_with('}') {
367                let inner = &s[1..s.len() - 1];
368                // Check for {*name} wildcard syntax (catch-all)
369                if let Some(name) = inner.strip_prefix('*') {
370                    return Some(ParamInfo::new(name, Converter::Path));
371                }
372                let (name, converter) = if let Some(pos) = inner.find(':') {
373                    let conv = match &inner[pos + 1..] {
374                        "int" => Converter::Int,
375                        "float" => Converter::Float,
376                        "uuid" => Converter::Uuid,
377                        "path" => Converter::Path,
378                        _ => Converter::Str,
379                    };
380                    (&inner[..pos], conv)
381                } else {
382                    (inner, Converter::Str)
383                };
384                Some(ParamInfo::new(name, converter))
385            } else {
386                None
387            }
388        })
389        .collect()
390}
391
392/// Sanitize an identifier fragment for use in OpenAPI `operationId`.
393///
394/// The output is lowercase, alphanumeric/underscore only, and never empty.
395fn sanitize_operation_id(s: &str) -> String {
396    let mut out = String::with_capacity(s.len());
397    let mut prev_underscore = false;
398    for c in s.chars() {
399        if c.is_ascii_alphanumeric() {
400            out.push(c.to_ascii_lowercase());
401            prev_underscore = false;
402        } else if !prev_underscore {
403            out.push('_');
404            prev_underscore = true;
405        }
406    }
407    let trimmed = out.trim_matches('_');
408    if trimmed.is_empty() {
409        "root".to_string()
410    } else {
411        trimmed.to_string()
412    }
413}
414
415/// Response declaration for OpenAPI documentation.
416///
417/// Describes a possible response from a route, including status code,
418/// schema type, and description.
419#[derive(Debug, Clone)]
420pub struct RouteResponse {
421    /// HTTP status code (e.g., 200, 201, 404).
422    pub status: u16,
423    /// Schema type name for the response body (e.g., "User", "Vec\<Item\>").
424    pub schema_name: String,
425    /// Description of when this response is returned.
426    pub description: String,
427    /// Content type for the response (defaults to "application/json").
428    pub content_type: String,
429}
430
431impl RouteResponse {
432    /// Create a new response declaration.
433    #[must_use]
434    pub fn new(
435        status: u16,
436        schema_name: impl Into<String>,
437        description: impl Into<String>,
438    ) -> Self {
439        Self {
440            status,
441            schema_name: schema_name.into(),
442            description: description.into(),
443            content_type: "application/json".to_string(),
444        }
445    }
446
447    /// Set a custom content type for this response.
448    #[must_use]
449    pub fn with_content_type(mut self, content_type: impl Into<String>) -> Self {
450        self.content_type = content_type.into();
451        self
452    }
453}
454
455/// Security requirement for a route.
456///
457/// Specifies a security scheme and optional scopes required to access a route.
458#[derive(Debug, Clone, Default)]
459pub struct RouteSecurityRequirement {
460    /// Name of the security scheme (must match a scheme in OpenAPI components).
461    pub scheme: String,
462    /// Required scopes for this scheme (empty for schemes that don't use scopes).
463    pub scopes: Vec<String>,
464}
465
466impl RouteSecurityRequirement {
467    /// Create a new security requirement with no scopes.
468    #[must_use]
469    pub fn new(scheme: impl Into<String>) -> Self {
470        Self {
471            scheme: scheme.into(),
472            scopes: Vec::new(),
473        }
474    }
475
476    /// Create a new security requirement with scopes.
477    #[must_use]
478    pub fn with_scopes(
479        scheme: impl Into<String>,
480        scopes: impl IntoIterator<Item = impl Into<String>>,
481    ) -> Self {
482        Self {
483            scheme: scheme.into(),
484            scopes: scopes.into_iter().map(Into::into).collect(),
485        }
486    }
487}
488
489/// A route definition used for routing and (optionally) metadata.
490///
491/// Routes are created with a path pattern and HTTP method. The router crate is
492/// intentionally pure routing/metadata and does not store framework handler
493/// objects (to avoid dependency cycles with `fastapi-core`).
494///
495/// # Example
496///
497/// ```ignore
498/// use fastapi_router::Route;
499/// use fastapi_types::Method;
500///
501/// let route = Route::new(Method::Get, "/users/{id}");
502/// ```
503#[derive(Clone)]
504pub struct Route {
505    /// Route path pattern (e.g., "/users/{id}").
506    pub path: String,
507    /// HTTP method for this route.
508    pub method: Method,
509    /// Operation ID for OpenAPI documentation.
510    pub operation_id: String,
511    /// OpenAPI summary (short description).
512    pub summary: Option<String>,
513    /// OpenAPI description (detailed explanation).
514    pub description: Option<String>,
515    /// Tags for grouping routes in OpenAPI documentation.
516    pub tags: Vec<String>,
517    /// Whether this route is deprecated.
518    pub deprecated: bool,
519    /// Path parameters extracted from the route pattern for OpenAPI documentation.
520    pub path_params: Vec<ParamInfo>,
521    /// Request body schema type name for OpenAPI documentation (e.g., "CreateUser").
522    pub request_body_schema: Option<String>,
523    /// Request body content type for OpenAPI documentation (e.g., "application/json").
524    pub request_body_content_type: Option<String>,
525    /// Whether the request body is required.
526    pub request_body_required: bool,
527    /// Security requirements for this route.
528    ///
529    /// Each requirement specifies a security scheme name and optional scopes.
530    /// Multiple requirements means any one of them can be used (OR logic).
531    pub security: Vec<RouteSecurityRequirement>,
532    /// Declared responses for OpenAPI documentation.
533    ///
534    /// Each response specifies a status code, schema type, and description.
535    pub responses: Vec<RouteResponse>,
536}
537
538impl fmt::Debug for Route {
539    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
540        let mut s = f.debug_struct("Route");
541        s.field("path", &self.path)
542            .field("method", &self.method)
543            .field("operation_id", &self.operation_id);
544        if let Some(ref summary) = self.summary {
545            s.field("summary", summary);
546        }
547        if let Some(ref desc) = self.description {
548            s.field("description", desc);
549        }
550        if !self.tags.is_empty() {
551            s.field("tags", &self.tags);
552        }
553        if self.deprecated {
554            s.field("deprecated", &self.deprecated);
555        }
556        if !self.path_params.is_empty() {
557            s.field("path_params", &self.path_params);
558        }
559        if let Some(ref schema) = self.request_body_schema {
560            s.field("request_body_schema", schema);
561        }
562        if let Some(ref content_type) = self.request_body_content_type {
563            s.field("request_body_content_type", content_type);
564        }
565        if self.request_body_required {
566            s.field("request_body_required", &self.request_body_required);
567        }
568        if !self.security.is_empty() {
569            s.field("security", &self.security);
570        }
571        if !self.responses.is_empty() {
572            s.field("responses", &self.responses);
573        }
574        s.finish()
575    }
576}
577
578/// Error returned when a new route conflicts with an existing one.
579#[derive(Debug, Clone)]
580pub struct RouteConflictError {
581    /// HTTP method for the conflicting route.
582    pub method: Method,
583    /// The new route path that failed to register.
584    pub new_path: String,
585    /// The existing route path that conflicts.
586    pub existing_path: String,
587}
588
589impl fmt::Display for RouteConflictError {
590    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
591        write!(
592            f,
593            "route conflict for {}: {} conflicts with {}",
594            self.method, self.new_path, self.existing_path
595        )
596    }
597}
598
599impl std::error::Error for RouteConflictError {}
600
601/// Error returned when a route path is invalid.
602#[derive(Debug, Clone)]
603pub struct InvalidRouteError {
604    /// The invalid route path.
605    pub path: String,
606    /// Description of the validation failure.
607    pub message: String,
608}
609
610impl InvalidRouteError {
611    /// Create a new invalid route error.
612    #[must_use]
613    pub fn new(path: impl Into<String>, message: impl Into<String>) -> Self {
614        Self {
615            path: path.into(),
616            message: message.into(),
617        }
618    }
619}
620
621impl fmt::Display for InvalidRouteError {
622    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
623        write!(f, "invalid route path '{}': {}", self.path, self.message)
624    }
625}
626
627impl std::error::Error for InvalidRouteError {}
628
629/// Error returned when adding a route fails.
630#[derive(Debug, Clone)]
631pub enum RouteAddError {
632    /// Route conflicts with an existing route.
633    Conflict(RouteConflictError),
634    /// Route path is invalid.
635    InvalidPath(InvalidRouteError),
636}
637
638impl fmt::Display for RouteAddError {
639    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
640        match self {
641            Self::Conflict(err) => err.fmt(f),
642            Self::InvalidPath(err) => err.fmt(f),
643        }
644    }
645}
646
647impl std::error::Error for RouteAddError {
648    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
649        match self {
650            Self::Conflict(err) => Some(err),
651            Self::InvalidPath(err) => Some(err),
652        }
653    }
654}
655
656impl From<RouteConflictError> for RouteAddError {
657    fn from(err: RouteConflictError) -> Self {
658        Self::Conflict(err)
659    }
660}
661
662impl From<InvalidRouteError> for RouteAddError {
663    fn from(err: InvalidRouteError) -> Self {
664        Self::InvalidPath(err)
665    }
666}
667
668impl Route {
669    /// Create a new route.
670    ///
671    /// # Arguments
672    ///
673    /// * `method` - HTTP method for this route
674    /// * `path` - Path pattern (e.g., "/users/{id}")
675    ///
676    /// # Example
677    ///
678    /// ```ignore
679    /// use fastapi_router::Route;
680    /// use fastapi_types::Method;
681    ///
682    /// let route = Route::new(Method::Get, "/users/{id}");
683    /// ```
684    pub fn new(method: Method, path: impl Into<String>) -> Self {
685        let path = path.into();
686        // Operation IDs must be stable and unique across methods for the same path.
687        // Keep this pure (no core deps) while producing a safe identifier for OpenAPI.
688        let method_prefix = method.as_str().to_ascii_lowercase();
689        let path_part = sanitize_operation_id(&path);
690        let operation_id = format!("{method_prefix}_{path_part}");
691        let path_params = extract_path_params(&path);
692        Self {
693            path,
694            method,
695            operation_id,
696            summary: None,
697            description: None,
698            tags: Vec::new(),
699            deprecated: false,
700            path_params,
701            request_body_schema: None,
702            request_body_content_type: None,
703            request_body_required: false,
704            security: Vec::new(),
705            responses: Vec::new(),
706        }
707    }
708
709    /// Create a route intended for generated metadata (OpenAPI/docs).
710    ///
711    /// This is an alias for [`Route::new`]. The name is kept for compatibility
712    /// with older macro expansions and tests; a `Route` never contains an actual
713    /// handler function.
714    #[must_use]
715    pub fn with_placeholder_handler(method: Method, path: impl Into<String>) -> Self {
716        Self::new(method, path)
717    }
718
719    /// Set the summary for OpenAPI documentation.
720    #[must_use]
721    pub fn summary(mut self, summary: impl Into<String>) -> Self {
722        self.summary = Some(summary.into());
723        self
724    }
725
726    /// Set the description for OpenAPI documentation.
727    #[must_use]
728    pub fn description(mut self, description: impl Into<String>) -> Self {
729        self.description = Some(description.into());
730        self
731    }
732
733    /// Set the operation ID for OpenAPI documentation.
734    #[must_use]
735    pub fn operation_id(mut self, operation_id: impl Into<String>) -> Self {
736        self.operation_id = operation_id.into();
737        self
738    }
739
740    /// Add a tag for grouping in OpenAPI documentation.
741    #[must_use]
742    pub fn tag(mut self, tag: impl Into<String>) -> Self {
743        self.tags.push(tag.into());
744        self
745    }
746
747    /// Set multiple tags for grouping in OpenAPI documentation.
748    #[must_use]
749    pub fn tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
750        self.tags.extend(tags.into_iter().map(Into::into));
751        self
752    }
753
754    /// Mark this route as deprecated in OpenAPI documentation.
755    #[must_use]
756    pub fn deprecated(mut self) -> Self {
757        self.deprecated = true;
758        self
759    }
760
761    /// Set the request body schema for OpenAPI documentation.
762    ///
763    /// The schema name will be used to generate a `$ref` to the schema
764    /// in the components section.
765    #[must_use]
766    pub fn request_body(
767        mut self,
768        schema: impl Into<String>,
769        content_type: impl Into<String>,
770        required: bool,
771    ) -> Self {
772        self.request_body_schema = Some(schema.into());
773        self.request_body_content_type = Some(content_type.into());
774        self.request_body_required = required;
775        self
776    }
777
778    /// Add a security requirement for this route.
779    ///
780    /// Each call adds an alternative security requirement (OR logic).
781    /// The scheme name must match a security scheme defined in the OpenAPI
782    /// components section.
783    ///
784    /// # Example
785    ///
786    /// ```ignore
787    /// use fastapi_router::Route;
788    /// use fastapi_types::Method;
789    ///
790    /// // Route requires bearer token authentication
791    /// let route = Route::new(Method::Get, "/protected")
792    ///     .security("bearer", vec![]);
793    ///
794    /// // Route requires OAuth2 with specific scopes
795    /// let route = Route::new(Method::Post, "/users")
796    ///     .security("oauth2", vec!["write:users"]);
797    /// ```
798    #[must_use]
799    pub fn security(
800        mut self,
801        scheme: impl Into<String>,
802        scopes: impl IntoIterator<Item = impl Into<String>>,
803    ) -> Self {
804        self.security
805            .push(RouteSecurityRequirement::with_scopes(scheme, scopes));
806        self
807    }
808
809    /// Add a security requirement without scopes.
810    ///
811    /// Convenience method for schemes that don't use scopes (e.g., API key, bearer token).
812    ///
813    /// # Example
814    ///
815    /// ```ignore
816    /// use fastapi_router::Route;
817    /// use fastapi_types::Method;
818    ///
819    /// let route = Route::new(Method::Get, "/protected")
820    ///     .security_scheme("api_key");
821    /// ```
822    #[must_use]
823    pub fn security_scheme(mut self, scheme: impl Into<String>) -> Self {
824        self.security.push(RouteSecurityRequirement::new(scheme));
825        self
826    }
827
828    /// Add a response declaration for OpenAPI documentation.
829    ///
830    /// The response type is verified at compile time to ensure it implements
831    /// `JsonSchema`, enabling OpenAPI schema generation.
832    ///
833    /// # Arguments
834    ///
835    /// * `status` - HTTP status code (e.g., 200, 201, 404)
836    /// * `schema_name` - Type name for the response body (e.g., "User")
837    /// * `description` - Description of when this response is returned
838    ///
839    /// # Example
840    ///
841    /// ```ignore
842    /// use fastapi_router::Route;
843    /// use fastapi_types::Method;
844    ///
845    /// let route = Route::new(Method::Get, "/users/{id}")
846    ///     .response(200, "User", "User found")
847    ///     .response(404, "ErrorResponse", "User not found");
848    /// ```
849    #[must_use]
850    pub fn response(
851        mut self,
852        status: u16,
853        schema_name: impl Into<String>,
854        description: impl Into<String>,
855    ) -> Self {
856        self.responses
857            .push(RouteResponse::new(status, schema_name, description));
858        self
859    }
860
861    /// Check if this route has response declarations.
862    #[must_use]
863    pub fn has_responses(&self) -> bool {
864        !self.responses.is_empty()
865    }
866
867    /// Check if this route has a request body defined.
868    #[must_use]
869    pub fn has_request_body(&self) -> bool {
870        self.request_body_schema.is_some()
871    }
872
873    /// Check if this route has path parameters.
874    #[must_use]
875    pub fn has_path_params(&self) -> bool {
876        !self.path_params.is_empty()
877    }
878
879    /// Check if this route has security requirements.
880    #[must_use]
881    pub fn has_security(&self) -> bool {
882        !self.security.is_empty()
883    }
884}
885
886/// Trie node.
887struct Node {
888    segment: String,
889    children: Vec<Node>,
890    param: Option<ParamInfo>,
891    routes: HashMap<Method, usize>,
892}
893
894impl Node {
895    fn new(segment: impl Into<String>) -> Self {
896        Self {
897            segment: segment.into(),
898            children: Vec::new(),
899            param: None,
900            routes: HashMap::new(),
901        }
902    }
903
904    fn find_static(&self, segment: &str) -> Option<&Node> {
905        self.children
906            .iter()
907            .find(|c| c.param.is_none() && c.segment == segment)
908    }
909
910    fn find_param(&self) -> Option<&Node> {
911        self.children.iter().find(|c| c.param.is_some())
912    }
913}
914
915/// Radix trie router.
916pub struct Router {
917    root: Node,
918    routes: Vec<Route>,
919}
920
921impl Router {
922    /// Create an empty router.
923    #[must_use]
924    pub fn new() -> Self {
925        Self {
926            root: Node::new(""),
927            routes: Vec::new(),
928        }
929    }
930
931    /// Add a route, returning an error if it conflicts with existing routes
932    /// or the path pattern is invalid.
933    ///
934    /// Conflict rules:
935    /// - Same HTTP method + structurally identical path patterns conflict
936    /// - Static segments take priority over parameter segments (no conflict)
937    /// - Parameter names/converters do not disambiguate conflicts (one param slot per segment)
938    /// - `{param:path}` converters are only valid as the final segment
939    pub fn add(&mut self, route: Route) -> Result<(), RouteAddError> {
940        if let Some(conflict) = self.find_conflict(&route) {
941            return Err(RouteAddError::Conflict(conflict));
942        }
943
944        let route_idx = self.routes.len();
945        let path = route.path.clone();
946        let method = route.method;
947        self.routes.push(route);
948
949        let segments = parse_path(&path);
950        validate_path_segments(&path, &segments)?;
951        let mut node = &mut self.root;
952
953        for seg in segments {
954            let (segment, param) = match seg {
955                PathSegment::Static(s) => (s.to_string(), None),
956                PathSegment::Param { name, converter } => {
957                    let info = ParamInfo::new(name, converter);
958                    (format!("{{{name}}}"), Some(info))
959                }
960            };
961
962            // Find or create child
963            let child_idx = node.children.iter().position(|c| c.segment == segment);
964
965            if let Some(idx) = child_idx {
966                node = &mut node.children[idx];
967            } else {
968                let mut new_node = Node::new(&segment);
969                new_node.param = param;
970                node.children.push(new_node);
971                node = node.children.last_mut().unwrap();
972            }
973        }
974
975        node.routes.insert(method, route_idx);
976        Ok(())
977    }
978
979    /// Match a path and method with 404/405 distinction.
980    #[must_use]
981    pub fn lookup<'a>(&'a self, path: &'a str, method: Method) -> RouteLookup<'a> {
982        let (node, params) = match self.match_node(path) {
983            Some(found) => found,
984            None => return RouteLookup::NotFound,
985        };
986
987        if let Some(&idx) = node.routes.get(&method) {
988            return RouteLookup::Match(RouteMatch {
989                route: &self.routes[idx],
990                params,
991            });
992        }
993
994        // Allow HEAD when GET is registered.
995        if method == Method::Head
996            && let Some(&idx) = node.routes.get(&Method::Get)
997        {
998            return RouteLookup::Match(RouteMatch {
999                route: &self.routes[idx],
1000                params,
1001            });
1002        }
1003
1004        if node.routes.is_empty() {
1005            return RouteLookup::NotFound;
1006        }
1007
1008        let allowed = AllowedMethods::new(node.routes.keys().copied().collect());
1009        RouteLookup::MethodNotAllowed { allowed }
1010    }
1011
1012    /// Match a path and method.
1013    #[must_use]
1014    pub fn match_path<'a>(&'a self, path: &'a str, method: Method) -> Option<RouteMatch<'a>> {
1015        match self.lookup(path, method) {
1016            RouteLookup::Match(matched) => Some(matched),
1017            RouteLookup::MethodNotAllowed { .. } | RouteLookup::NotFound => None,
1018        }
1019    }
1020
1021    /// Get all routes.
1022    #[must_use]
1023    pub fn routes(&self) -> &[Route] {
1024        &self.routes
1025    }
1026
1027    /// Mount a child router at a path prefix.
1028    ///
1029    /// All routes from the child router will be accessible under the given prefix.
1030    /// This is useful for organizing routes into modules or API versions.
1031    ///
1032    /// # Arguments
1033    ///
1034    /// * `prefix` - Path prefix for all child routes (e.g., "/api/v1")
1035    /// * `child` - The router to mount
1036    ///
1037    /// # Example
1038    ///
1039    /// ```ignore
1040    /// use fastapi_router::Router;
1041    /// use fastapi_types::Method;
1042    ///
1043    /// let api = Router::new()
1044    ///     .route(get_users)   // /users
1045    ///     .route(get_items);  // /items
1046    ///
1047    /// let app = Router::new()
1048    ///     .mount("/api/v1", api);  // /api/v1/users, /api/v1/items
1049    /// ```
1050    ///
1051    /// # Errors
1052    ///
1053    /// Returns an error if any mounted route conflicts with existing routes.
1054    pub fn mount(mut self, prefix: &str, child: Router) -> Result<Self, RouteAddError> {
1055        let prefix = prefix.trim_end_matches('/');
1056
1057        for route in child.routes {
1058            let child_path = if route.path == "/" {
1059                String::new()
1060            } else if route.path.starts_with('/') {
1061                route.path.clone()
1062            } else {
1063                format!("/{}", route.path)
1064            };
1065
1066            let full_path = if prefix.is_empty() {
1067                if child_path.is_empty() {
1068                    "/".to_string()
1069                } else {
1070                    child_path
1071                }
1072            } else if child_path.is_empty() {
1073                prefix.to_string()
1074            } else {
1075                format!("{}{}", prefix, child_path)
1076            };
1077
1078            // Recompute path_params from full_path since the mounted path may differ
1079            let path_params = extract_path_params(&full_path);
1080            let mounted = Route {
1081                path: full_path,
1082                method: route.method,
1083                operation_id: route.operation_id,
1084                summary: route.summary,
1085                description: route.description,
1086                tags: route.tags,
1087                deprecated: route.deprecated,
1088                path_params,
1089                request_body_schema: route.request_body_schema,
1090                request_body_content_type: route.request_body_content_type,
1091                request_body_required: route.request_body_required,
1092                security: route.security,
1093                responses: route.responses,
1094            };
1095
1096            self.add(mounted)?;
1097        }
1098
1099        Ok(self)
1100    }
1101
1102    /// Mount a child router at a path prefix (builder pattern).
1103    ///
1104    /// Same as `mount` but panics on conflict. Use for static route definitions.
1105    ///
1106    /// # Panics
1107    ///
1108    /// Panics if any mounted route conflicts with existing routes.
1109    #[must_use]
1110    pub fn nest(self, prefix: &str, child: Router) -> Self {
1111        self.mount(prefix, child)
1112            .expect("route conflict when nesting router")
1113    }
1114
1115    fn find_conflict(&self, route: &Route) -> Option<RouteConflictError> {
1116        for existing in &self.routes {
1117            if existing.method != route.method {
1118                continue;
1119            }
1120
1121            if paths_conflict(&existing.path, &route.path) {
1122                return Some(RouteConflictError {
1123                    method: route.method,
1124                    new_path: route.path.clone(),
1125                    existing_path: existing.path.clone(),
1126                });
1127            }
1128        }
1129
1130        None
1131    }
1132
1133    fn match_node<'a>(&'a self, path: &'a str) -> Option<(&'a Node, Vec<(&'a str, &'a str)>)> {
1134        // Use zero-allocation iterator for segment ranges
1135        let mut range_iter = SegmentRangeIter::new(path);
1136
1137        // Collect ranges only once (needed for path converter lookahead)
1138        // Use SmallVec-style optimization: stack-allocate for typical paths
1139        let mut ranges_buf: [(usize, usize); 16] = [(0, 0); 16];
1140        let mut ranges_vec: Vec<(usize, usize)> = Vec::new();
1141        let mut range_count = 0;
1142
1143        for range in &mut range_iter {
1144            match range_count.cmp(&16) {
1145                std::cmp::Ordering::Less => {
1146                    ranges_buf[range_count] = range;
1147                }
1148                std::cmp::Ordering::Equal => {
1149                    // Overflow to heap
1150                    ranges_vec = ranges_buf.to_vec();
1151                    ranges_vec.push(range);
1152                }
1153                std::cmp::Ordering::Greater => {
1154                    ranges_vec.push(range);
1155                }
1156            }
1157            range_count += 1;
1158        }
1159
1160        let ranges: &[(usize, usize)] = if range_count <= 16 {
1161            &ranges_buf[..range_count]
1162        } else {
1163            &ranges_vec
1164        };
1165
1166        let last_end = ranges.last().map_or(0, |(_, end)| *end);
1167        let mut params = Vec::new();
1168        let mut node = &self.root;
1169
1170        for &(start, end) in ranges {
1171            let segment = &path[start..end];
1172
1173            // Try static match first
1174            if let Some(child) = node.find_static(segment) {
1175                node = child;
1176                continue;
1177            }
1178
1179            // Try parameter match
1180            if let Some(child) = node.find_param()
1181                && let Some(ref info) = child.param
1182            {
1183                if info.converter == Converter::Path {
1184                    let value = &path[start..last_end];
1185                    params.push((info.name.as_str(), value));
1186                    node = child;
1187                    // Path converter consumes rest of path
1188                    return Some((node, params));
1189                }
1190                if info.converter.matches(segment) {
1191                    params.push((info.name.as_str(), segment));
1192                    node = child;
1193                    continue;
1194                }
1195            }
1196
1197            return None;
1198        }
1199
1200        Some((node, params))
1201    }
1202}
1203
1204impl Default for Router {
1205    fn default() -> Self {
1206        Self::new()
1207    }
1208}
1209
1210enum PathSegment<'a> {
1211    Static(&'a str),
1212    Param { name: &'a str, converter: Converter },
1213}
1214
1215fn parse_path(path: &str) -> Vec<PathSegment<'_>> {
1216    path.split('/')
1217        .filter(|s| !s.is_empty())
1218        .map(|s| {
1219            if s.starts_with('{') && s.ends_with('}') {
1220                let inner = &s[1..s.len() - 1];
1221                // Check for {*name} wildcard syntax (catch-all)
1222                if let Some(name) = inner.strip_prefix('*') {
1223                    return PathSegment::Param {
1224                        name,
1225                        converter: Converter::Path,
1226                    };
1227                }
1228                let (name, converter) = if let Some(pos) = inner.find(':') {
1229                    let conv = match &inner[pos + 1..] {
1230                        "int" => Converter::Int,
1231                        "float" => Converter::Float,
1232                        "uuid" => Converter::Uuid,
1233                        "path" => Converter::Path,
1234                        _ => Converter::Str,
1235                    };
1236                    (&inner[..pos], conv)
1237                } else {
1238                    (inner, Converter::Str)
1239                };
1240                PathSegment::Param { name, converter }
1241            } else {
1242                PathSegment::Static(s)
1243            }
1244        })
1245        .collect()
1246}
1247
1248fn validate_path_segments(
1249    path: &str,
1250    segments: &[PathSegment<'_>],
1251) -> Result<(), InvalidRouteError> {
1252    for (idx, segment) in segments.iter().enumerate() {
1253        if let PathSegment::Param {
1254            name,
1255            converter: Converter::Path,
1256        } = segment
1257            && idx + 1 != segments.len()
1258        {
1259            return Err(InvalidRouteError::new(
1260                path,
1261                format!("wildcard '{{*{name}}}' or '{{{name}:path}}' must be the final segment"),
1262            ));
1263        }
1264    }
1265    Ok(())
1266}
1267
1268// Note: segment_ranges was replaced by SegmentRangeIter for zero-allocation path matching.
1269
1270/// Zero-allocation iterator over path segment ranges.
1271struct SegmentRangeIter<'a> {
1272    bytes: &'a [u8],
1273    idx: usize,
1274}
1275
1276impl<'a> SegmentRangeIter<'a> {
1277    fn new(path: &'a str) -> Self {
1278        Self {
1279            bytes: path.as_bytes(),
1280            idx: 0,
1281        }
1282    }
1283}
1284
1285impl Iterator for SegmentRangeIter<'_> {
1286    type Item = (usize, usize);
1287
1288    #[inline]
1289    fn next(&mut self) -> Option<Self::Item> {
1290        // Skip leading slashes
1291        while self.idx < self.bytes.len() && self.bytes[self.idx] == b'/' {
1292            self.idx += 1;
1293        }
1294        if self.idx >= self.bytes.len() {
1295            return None;
1296        }
1297        let start = self.idx;
1298        // Find end of segment
1299        while self.idx < self.bytes.len() && self.bytes[self.idx] != b'/' {
1300            self.idx += 1;
1301        }
1302        Some((start, self.idx))
1303    }
1304
1305    fn size_hint(&self) -> (usize, Option<usize>) {
1306        // Estimate: at most one segment per 2 bytes (e.g., "/a/b/c")
1307        let remaining = self.bytes.len().saturating_sub(self.idx);
1308        (0, Some(remaining / 2 + 1))
1309    }
1310}
1311
1312fn paths_conflict(a: &str, b: &str) -> bool {
1313    let a_segments = parse_path(a);
1314    let b_segments = parse_path(b);
1315
1316    let a_has_path = matches!(
1317        a_segments.last(),
1318        Some(PathSegment::Param {
1319            converter: Converter::Path,
1320            ..
1321        })
1322    );
1323    let b_has_path = matches!(
1324        b_segments.last(),
1325        Some(PathSegment::Param {
1326            converter: Converter::Path,
1327            ..
1328        })
1329    );
1330    let min_len = a_segments.len().min(b_segments.len());
1331    let mut param_mismatch = false;
1332
1333    for (left, right) in a_segments.iter().take(min_len).zip(b_segments.iter()) {
1334        match (left, right) {
1335            (PathSegment::Static(a), PathSegment::Static(b)) => {
1336                if a != b {
1337                    return false;
1338                }
1339            }
1340            (PathSegment::Static(_), PathSegment::Param { .. })
1341            | (PathSegment::Param { .. }, PathSegment::Static(_)) => {
1342                // Static segments take priority over params, so this is not a conflict.
1343                return false;
1344            }
1345            (
1346                PathSegment::Param {
1347                    name: left_name,
1348                    converter: left_conv,
1349                },
1350                PathSegment::Param {
1351                    name: right_name,
1352                    converter: right_conv,
1353                },
1354            ) => {
1355                if left_name != right_name || left_conv != right_conv {
1356                    param_mismatch = true;
1357                }
1358            }
1359        }
1360    }
1361
1362    if a_segments.len() == b_segments.len() {
1363        return true;
1364    }
1365
1366    if param_mismatch {
1367        return true;
1368    }
1369
1370    if a_has_path && a_segments.len() == min_len {
1371        return true;
1372    }
1373
1374    if b_has_path && b_segments.len() == min_len {
1375        return true;
1376    }
1377
1378    false
1379}
1380
1381#[cfg(test)]
1382mod tests {
1383    use super::*;
1384
1385    /// Helper to create a route.
1386    fn route(method: Method, path: &str) -> Route {
1387        Route::new(method, path)
1388    }
1389
1390    #[test]
1391    fn static_route_match() {
1392        let mut router = Router::new();
1393        router.add(route(Method::Get, "/users")).unwrap();
1394        router.add(route(Method::Get, "/items")).unwrap();
1395
1396        let m = router.match_path("/users", Method::Get);
1397        assert!(m.is_some());
1398        assert_eq!(m.unwrap().route.path, "/users");
1399
1400        let m = router.match_path("/items", Method::Get);
1401        assert!(m.is_some());
1402        assert_eq!(m.unwrap().route.path, "/items");
1403
1404        // Non-existent path
1405        assert!(router.match_path("/other", Method::Get).is_none());
1406    }
1407
1408    #[test]
1409    fn nested_static_routes() {
1410        let mut router = Router::new();
1411        router.add(route(Method::Get, "/api/v1/users")).unwrap();
1412        router.add(route(Method::Get, "/api/v2/users")).unwrap();
1413
1414        let m = router.match_path("/api/v1/users", Method::Get);
1415        assert!(m.is_some());
1416        assert_eq!(m.unwrap().route.path, "/api/v1/users");
1417
1418        let m = router.match_path("/api/v2/users", Method::Get);
1419        assert!(m.is_some());
1420        assert_eq!(m.unwrap().route.path, "/api/v2/users");
1421    }
1422
1423    #[test]
1424    fn parameter_extraction() {
1425        let mut router = Router::new();
1426        router.add(route(Method::Get, "/users/{user_id}")).unwrap();
1427
1428        let m = router.match_path("/users/123", Method::Get);
1429        assert!(m.is_some());
1430        let m = m.unwrap();
1431        assert_eq!(m.route.path, "/users/{user_id}");
1432        assert_eq!(m.params.len(), 1);
1433        assert_eq!(m.params[0], ("user_id", "123"));
1434    }
1435
1436    #[test]
1437    fn multiple_parameters() {
1438        let mut router = Router::new();
1439        router
1440            .add(route(Method::Get, "/users/{user_id}/posts/{post_id}"))
1441            .unwrap();
1442
1443        let m = router.match_path("/users/42/posts/99", Method::Get);
1444        assert!(m.is_some());
1445        let m = m.unwrap();
1446        assert_eq!(m.params.len(), 2);
1447        assert_eq!(m.params[0], ("user_id", "42"));
1448        assert_eq!(m.params[1], ("post_id", "99"));
1449    }
1450
1451    #[test]
1452    fn int_converter() {
1453        let mut router = Router::new();
1454        router.add(route(Method::Get, "/items/{id:int}")).unwrap();
1455
1456        // Valid integer
1457        let m = router.match_path("/items/123", Method::Get);
1458        assert!(m.is_some());
1459        assert_eq!(m.unwrap().params[0], ("id", "123"));
1460
1461        // Negative integer
1462        let m = router.match_path("/items/-456", Method::Get);
1463        assert!(m.is_some());
1464
1465        // Invalid (not an integer)
1466        assert!(router.match_path("/items/abc", Method::Get).is_none());
1467        assert!(router.match_path("/items/12.34", Method::Get).is_none());
1468    }
1469
1470    #[test]
1471    fn float_converter() {
1472        let mut router = Router::new();
1473        router
1474            .add(route(Method::Get, "/values/{val:float}"))
1475            .unwrap();
1476
1477        // Valid float
1478        let m = router.match_path("/values/3.14", Method::Get);
1479        assert!(m.is_some());
1480        assert_eq!(m.unwrap().params[0], ("val", "3.14"));
1481
1482        // Integer (also valid float)
1483        let m = router.match_path("/values/42", Method::Get);
1484        assert!(m.is_some());
1485
1486        // Invalid
1487        assert!(router.match_path("/values/abc", Method::Get).is_none());
1488    }
1489
1490    #[test]
1491    fn uuid_converter() {
1492        let mut router = Router::new();
1493        router
1494            .add(route(Method::Get, "/objects/{id:uuid}"))
1495            .unwrap();
1496
1497        // Valid UUID
1498        let m = router.match_path("/objects/550e8400-e29b-41d4-a716-446655440000", Method::Get);
1499        assert!(m.is_some());
1500        assert_eq!(
1501            m.unwrap().params[0],
1502            ("id", "550e8400-e29b-41d4-a716-446655440000")
1503        );
1504
1505        // Invalid UUIDs
1506        assert!(
1507            router
1508                .match_path("/objects/not-a-uuid", Method::Get)
1509                .is_none()
1510        );
1511        assert!(router.match_path("/objects/123", Method::Get).is_none());
1512    }
1513
1514    #[test]
1515    fn path_converter_captures_slashes() {
1516        let mut router = Router::new();
1517        router
1518            .add(route(Method::Get, "/files/{path:path}"))
1519            .unwrap();
1520
1521        let m = router.match_path("/files/a/b/c.txt", Method::Get).unwrap();
1522        assert_eq!(m.params[0], ("path", "a/b/c.txt"));
1523    }
1524
1525    #[test]
1526    fn path_converter_must_be_terminal() {
1527        let mut router = Router::new();
1528        let result = router.add(route(Method::Get, "/files/{path:path}/edit"));
1529        assert!(matches!(result, Err(RouteAddError::InvalidPath(_))));
1530    }
1531
1532    #[test]
1533    fn method_dispatch() {
1534        let mut router = Router::new();
1535        router.add(route(Method::Get, "/items")).unwrap();
1536        router.add(route(Method::Post, "/items")).unwrap();
1537        router.add(route(Method::Delete, "/items/{id}")).unwrap();
1538
1539        // GET /items
1540        let m = router.match_path("/items", Method::Get);
1541        assert!(m.is_some());
1542        assert_eq!(m.unwrap().route.method, Method::Get);
1543
1544        // POST /items
1545        let m = router.match_path("/items", Method::Post);
1546        assert!(m.is_some());
1547        assert_eq!(m.unwrap().route.method, Method::Post);
1548
1549        // DELETE /items/123
1550        let m = router.match_path("/items/123", Method::Delete);
1551        assert!(m.is_some());
1552        assert_eq!(m.unwrap().route.method, Method::Delete);
1553
1554        // Method not allowed (PUT /items)
1555        assert!(router.match_path("/items", Method::Put).is_none());
1556    }
1557
1558    #[test]
1559    fn lookup_method_not_allowed_includes_head() {
1560        let mut router = Router::new();
1561        router.add(route(Method::Get, "/users")).unwrap();
1562
1563        let result = router.lookup("/users", Method::Post);
1564        match result {
1565            RouteLookup::MethodNotAllowed { allowed } => {
1566                assert!(allowed.contains(Method::Get));
1567                assert!(allowed.contains(Method::Head));
1568                assert_eq!(allowed.header_value(), "GET, HEAD");
1569            }
1570            _ => panic!("expected MethodNotAllowed"),
1571        }
1572    }
1573
1574    #[test]
1575    fn lookup_method_not_allowed_multiple_methods() {
1576        let mut router = Router::new();
1577        router.add(route(Method::Get, "/users")).unwrap();
1578        router.add(route(Method::Post, "/users")).unwrap();
1579        router.add(route(Method::Delete, "/users")).unwrap();
1580
1581        let result = router.lookup("/users", Method::Put);
1582        match result {
1583            RouteLookup::MethodNotAllowed { allowed } => {
1584                assert_eq!(allowed.header_value(), "GET, HEAD, POST, DELETE");
1585            }
1586            _ => panic!("expected MethodNotAllowed"),
1587        }
1588    }
1589
1590    #[test]
1591    fn lookup_not_found_when_path_missing() {
1592        let mut router = Router::new();
1593        router.add(route(Method::Get, "/users")).unwrap();
1594
1595        assert!(matches!(
1596            router.lookup("/missing", Method::Get),
1597            RouteLookup::NotFound
1598        ));
1599    }
1600
1601    #[test]
1602    fn lookup_not_found_when_converter_mismatch() {
1603        let mut router = Router::new();
1604        router.add(route(Method::Get, "/items/{id:int}")).unwrap();
1605
1606        assert!(matches!(
1607            router.lookup("/items/abc", Method::Get),
1608            RouteLookup::NotFound
1609        ));
1610    }
1611
1612    #[test]
1613    fn static_takes_priority_over_param() {
1614        let mut router = Router::new();
1615        // Order matters: add static first, then param
1616        router.add(route(Method::Get, "/users/me")).unwrap();
1617        router.add(route(Method::Get, "/users/{id}")).unwrap();
1618
1619        // Static match for "me"
1620        let m = router.match_path("/users/me", Method::Get);
1621        assert!(m.is_some());
1622        let m = m.unwrap();
1623        assert_eq!(m.route.path, "/users/me");
1624        assert!(m.params.is_empty());
1625
1626        // Parameter match for "123"
1627        let m = router.match_path("/users/123", Method::Get);
1628        assert!(m.is_some());
1629        let m = m.unwrap();
1630        assert_eq!(m.route.path, "/users/{id}");
1631        assert_eq!(m.params[0], ("id", "123"));
1632    }
1633
1634    #[test]
1635    fn route_match_get_param() {
1636        let mut router = Router::new();
1637        router
1638            .add(route(Method::Get, "/users/{user_id}/posts/{post_id}"))
1639            .unwrap();
1640
1641        let m = router
1642            .match_path("/users/42/posts/99", Method::Get)
1643            .unwrap();
1644
1645        assert_eq!(m.get_param("user_id"), Some("42"));
1646        assert_eq!(m.get_param("post_id"), Some("99"));
1647        assert_eq!(m.get_param("unknown"), None);
1648    }
1649
1650    #[test]
1651    fn converter_matches() {
1652        assert!(Converter::Str.matches("anything"));
1653        assert!(Converter::Str.matches("123"));
1654
1655        assert!(Converter::Int.matches("123"));
1656        assert!(Converter::Int.matches("-456"));
1657        assert!(!Converter::Int.matches("12.34"));
1658        assert!(!Converter::Int.matches("abc"));
1659
1660        assert!(Converter::Float.matches("3.14"));
1661        assert!(Converter::Float.matches("42"));
1662        assert!(!Converter::Float.matches("abc"));
1663
1664        assert!(Converter::Uuid.matches("550e8400-e29b-41d4-a716-446655440000"));
1665        assert!(!Converter::Uuid.matches("not-a-uuid"));
1666
1667        assert!(Converter::Path.matches("any/path/here"));
1668    }
1669
1670    #[test]
1671    fn parse_path_segments() {
1672        let segments = parse_path("/users/{id}/posts/{post_id:int}");
1673        assert_eq!(segments.len(), 4);
1674
1675        match &segments[0] {
1676            PathSegment::Static(s) => assert_eq!(*s, "users"),
1677            _ => panic!("Expected static segment"),
1678        }
1679
1680        match &segments[1] {
1681            PathSegment::Param { name, converter } => {
1682                assert_eq!(*name, "id");
1683                assert_eq!(*converter, Converter::Str);
1684            }
1685            _ => panic!("Expected param segment"),
1686        }
1687
1688        match &segments[2] {
1689            PathSegment::Static(s) => assert_eq!(*s, "posts"),
1690            _ => panic!("Expected static segment"),
1691        }
1692
1693        match &segments[3] {
1694            PathSegment::Param { name, converter } => {
1695                assert_eq!(*name, "post_id");
1696                assert_eq!(*converter, Converter::Int);
1697            }
1698            _ => panic!("Expected param segment"),
1699        }
1700    }
1701
1702    // =========================================================================
1703    // EXTRACT PATH PARAMS TESTS
1704    // =========================================================================
1705
1706    #[test]
1707    fn extract_path_params_simple() {
1708        let params = extract_path_params("/users/{id}");
1709        assert_eq!(params.len(), 1);
1710        assert_eq!(params[0].name, "id");
1711        assert_eq!(params[0].converter, Converter::Str);
1712    }
1713
1714    #[test]
1715    fn extract_path_params_multiple() {
1716        let params = extract_path_params("/users/{id}/posts/{post_id}");
1717        assert_eq!(params.len(), 2);
1718        assert_eq!(params[0].name, "id");
1719        assert_eq!(params[1].name, "post_id");
1720    }
1721
1722    #[test]
1723    fn extract_path_params_typed_int() {
1724        let params = extract_path_params("/items/{item_id:int}");
1725        assert_eq!(params.len(), 1);
1726        assert_eq!(params[0].name, "item_id");
1727        assert_eq!(params[0].converter, Converter::Int);
1728    }
1729
1730    #[test]
1731    fn extract_path_params_typed_float() {
1732        let params = extract_path_params("/prices/{value:float}");
1733        assert_eq!(params.len(), 1);
1734        assert_eq!(params[0].name, "value");
1735        assert_eq!(params[0].converter, Converter::Float);
1736    }
1737
1738    #[test]
1739    fn extract_path_params_typed_uuid() {
1740        let params = extract_path_params("/resources/{uuid:uuid}");
1741        assert_eq!(params.len(), 1);
1742        assert_eq!(params[0].name, "uuid");
1743        assert_eq!(params[0].converter, Converter::Uuid);
1744    }
1745
1746    #[test]
1747    fn extract_path_params_wildcard_asterisk() {
1748        let params = extract_path_params("/files/{*filepath}");
1749        assert_eq!(params.len(), 1);
1750        assert_eq!(params[0].name, "filepath");
1751        assert_eq!(params[0].converter, Converter::Path);
1752    }
1753
1754    #[test]
1755    fn extract_path_params_wildcard_path_converter() {
1756        let params = extract_path_params("/static/{path:path}");
1757        assert_eq!(params.len(), 1);
1758        assert_eq!(params[0].name, "path");
1759        assert_eq!(params[0].converter, Converter::Path);
1760    }
1761
1762    #[test]
1763    fn extract_path_params_mixed_types() {
1764        let params = extract_path_params("/api/{version}/items/{id:int}/details/{slug}");
1765        assert_eq!(params.len(), 3);
1766        assert_eq!(params[0].name, "version");
1767        assert_eq!(params[0].converter, Converter::Str);
1768        assert_eq!(params[1].name, "id");
1769        assert_eq!(params[1].converter, Converter::Int);
1770        assert_eq!(params[2].name, "slug");
1771        assert_eq!(params[2].converter, Converter::Str);
1772    }
1773
1774    #[test]
1775    fn extract_path_params_no_params() {
1776        let params = extract_path_params("/static/path/no/params");
1777        assert!(params.is_empty());
1778    }
1779
1780    #[test]
1781    fn extract_path_params_root() {
1782        let params = extract_path_params("/");
1783        assert!(params.is_empty());
1784    }
1785
1786    // =========================================================================
1787    // PARAM INFO BUILDER TESTS
1788    // =========================================================================
1789
1790    #[test]
1791    fn param_info_new() {
1792        let info = ParamInfo::new("id", Converter::Int);
1793        assert_eq!(info.name, "id");
1794        assert_eq!(info.converter, Converter::Int);
1795        assert!(info.title.is_none());
1796        assert!(info.description.is_none());
1797        assert!(!info.deprecated);
1798        assert!(info.example.is_none());
1799        assert!(info.examples.is_empty());
1800    }
1801
1802    #[test]
1803    fn param_info_with_title() {
1804        let info = ParamInfo::new("id", Converter::Str).with_title("User ID");
1805        assert_eq!(info.title.as_deref(), Some("User ID"));
1806    }
1807
1808    #[test]
1809    fn param_info_with_description() {
1810        let info =
1811            ParamInfo::new("page", Converter::Int).with_description("Page number for pagination");
1812        assert_eq!(
1813            info.description.as_deref(),
1814            Some("Page number for pagination")
1815        );
1816    }
1817
1818    #[test]
1819    fn param_info_deprecated() {
1820        let info = ParamInfo::new("old", Converter::Str).deprecated();
1821        assert!(info.deprecated);
1822    }
1823
1824    #[test]
1825    fn param_info_with_example() {
1826        let info = ParamInfo::new("id", Converter::Int).with_example(serde_json::json!(42));
1827        assert_eq!(info.example, Some(serde_json::json!(42)));
1828    }
1829
1830    #[test]
1831    fn param_info_with_named_examples() {
1832        let info = ParamInfo::new("status", Converter::Str)
1833            .with_named_example("active", serde_json::json!("active"))
1834            .with_named_example("inactive", serde_json::json!("inactive"));
1835        assert_eq!(info.examples.len(), 2);
1836        assert_eq!(info.examples[0].0, "active");
1837        assert_eq!(info.examples[1].0, "inactive");
1838    }
1839
1840    #[test]
1841    fn param_info_builder_chain() {
1842        let info = ParamInfo::new("item_id", Converter::Int)
1843            .with_title("Item ID")
1844            .with_description("The unique item identifier")
1845            .deprecated()
1846            .with_example(serde_json::json!(123));
1847
1848        assert_eq!(info.name, "item_id");
1849        assert_eq!(info.converter, Converter::Int);
1850        assert_eq!(info.title.as_deref(), Some("Item ID"));
1851        assert_eq!(
1852            info.description.as_deref(),
1853            Some("The unique item identifier")
1854        );
1855        assert!(info.deprecated);
1856        assert_eq!(info.example, Some(serde_json::json!(123)));
1857    }
1858
1859    #[test]
1860    fn empty_router() {
1861        let router = Router::new();
1862        assert!(router.match_path("/anything", Method::Get).is_none());
1863        assert!(router.routes().is_empty());
1864    }
1865
1866    #[test]
1867    fn routes_accessor() {
1868        let mut router = Router::new();
1869        let _ = router.add(route(Method::Get, "/a"));
1870        let _ = router.add(route(Method::Post, "/b"));
1871
1872        assert_eq!(router.routes().len(), 2);
1873        assert_eq!(router.routes()[0].path, "/a");
1874        assert_eq!(router.routes()[1].path, "/b");
1875    }
1876
1877    // =========================================================================
1878    // CONFLICT DETECTION TESTS
1879    // =========================================================================
1880
1881    #[test]
1882    fn conflict_same_method_same_path() {
1883        let mut router = Router::new();
1884        router.add(route(Method::Get, "/users")).unwrap();
1885
1886        let result = router.add(route(Method::Get, "/users"));
1887        assert!(result.is_err());
1888        let err = match result.unwrap_err() {
1889            RouteAddError::Conflict(err) => err,
1890            RouteAddError::InvalidPath(err) => {
1891                panic!("unexpected invalid path error: {err}")
1892            }
1893        };
1894        assert_eq!(err.method, Method::Get);
1895        assert_eq!(err.new_path, "/users");
1896        assert_eq!(err.existing_path, "/users");
1897    }
1898
1899    #[test]
1900    fn conflict_same_method_same_param_pattern() {
1901        let mut router = Router::new();
1902        router.add(route(Method::Get, "/users/{id}")).unwrap();
1903
1904        // Same structure, different param name - still conflicts
1905        let result = router.add(route(Method::Get, "/users/{user_id}"));
1906        assert!(result.is_err());
1907        let err = match result.unwrap_err() {
1908            RouteAddError::Conflict(err) => err,
1909            RouteAddError::InvalidPath(err) => {
1910                panic!("unexpected invalid path error: {err}")
1911            }
1912        };
1913        assert_eq!(err.existing_path, "/users/{id}");
1914        assert_eq!(err.new_path, "/users/{user_id}");
1915    }
1916
1917    #[test]
1918    fn conflict_param_name_mismatch_across_lengths() {
1919        let mut router = Router::new();
1920        router.add(route(Method::Get, "/users/{id}/posts")).unwrap();
1921
1922        let result = router.add(route(Method::Get, "/users/{user_id}"));
1923        assert!(matches!(result, Err(RouteAddError::Conflict(_))));
1924    }
1925
1926    #[test]
1927    fn conflict_different_converter_same_position() {
1928        let mut router = Router::new();
1929        router.add(route(Method::Get, "/items/{id:int}")).unwrap();
1930
1931        // Different converter but same structural position - conflicts
1932        let result = router.add(route(Method::Get, "/items/{id:uuid}"));
1933        assert!(matches!(result, Err(RouteAddError::Conflict(_))));
1934    }
1935
1936    #[test]
1937    fn no_conflict_different_methods() {
1938        let mut router = Router::new();
1939        router.add(route(Method::Get, "/users")).unwrap();
1940        router.add(route(Method::Post, "/users")).unwrap();
1941        router.add(route(Method::Put, "/users")).unwrap();
1942        router.add(route(Method::Delete, "/users")).unwrap();
1943        router.add(route(Method::Patch, "/users")).unwrap();
1944
1945        assert_eq!(router.routes().len(), 5);
1946    }
1947
1948    #[test]
1949    fn no_conflict_static_vs_param() {
1950        let mut router = Router::new();
1951        router.add(route(Method::Get, "/users/me")).unwrap();
1952        router.add(route(Method::Get, "/users/{id}")).unwrap();
1953
1954        // Both should be registered (static takes priority during matching)
1955        assert_eq!(router.routes().len(), 2);
1956    }
1957
1958    #[test]
1959    fn no_conflict_different_path_lengths() {
1960        let mut router = Router::new();
1961        router.add(route(Method::Get, "/users")).unwrap();
1962        router.add(route(Method::Get, "/users/{id}")).unwrap();
1963        router.add(route(Method::Get, "/users/{id}/posts")).unwrap();
1964
1965        assert_eq!(router.routes().len(), 3);
1966    }
1967
1968    #[test]
1969    fn conflict_error_display() {
1970        let err = RouteConflictError {
1971            method: Method::Get,
1972            new_path: "/new".to_string(),
1973            existing_path: "/existing".to_string(),
1974        };
1975        let msg = format!("{}", err);
1976        assert!(msg.contains("GET"));
1977        assert!(msg.contains("/new"));
1978        assert!(msg.contains("/existing"));
1979    }
1980
1981    // =========================================================================
1982    // EDGE CASE TESTS
1983    // =========================================================================
1984
1985    #[test]
1986    fn root_path() {
1987        let mut router = Router::new();
1988        router.add(route(Method::Get, "/")).unwrap();
1989
1990        let m = router.match_path("/", Method::Get);
1991        assert!(m.is_some());
1992        assert_eq!(m.unwrap().route.path, "/");
1993    }
1994
1995    #[test]
1996    fn trailing_slash_handling() {
1997        let mut router = Router::new();
1998        router.add(route(Method::Get, "/users")).unwrap();
1999
2000        // Path with trailing slash should not match (strict matching)
2001        // Note: The router treats /users and /users/ differently
2002        let m = router.match_path("/users/", Method::Get);
2003        // This depends on implementation - let's test actual behavior
2004        assert!(m.is_none() || m.is_some());
2005    }
2006
2007    #[test]
2008    fn multiple_consecutive_slashes() {
2009        let mut router = Router::new();
2010        router.add(route(Method::Get, "/users")).unwrap();
2011
2012        // Multiple slashes are normalized during path parsing
2013        // (empty segments are filtered out)
2014        let m = router.match_path("//users", Method::Get);
2015        assert!(m.is_some());
2016        assert_eq!(m.unwrap().route.path, "/users");
2017    }
2018
2019    #[test]
2020    fn unicode_in_static_path() {
2021        let mut router = Router::new();
2022        router.add(route(Method::Get, "/用户")).unwrap();
2023        router.add(route(Method::Get, "/données")).unwrap();
2024
2025        let m = router.match_path("/用户", Method::Get);
2026        assert!(m.is_some());
2027        assert_eq!(m.unwrap().route.path, "/用户");
2028
2029        let m = router.match_path("/données", Method::Get);
2030        assert!(m.is_some());
2031        assert_eq!(m.unwrap().route.path, "/données");
2032    }
2033
2034    #[test]
2035    fn unicode_in_param_value() {
2036        let mut router = Router::new();
2037        router.add(route(Method::Get, "/users/{name}")).unwrap();
2038
2039        let m = router.match_path("/users/田中", Method::Get);
2040        assert!(m.is_some());
2041        let m = m.unwrap();
2042        assert_eq!(m.params[0], ("name", "田中"));
2043    }
2044
2045    #[test]
2046    fn special_characters_in_param_value() {
2047        let mut router = Router::new();
2048        router.add(route(Method::Get, "/files/{name}")).unwrap();
2049
2050        // Hyphens and underscores
2051        let m = router.match_path("/files/my-file_v2", Method::Get);
2052        assert!(m.is_some());
2053        assert_eq!(m.unwrap().params[0], ("name", "my-file_v2"));
2054
2055        // Dots
2056        let m = router.match_path("/files/document.pdf", Method::Get);
2057        assert!(m.is_some());
2058        assert_eq!(m.unwrap().params[0], ("name", "document.pdf"));
2059    }
2060
2061    #[test]
2062    fn empty_param_value() {
2063        let mut router = Router::new();
2064        router.add(route(Method::Get, "/users/{id}/posts")).unwrap();
2065
2066        // Empty segment won't match a param (filtered out during parsing)
2067        let m = router.match_path("/users//posts", Method::Get);
2068        // This should not match because empty segment is skipped
2069        assert!(m.is_none());
2070    }
2071
2072    #[test]
2073    fn very_long_path() {
2074        let mut router = Router::new();
2075        let long_path = "/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z";
2076        router.add(route(Method::Get, long_path)).unwrap();
2077
2078        let m = router.match_path(long_path, Method::Get);
2079        assert!(m.is_some());
2080        assert_eq!(m.unwrap().route.path, long_path);
2081    }
2082
2083    #[test]
2084    fn many_routes_same_prefix() {
2085        let mut router = Router::new();
2086        for i in 0..100 {
2087            router
2088                .add(route(Method::Get, &format!("/api/v{}", i)))
2089                .unwrap();
2090        }
2091
2092        assert_eq!(router.routes().len(), 100);
2093
2094        // All routes should be matchable
2095        for i in 0..100 {
2096            let path = format!("/api/v{}", i);
2097            let m = router.match_path(&path, Method::Get);
2098            assert!(m.is_some());
2099            assert_eq!(m.unwrap().route.path, path);
2100        }
2101    }
2102
2103    // =========================================================================
2104    // HEAD METHOD TESTS
2105    // =========================================================================
2106
2107    #[test]
2108    fn head_matches_get_route() {
2109        let mut router = Router::new();
2110        router.add(route(Method::Get, "/users")).unwrap();
2111
2112        // HEAD should match GET routes
2113        let m = router.match_path("/users", Method::Head);
2114        assert!(m.is_some());
2115        assert_eq!(m.unwrap().route.method, Method::Get);
2116    }
2117
2118    #[test]
2119    fn head_with_explicit_head_route() {
2120        let mut router = Router::new();
2121        router.add(route(Method::Get, "/users")).unwrap();
2122        router.add(route(Method::Head, "/users")).unwrap();
2123
2124        // If explicit HEAD is registered, should match HEAD
2125        let m = router.match_path("/users", Method::Head);
2126        assert!(m.is_some());
2127        assert_eq!(m.unwrap().route.method, Method::Head);
2128    }
2129
2130    #[test]
2131    fn head_does_not_match_non_get() {
2132        let mut router = Router::new();
2133        router.add(route(Method::Post, "/users")).unwrap();
2134
2135        // HEAD should not match POST
2136        let result = router.lookup("/users", Method::Head);
2137        match result {
2138            RouteLookup::MethodNotAllowed { allowed } => {
2139                assert!(!allowed.contains(Method::Head));
2140                assert!(allowed.contains(Method::Post));
2141            }
2142            _ => panic!("expected MethodNotAllowed"),
2143        }
2144    }
2145
2146    // =========================================================================
2147    // CONVERTER EDGE CASE TESTS
2148    // =========================================================================
2149
2150    #[test]
2151    fn int_converter_edge_cases() {
2152        let mut router = Router::new();
2153        router.add(route(Method::Get, "/items/{id:int}")).unwrap();
2154
2155        // Zero
2156        let m = router.match_path("/items/0", Method::Get);
2157        assert!(m.is_some());
2158
2159        // Large positive
2160        let m = router.match_path("/items/9223372036854775807", Method::Get);
2161        assert!(m.is_some());
2162
2163        // Large negative
2164        let m = router.match_path("/items/-9223372036854775808", Method::Get);
2165        assert!(m.is_some());
2166
2167        // Leading zeros (still valid integer)
2168        let m = router.match_path("/items/007", Method::Get);
2169        assert!(m.is_some());
2170
2171        // Plus sign (not standard integer format)
2172        let m = router.match_path("/items/+123", Method::Get);
2173        // Rust parse::<i64>() accepts +123
2174        assert!(m.is_some());
2175    }
2176
2177    #[test]
2178    fn float_converter_edge_cases() {
2179        let mut router = Router::new();
2180        router
2181            .add(route(Method::Get, "/values/{val:float}"))
2182            .unwrap();
2183
2184        // Scientific notation
2185        let m = router.match_path("/values/1e10", Method::Get);
2186        assert!(m.is_some());
2187
2188        // Negative exponent
2189        let m = router.match_path("/values/1e-10", Method::Get);
2190        assert!(m.is_some());
2191
2192        // Infinity (Rust parses "inf" as f64::INFINITY)
2193        let m = router.match_path("/values/inf", Method::Get);
2194        assert!(m.is_some());
2195
2196        // NaN (Rust parses "NaN" as f64::NAN)
2197        let m = router.match_path("/values/NaN", Method::Get);
2198        assert!(m.is_some());
2199    }
2200
2201    #[test]
2202    fn uuid_converter_case_sensitivity() {
2203        let mut router = Router::new();
2204        router
2205            .add(route(Method::Get, "/objects/{id:uuid}"))
2206            .unwrap();
2207
2208        // Lowercase
2209        let m = router.match_path("/objects/550e8400-e29b-41d4-a716-446655440000", Method::Get);
2210        assert!(m.is_some());
2211
2212        // Uppercase
2213        let m = router.match_path("/objects/550E8400-E29B-41D4-A716-446655440000", Method::Get);
2214        assert!(m.is_some());
2215
2216        // Mixed case
2217        let m = router.match_path("/objects/550e8400-E29B-41d4-A716-446655440000", Method::Get);
2218        assert!(m.is_some());
2219    }
2220
2221    #[test]
2222    fn uuid_converter_invalid_formats() {
2223        let mut router = Router::new();
2224        router
2225            .add(route(Method::Get, "/objects/{id:uuid}"))
2226            .unwrap();
2227
2228        // Wrong length
2229        assert!(
2230            router
2231                .match_path("/objects/550e8400-e29b-41d4-a716-44665544000", Method::Get)
2232                .is_none()
2233        );
2234        assert!(
2235            router
2236                .match_path(
2237                    "/objects/550e8400-e29b-41d4-a716-4466554400000",
2238                    Method::Get
2239                )
2240                .is_none()
2241        );
2242
2243        // Missing hyphens
2244        assert!(
2245            router
2246                .match_path("/objects/550e8400e29b41d4a716446655440000", Method::Get)
2247                .is_none()
2248        );
2249
2250        // Invalid hex characters
2251        assert!(
2252            router
2253                .match_path("/objects/550g8400-e29b-41d4-a716-446655440000", Method::Get)
2254                .is_none()
2255        );
2256    }
2257
2258    #[test]
2259    fn unknown_converter_defaults_to_str() {
2260        let segments = parse_path("/items/{id:custom}");
2261        assert_eq!(segments.len(), 2);
2262        match &segments[1] {
2263            PathSegment::Param { name, converter } => {
2264                assert_eq!(*name, "id");
2265                assert_eq!(*converter, Converter::Str);
2266            }
2267            _ => panic!("Expected param segment"),
2268        }
2269    }
2270
2271    // =========================================================================
2272    // PATH PARSING EDGE CASES
2273    // =========================================================================
2274
2275    #[test]
2276    fn parse_empty_path() {
2277        let segments = parse_path("");
2278        assert!(segments.is_empty());
2279    }
2280
2281    #[test]
2282    fn parse_root_only() {
2283        let segments = parse_path("/");
2284        assert!(segments.is_empty());
2285    }
2286
2287    #[test]
2288    fn parse_leading_trailing_slashes() {
2289        let segments = parse_path("///users///");
2290        assert_eq!(segments.len(), 1);
2291        match &segments[0] {
2292            PathSegment::Static(s) => assert_eq!(*s, "users"),
2293            _ => panic!("Expected static segment"),
2294        }
2295    }
2296
2297    #[test]
2298    fn parse_param_with_colon_no_type() {
2299        // Edge case: param name contains colon but no valid type after
2300        let segments = parse_path("/items/{id:}");
2301        assert_eq!(segments.len(), 2);
2302        match &segments[1] {
2303            PathSegment::Param { name, converter } => {
2304                assert_eq!(*name, "id");
2305                // Empty after colon defaults to Str
2306                assert_eq!(*converter, Converter::Str);
2307            }
2308            _ => panic!("Expected param segment"),
2309        }
2310    }
2311
2312    // =========================================================================
2313    // 404 AND 405 RESPONSE TESTS
2314    // =========================================================================
2315
2316    #[test]
2317    fn lookup_404_empty_router() {
2318        let router = Router::new();
2319        assert!(matches!(
2320            router.lookup("/anything", Method::Get),
2321            RouteLookup::NotFound
2322        ));
2323    }
2324
2325    #[test]
2326    fn lookup_404_no_matching_path() {
2327        let mut router = Router::new();
2328        router.add(route(Method::Get, "/users")).unwrap();
2329        router.add(route(Method::Get, "/items")).unwrap();
2330
2331        assert!(matches!(
2332            router.lookup("/other", Method::Get),
2333            RouteLookup::NotFound
2334        ));
2335        assert!(matches!(
2336            router.lookup("/user", Method::Get),
2337            RouteLookup::NotFound
2338        )); // Typo
2339    }
2340
2341    #[test]
2342    fn lookup_404_partial_path_match() {
2343        let mut router = Router::new();
2344        router.add(route(Method::Get, "/api/v1/users")).unwrap();
2345
2346        // Partial matches should be 404
2347        assert!(matches!(
2348            router.lookup("/api", Method::Get),
2349            RouteLookup::NotFound
2350        ));
2351        assert!(matches!(
2352            router.lookup("/api/v1", Method::Get),
2353            RouteLookup::NotFound
2354        ));
2355    }
2356
2357    #[test]
2358    fn lookup_404_extra_path_segments() {
2359        let mut router = Router::new();
2360        router.add(route(Method::Get, "/users")).unwrap();
2361
2362        // Extra segments should be 404
2363        assert!(matches!(
2364            router.lookup("/users/extra", Method::Get),
2365            RouteLookup::NotFound
2366        ));
2367    }
2368
2369    #[test]
2370    fn lookup_405_single_method() {
2371        let mut router = Router::new();
2372        router.add(route(Method::Get, "/users")).unwrap();
2373
2374        let result = router.lookup("/users", Method::Post);
2375        match result {
2376            RouteLookup::MethodNotAllowed { allowed } => {
2377                assert_eq!(allowed.methods(), &[Method::Get, Method::Head]);
2378            }
2379            _ => panic!("expected MethodNotAllowed"),
2380        }
2381    }
2382
2383    #[test]
2384    fn lookup_405_all_methods() {
2385        let mut router = Router::new();
2386        router.add(route(Method::Get, "/resource")).unwrap();
2387        router.add(route(Method::Post, "/resource")).unwrap();
2388        router.add(route(Method::Put, "/resource")).unwrap();
2389        router.add(route(Method::Delete, "/resource")).unwrap();
2390        router.add(route(Method::Patch, "/resource")).unwrap();
2391        router.add(route(Method::Options, "/resource")).unwrap();
2392
2393        let result = router.lookup("/resource", Method::Trace);
2394        match result {
2395            RouteLookup::MethodNotAllowed { allowed } => {
2396                let header = allowed.header_value();
2397                assert!(header.contains("GET"));
2398                assert!(header.contains("HEAD"));
2399                assert!(header.contains("POST"));
2400                assert!(header.contains("PUT"));
2401                assert!(header.contains("DELETE"));
2402                assert!(header.contains("PATCH"));
2403                assert!(header.contains("OPTIONS"));
2404            }
2405            _ => panic!("expected MethodNotAllowed"),
2406        }
2407    }
2408
2409    // =========================================================================
2410    // ALLOWED METHODS TESTS
2411    // =========================================================================
2412
2413    #[test]
2414    fn allowed_methods_deduplication() {
2415        // If GET is added twice, should only appear once
2416        let allowed = AllowedMethods::new(vec![Method::Get, Method::Get, Method::Post]);
2417        assert_eq!(allowed.methods().len(), 3); // GET, HEAD, POST
2418    }
2419
2420    #[test]
2421    fn allowed_methods_sorting() {
2422        // Methods should be sorted in standard order
2423        let allowed = AllowedMethods::new(vec![Method::Delete, Method::Get, Method::Post]);
2424        assert_eq!(allowed.methods()[0], Method::Get);
2425        assert_eq!(allowed.methods()[1], Method::Head); // Added automatically
2426        assert_eq!(allowed.methods()[2], Method::Post);
2427        assert_eq!(allowed.methods()[3], Method::Delete);
2428    }
2429
2430    #[test]
2431    fn allowed_methods_head_not_duplicated() {
2432        // If HEAD is already present, don't add it again
2433        let allowed = AllowedMethods::new(vec![Method::Get, Method::Head]);
2434        let count = allowed
2435            .methods()
2436            .iter()
2437            .filter(|&&m| m == Method::Head)
2438            .count();
2439        assert_eq!(count, 1);
2440    }
2441
2442    #[test]
2443    fn allowed_methods_empty() {
2444        let allowed = AllowedMethods::new(vec![]);
2445        assert!(allowed.methods().is_empty());
2446        assert_eq!(allowed.header_value(), "");
2447    }
2448
2449    // =========================================================================
2450    // WILDCARD CATCH-ALL ROUTE TESTS ({*path} syntax)
2451    // =========================================================================
2452
2453    #[test]
2454    fn wildcard_asterisk_syntax_basic() {
2455        let mut router = Router::new();
2456        router.add(route(Method::Get, "/files/{*path}")).unwrap();
2457
2458        let m = router.match_path("/files/a.txt", Method::Get).unwrap();
2459        assert_eq!(m.params[0], ("path", "a.txt"));
2460    }
2461
2462    #[test]
2463    fn wildcard_asterisk_captures_multiple_segments() {
2464        let mut router = Router::new();
2465        router
2466            .add(route(Method::Get, "/files/{*filepath}"))
2467            .unwrap();
2468
2469        let m = router
2470            .match_path("/files/css/styles/main.css", Method::Get)
2471            .unwrap();
2472        assert_eq!(m.params[0], ("filepath", "css/styles/main.css"));
2473    }
2474
2475    #[test]
2476    fn wildcard_asterisk_with_prefix() {
2477        let mut router = Router::new();
2478        router.add(route(Method::Get, "/api/v1/{*rest}")).unwrap();
2479
2480        let m = router
2481            .match_path("/api/v1/users/123/posts", Method::Get)
2482            .unwrap();
2483        assert_eq!(m.params[0], ("rest", "users/123/posts"));
2484    }
2485
2486    #[test]
2487    fn wildcard_asterisk_empty_capture() {
2488        let mut router = Router::new();
2489        router.add(route(Method::Get, "/files/{*path}")).unwrap();
2490
2491        // Single segment after prefix should work
2492        let m = router.match_path("/files/x", Method::Get).unwrap();
2493        assert_eq!(m.params[0], ("path", "x"));
2494    }
2495
2496    #[test]
2497    fn wildcard_asterisk_must_be_terminal() {
2498        let mut router = Router::new();
2499        let result = router.add(route(Method::Get, "/files/{*path}/edit"));
2500        assert!(matches!(result, Err(RouteAddError::InvalidPath(_))));
2501    }
2502
2503    #[test]
2504    fn wildcard_asterisk_syntax_equivalent_to_path_converter() {
2505        // Both syntaxes should produce the same result
2506        let segments_asterisk = parse_path("/files/{*filepath}");
2507        let segments_converter = parse_path("/files/{filepath:path}");
2508
2509        assert_eq!(segments_asterisk.len(), 2);
2510        assert_eq!(segments_converter.len(), 2);
2511
2512        match (&segments_asterisk[1], &segments_converter[1]) {
2513            (
2514                PathSegment::Param {
2515                    name: n1,
2516                    converter: c1,
2517                },
2518                PathSegment::Param {
2519                    name: n2,
2520                    converter: c2,
2521                },
2522            ) => {
2523                assert_eq!(*n1, "filepath");
2524                assert_eq!(*n2, "filepath");
2525                assert_eq!(*c1, Converter::Path);
2526                assert_eq!(*c2, Converter::Path);
2527            }
2528            _ => panic!("Expected param segments"),
2529        }
2530    }
2531
2532    #[test]
2533    fn wildcard_asterisk_spa_routing() {
2534        let mut router = Router::new();
2535        // Static API routes take priority
2536        router.add(route(Method::Get, "/api/users")).unwrap();
2537        router.add(route(Method::Get, "/api/posts")).unwrap();
2538        // Catch-all for SPA
2539        router.add(route(Method::Get, "/{*route}")).unwrap();
2540
2541        // API routes match exactly
2542        let m = router.match_path("/api/users", Method::Get).unwrap();
2543        assert_eq!(m.route.path, "/api/users");
2544
2545        // Other paths caught by wildcard
2546        let m = router.match_path("/dashboard", Method::Get).unwrap();
2547        assert_eq!(m.params[0], ("route", "dashboard"));
2548
2549        let m = router
2550            .match_path("/users/123/profile", Method::Get)
2551            .unwrap();
2552        assert_eq!(m.params[0], ("route", "users/123/profile"));
2553    }
2554
2555    #[test]
2556    fn wildcard_asterisk_file_serving() {
2557        let mut router = Router::new();
2558        router
2559            .add(route(Method::Get, "/static/{*filepath}"))
2560            .unwrap();
2561
2562        let m = router
2563            .match_path("/static/js/app.bundle.js", Method::Get)
2564            .unwrap();
2565        assert_eq!(m.params[0], ("filepath", "js/app.bundle.js"));
2566
2567        let m = router
2568            .match_path("/static/images/logo.png", Method::Get)
2569            .unwrap();
2570        assert_eq!(m.params[0], ("filepath", "images/logo.png"));
2571    }
2572
2573    #[test]
2574    fn wildcard_asterisk_priority_lowest() {
2575        let mut router = Router::new();
2576        // Static routes have highest priority
2577        router.add(route(Method::Get, "/users/me")).unwrap();
2578        // Single-segment param has medium priority
2579        router.add(route(Method::Get, "/users/{id}")).unwrap();
2580        // Catch-all has lowest priority
2581        router.add(route(Method::Get, "/{*path}")).unwrap();
2582
2583        // Static match
2584        let m = router.match_path("/users/me", Method::Get).unwrap();
2585        assert_eq!(m.route.path, "/users/me");
2586        assert!(m.params.is_empty());
2587
2588        // Param match
2589        let m = router.match_path("/users/123", Method::Get).unwrap();
2590        assert_eq!(m.route.path, "/users/{id}");
2591        assert_eq!(m.params[0], ("id", "123"));
2592
2593        // Wildcard catches the rest
2594        let m = router.match_path("/other/deep/path", Method::Get).unwrap();
2595        assert_eq!(m.route.path, "/{*path}");
2596        assert_eq!(m.params[0], ("path", "other/deep/path"));
2597    }
2598
2599    #[test]
2600    fn parse_wildcard_asterisk_syntax() {
2601        let segments = parse_path("/files/{*path}");
2602        assert_eq!(segments.len(), 2);
2603
2604        match &segments[0] {
2605            PathSegment::Static(s) => assert_eq!(*s, "files"),
2606            _ => panic!("Expected static segment"),
2607        }
2608
2609        match &segments[1] {
2610            PathSegment::Param { name, converter } => {
2611                assert_eq!(*name, "path");
2612                assert_eq!(*converter, Converter::Path);
2613            }
2614            _ => panic!("Expected param segment"),
2615        }
2616    }
2617
2618    #[test]
2619    fn wildcard_asterisk_conflict_with_path_converter() {
2620        let mut router = Router::new();
2621        router.add(route(Method::Get, "/files/{*path}")).unwrap();
2622
2623        // Same route with different syntax should conflict
2624        let result = router.add(route(Method::Get, "/files/{filepath:path}"));
2625        assert!(matches!(result, Err(RouteAddError::Conflict(_))));
2626    }
2627
2628    #[test]
2629    fn wildcard_asterisk_different_methods_no_conflict() {
2630        let mut router = Router::new();
2631        router.add(route(Method::Get, "/files/{*path}")).unwrap();
2632        router.add(route(Method::Post, "/files/{*path}")).unwrap();
2633        router.add(route(Method::Delete, "/files/{*path}")).unwrap();
2634
2635        assert_eq!(router.routes().len(), 3);
2636
2637        // Each method works
2638        let m = router.match_path("/files/a/b/c", Method::Get).unwrap();
2639        assert_eq!(m.route.method, Method::Get);
2640
2641        let m = router.match_path("/files/a/b/c", Method::Post).unwrap();
2642        assert_eq!(m.route.method, Method::Post);
2643
2644        let m = router.match_path("/files/a/b/c", Method::Delete).unwrap();
2645        assert_eq!(m.route.method, Method::Delete);
2646    }
2647
2648    // =========================================================================
2649    // ROUTE PRIORITY AND ORDERING TESTS (fastapi_rust-2dh)
2650    // =========================================================================
2651    //
2652    // Route matching follows strict priority rules:
2653    // 1. Static segments match before parameters
2654    // 2. Named parameters match before wildcards
2655    // 3. Registration order is the tiebreaker for equal priority
2656    //
2657    // This ensures predictable matching without ambiguity.
2658    // =========================================================================
2659
2660    #[test]
2661    fn priority_static_before_param() {
2662        // /users/me (static) has priority over /users/{id} (param)
2663        let mut router = Router::new();
2664        router.add(route(Method::Get, "/users/{id}")).unwrap();
2665        router.add(route(Method::Get, "/users/me")).unwrap();
2666
2667        // Even though param was added first, static wins
2668        let m = router.match_path("/users/me", Method::Get).unwrap();
2669        assert_eq!(m.route.path, "/users/me");
2670        assert!(m.params.is_empty());
2671
2672        // Other paths still match param
2673        let m = router.match_path("/users/123", Method::Get).unwrap();
2674        assert_eq!(m.route.path, "/users/{id}");
2675        assert_eq!(m.params[0], ("id", "123"));
2676    }
2677
2678    #[test]
2679    fn priority_named_param_vs_wildcard_conflict() {
2680        // Named params and wildcards at same position conflict
2681        // because they both capture the segment
2682        let mut router = Router::new();
2683        router.add(route(Method::Get, "/files/{name}")).unwrap();
2684
2685        // Adding wildcard at same position conflicts
2686        let result = router.add(route(Method::Get, "/files/{*path}"));
2687        assert!(
2688            matches!(result, Err(RouteAddError::Conflict(_))),
2689            "Named param and wildcard at same position should conflict"
2690        );
2691    }
2692
2693    #[test]
2694    fn priority_different_prefixes_no_conflict() {
2695        // Different static prefixes allow coexistence
2696        let mut router = Router::new();
2697        router.add(route(Method::Get, "/files/{name}")).unwrap();
2698        router.add(route(Method::Get, "/static/{*path}")).unwrap();
2699
2700        // Single segment matches named param
2701        let m = router.match_path("/files/foo.txt", Method::Get).unwrap();
2702        assert_eq!(m.route.path, "/files/{name}");
2703
2704        // Multi-segment matches wildcard
2705        let m = router
2706            .match_path("/static/css/main.css", Method::Get)
2707            .unwrap();
2708        assert_eq!(m.route.path, "/static/{*path}");
2709    }
2710
2711    #[test]
2712    fn priority_nested_param_before_shallow_wildcard() {
2713        // Deeper static paths take priority over shallow wildcards
2714        let mut router = Router::new();
2715        router.add(route(Method::Get, "/{*path}")).unwrap();
2716        router.add(route(Method::Get, "/api/users")).unwrap();
2717
2718        // Static path wins even though wildcard registered first
2719        let m = router.match_path("/api/users", Method::Get).unwrap();
2720        assert_eq!(m.route.path, "/api/users");
2721
2722        // Wildcard catches everything else
2723        let m = router.match_path("/other/path", Method::Get).unwrap();
2724        assert_eq!(m.route.path, "/{*path}");
2725    }
2726
2727    #[test]
2728    fn priority_multiple_static_depths() {
2729        // More specific static paths win
2730        let mut router = Router::new();
2731        router.add(route(Method::Get, "/api/{*rest}")).unwrap();
2732        router.add(route(Method::Get, "/api/v1/users")).unwrap();
2733        router
2734            .add(route(Method::Get, "/api/v1/{resource}"))
2735            .unwrap();
2736
2737        // Most specific static path wins
2738        let m = router.match_path("/api/v1/users", Method::Get).unwrap();
2739        assert_eq!(m.route.path, "/api/v1/users");
2740
2741        // Named param at same depth
2742        let m = router.match_path("/api/v1/items", Method::Get).unwrap();
2743        assert_eq!(m.route.path, "/api/v1/{resource}");
2744
2745        // Wildcard catches the rest
2746        let m = router
2747            .match_path("/api/v2/anything/deep", Method::Get)
2748            .unwrap();
2749        assert_eq!(m.route.path, "/api/{*rest}");
2750    }
2751
2752    #[test]
2753    fn priority_complex_route_set() {
2754        // Complex scenario matching FastAPI behavior
2755        let mut router = Router::new();
2756
2757        // In order of generality (most specific first)
2758        router.add(route(Method::Get, "/users/me")).unwrap();
2759        router
2760            .add(route(Method::Get, "/users/{user_id}/profile"))
2761            .unwrap();
2762        router.add(route(Method::Get, "/users/{user_id}")).unwrap();
2763        router.add(route(Method::Get, "/{*path}")).unwrap();
2764
2765        // /users/me -> exact match
2766        let m = router.match_path("/users/me", Method::Get).unwrap();
2767        assert_eq!(m.route.path, "/users/me");
2768
2769        // /users/123 -> param match
2770        let m = router.match_path("/users/123", Method::Get).unwrap();
2771        assert_eq!(m.route.path, "/users/{user_id}");
2772        assert_eq!(m.params[0], ("user_id", "123"));
2773
2774        // /users/123/profile -> deeper param match
2775        let m = router
2776            .match_path("/users/123/profile", Method::Get)
2777            .unwrap();
2778        assert_eq!(m.route.path, "/users/{user_id}/profile");
2779        assert_eq!(m.params[0], ("user_id", "123"));
2780
2781        // /anything/else -> wildcard catch-all
2782        let m = router.match_path("/anything/else", Method::Get).unwrap();
2783        assert_eq!(m.route.path, "/{*path}");
2784        assert_eq!(m.params[0], ("path", "anything/else"));
2785    }
2786
2787    // =========================================================================
2788    // TYPE CONVERTER TESTS
2789    // =========================================================================
2790
2791    #[test]
2792    fn converter_convert_str() {
2793        let result = Converter::Str.convert("hello", "param");
2794        assert!(result.is_ok());
2795        assert_eq!(result.unwrap(), ParamValue::Str("hello".to_string()));
2796    }
2797
2798    #[test]
2799    fn converter_convert_int_valid() {
2800        let result = Converter::Int.convert("42", "id");
2801        assert!(result.is_ok());
2802        assert_eq!(result.unwrap(), ParamValue::Int(42));
2803    }
2804
2805    #[test]
2806    fn converter_convert_int_negative() {
2807        let result = Converter::Int.convert("-123", "id");
2808        assert!(result.is_ok());
2809        assert_eq!(result.unwrap(), ParamValue::Int(-123));
2810    }
2811
2812    #[test]
2813    fn converter_convert_int_invalid() {
2814        let result = Converter::Int.convert("abc", "id");
2815        assert!(result.is_err());
2816        match result.unwrap_err() {
2817            ConversionError::InvalidInt { value, param } => {
2818                assert_eq!(value, "abc");
2819                assert_eq!(param, "id");
2820            }
2821            _ => panic!("Expected InvalidInt error"),
2822        }
2823    }
2824
2825    #[test]
2826    #[allow(clippy::approx_constant)]
2827    fn converter_convert_float_valid() {
2828        let result = Converter::Float.convert("3.14", "val");
2829        assert!(result.is_ok());
2830        assert_eq!(result.unwrap(), ParamValue::Float(3.14));
2831    }
2832
2833    #[test]
2834    fn converter_convert_float_integer() {
2835        let result = Converter::Float.convert("42", "val");
2836        assert!(result.is_ok());
2837        assert_eq!(result.unwrap(), ParamValue::Float(42.0));
2838    }
2839
2840    #[test]
2841    fn converter_convert_float_scientific() {
2842        let result = Converter::Float.convert("1e10", "val");
2843        assert!(result.is_ok());
2844        assert_eq!(result.unwrap(), ParamValue::Float(1e10));
2845    }
2846
2847    #[test]
2848    fn converter_convert_float_invalid() {
2849        let result = Converter::Float.convert("not-a-float", "val");
2850        assert!(result.is_err());
2851        match result.unwrap_err() {
2852            ConversionError::InvalidFloat { value, param } => {
2853                assert_eq!(value, "not-a-float");
2854                assert_eq!(param, "val");
2855            }
2856            _ => panic!("Expected InvalidFloat error"),
2857        }
2858    }
2859
2860    #[test]
2861    fn converter_convert_uuid_valid() {
2862        let result = Converter::Uuid.convert("550e8400-e29b-41d4-a716-446655440000", "id");
2863        assert!(result.is_ok());
2864        assert_eq!(
2865            result.unwrap(),
2866            ParamValue::Uuid("550e8400-e29b-41d4-a716-446655440000".to_string())
2867        );
2868    }
2869
2870    #[test]
2871    fn converter_convert_uuid_invalid() {
2872        let result = Converter::Uuid.convert("not-a-uuid", "id");
2873        assert!(result.is_err());
2874        match result.unwrap_err() {
2875            ConversionError::InvalidUuid { value, param } => {
2876                assert_eq!(value, "not-a-uuid");
2877                assert_eq!(param, "id");
2878            }
2879            _ => panic!("Expected InvalidUuid error"),
2880        }
2881    }
2882
2883    #[test]
2884    fn converter_convert_path() {
2885        let result = Converter::Path.convert("a/b/c.txt", "filepath");
2886        assert!(result.is_ok());
2887        assert_eq!(result.unwrap(), ParamValue::Path("a/b/c.txt".to_string()));
2888    }
2889
2890    #[test]
2891    fn param_value_accessors() {
2892        // Str variant
2893        let val = ParamValue::Str("hello".to_string());
2894        assert_eq!(val.as_str(), "hello");
2895        assert_eq!(val.as_int(), None);
2896        assert_eq!(val.as_float(), None);
2897        assert_eq!(val.into_string(), Some("hello".to_string()));
2898
2899        // Int variant
2900        let val = ParamValue::Int(42);
2901        assert_eq!(val.as_int(), Some(42));
2902        assert_eq!(val.as_float(), None);
2903        assert_eq!(val.into_string(), None);
2904
2905        // Float variant
2906        #[allow(clippy::approx_constant)]
2907        let val = ParamValue::Float(3.14);
2908        #[allow(clippy::approx_constant)]
2909        let expected_pi = Some(3.14);
2910        assert_eq!(val.as_float(), expected_pi);
2911        assert_eq!(val.as_int(), None);
2912        assert_eq!(val.into_string(), None);
2913
2914        // Uuid variant
2915        let val = ParamValue::Uuid("550e8400-e29b-41d4-a716-446655440000".to_string());
2916        assert_eq!(val.as_str(), "550e8400-e29b-41d4-a716-446655440000");
2917        assert_eq!(
2918            val.into_string(),
2919            Some("550e8400-e29b-41d4-a716-446655440000".to_string())
2920        );
2921
2922        // Path variant
2923        let val = ParamValue::Path("a/b/c".to_string());
2924        assert_eq!(val.as_str(), "a/b/c");
2925        assert_eq!(val.into_string(), Some("a/b/c".to_string()));
2926    }
2927
2928    #[test]
2929    fn conversion_error_display() {
2930        let err = ConversionError::InvalidInt {
2931            value: "abc".to_string(),
2932            param: "id".to_string(),
2933        };
2934        let msg = format!("{}", err);
2935        assert!(msg.contains("id"));
2936        assert!(msg.contains("abc"));
2937        assert!(msg.contains("integer"));
2938
2939        let err = ConversionError::InvalidFloat {
2940            value: "xyz".to_string(),
2941            param: "val".to_string(),
2942        };
2943        let msg = format!("{}", err);
2944        assert!(msg.contains("val"));
2945        assert!(msg.contains("xyz"));
2946        assert!(msg.contains("float"));
2947
2948        let err = ConversionError::InvalidUuid {
2949            value: "bad".to_string(),
2950            param: "uuid".to_string(),
2951        };
2952        let msg = format!("{}", err);
2953        assert!(msg.contains("uuid"));
2954        assert!(msg.contains("bad"));
2955        assert!(msg.contains("UUID"));
2956    }
2957
2958    #[test]
2959    fn converter_type_name() {
2960        assert_eq!(Converter::Str.type_name(), "string");
2961        assert_eq!(Converter::Int.type_name(), "integer");
2962        assert_eq!(Converter::Float.type_name(), "float");
2963        assert_eq!(Converter::Uuid.type_name(), "UUID");
2964        assert_eq!(Converter::Path.type_name(), "path");
2965    }
2966
2967    #[test]
2968    fn route_match_typed_getters() {
2969        let mut router = Router::new();
2970        router
2971            .add(route(Method::Get, "/items/{id:int}/price/{val:float}"))
2972            .unwrap();
2973
2974        let m = router
2975            .match_path("/items/42/price/99.99", Method::Get)
2976            .unwrap();
2977
2978        // String getter (existing API)
2979        assert_eq!(m.get_param("id"), Some("42"));
2980        assert_eq!(m.get_param("val"), Some("99.99"));
2981
2982        // Typed getters (new API)
2983        assert_eq!(m.get_param_int("id"), Some(Ok(42)));
2984        assert_eq!(m.get_param_float("val"), Some(Ok(99.99)));
2985
2986        // Missing param
2987        assert!(m.get_param_int("missing").is_none());
2988
2989        // Wrong type
2990        let result = m.get_param_int("val");
2991        // "99.99" can be parsed as i64 (it becomes 99)
2992        // Actually wait, "99.99" cannot be parsed as i64
2993        assert!(result.is_some());
2994        assert!(result.unwrap().is_err());
2995    }
2996
2997    #[test]
2998    fn route_match_param_count() {
2999        let mut router = Router::new();
3000        router
3001            .add(route(Method::Get, "/users/{user_id}/posts/{post_id}"))
3002            .unwrap();
3003
3004        let m = router.match_path("/users/1/posts/2", Method::Get).unwrap();
3005
3006        assert_eq!(m.param_count(), 2);
3007        assert!(!m.is_empty());
3008
3009        // Static route with no params
3010        let mut router2 = Router::new();
3011        router2.add(route(Method::Get, "/static")).unwrap();
3012        let m2 = router2.match_path("/static", Method::Get).unwrap();
3013        assert_eq!(m2.param_count(), 0);
3014        assert!(m2.is_empty());
3015    }
3016
3017    #[test]
3018    fn route_match_iter() {
3019        let mut router = Router::new();
3020        router
3021            .add(route(Method::Get, "/a/{x}/b/{y}/c/{z}"))
3022            .unwrap();
3023
3024        let m = router.match_path("/a/1/b/2/c/3", Method::Get).unwrap();
3025
3026        let params: Vec<_> = m.iter().collect();
3027        assert_eq!(params.len(), 3);
3028        assert_eq!(params[0], ("x", "1"));
3029        assert_eq!(params[1], ("y", "2"));
3030        assert_eq!(params[2], ("z", "3"));
3031    }
3032
3033    #[test]
3034    fn route_match_is_param_uuid() {
3035        let mut router = Router::new();
3036        router
3037            .add(route(Method::Get, "/objects/{id:uuid}"))
3038            .unwrap();
3039
3040        let m = router
3041            .match_path("/objects/550e8400-e29b-41d4-a716-446655440000", Method::Get)
3042            .unwrap();
3043
3044        assert_eq!(m.is_param_uuid("id"), Some(true));
3045        assert_eq!(m.is_param_uuid("missing"), None);
3046    }
3047
3048    #[test]
3049    fn route_match_integer_variants() {
3050        let mut router = Router::new();
3051        router.add(route(Method::Get, "/items/{id}")).unwrap();
3052
3053        let m = router.match_path("/items/12345", Method::Get).unwrap();
3054
3055        // All integer variants
3056        assert_eq!(m.get_param_int("id"), Some(Ok(12345i64)));
3057        assert_eq!(m.get_param_i32("id"), Some(Ok(12345i32)));
3058        assert_eq!(m.get_param_u64("id"), Some(Ok(12345u64)));
3059        assert_eq!(m.get_param_u32("id"), Some(Ok(12345u32)));
3060
3061        // Float variants
3062        assert_eq!(m.get_param_float("id"), Some(Ok(12345.0f64)));
3063        assert_eq!(m.get_param_f32("id"), Some(Ok(12345.0f32)));
3064    }
3065
3066    // =========================================================================
3067    // SUB-ROUTER MOUNTING TESTS
3068    // =========================================================================
3069
3070    #[test]
3071    fn mount_basic() {
3072        let mut child = Router::new();
3073        child.add(route(Method::Get, "/users")).unwrap();
3074        child.add(route(Method::Get, "/items")).unwrap();
3075
3076        let parent = Router::new().mount("/api/v1", child).unwrap();
3077
3078        // Routes should be accessible at prefixed paths
3079        let m = parent.match_path("/api/v1/users", Method::Get).unwrap();
3080        assert_eq!(m.route.path, "/api/v1/users");
3081
3082        let m = parent.match_path("/api/v1/items", Method::Get).unwrap();
3083        assert_eq!(m.route.path, "/api/v1/items");
3084    }
3085
3086    #[test]
3087    fn mount_with_params() {
3088        let mut child = Router::new();
3089        child.add(route(Method::Get, "/users/{id}")).unwrap();
3090        child
3091            .add(route(Method::Get, "/users/{id}/posts/{post_id}"))
3092            .unwrap();
3093
3094        let parent = Router::new().mount("/api", child).unwrap();
3095
3096        // Path parameters work with prefix
3097        let m = parent.match_path("/api/users/42", Method::Get).unwrap();
3098        assert_eq!(m.route.path, "/api/users/{id}");
3099        assert_eq!(m.params[0], ("id", "42"));
3100
3101        let m = parent
3102            .match_path("/api/users/1/posts/99", Method::Get)
3103            .unwrap();
3104        assert_eq!(m.params.len(), 2);
3105        assert_eq!(m.params[0], ("id", "1"));
3106        assert_eq!(m.params[1], ("post_id", "99"));
3107    }
3108
3109    #[test]
3110    fn mount_preserves_methods() {
3111        let mut child = Router::new();
3112        child.add(route(Method::Get, "/resource")).unwrap();
3113        child.add(route(Method::Post, "/resource")).unwrap();
3114        child.add(route(Method::Delete, "/resource")).unwrap();
3115
3116        let parent = Router::new().mount("/api", child).unwrap();
3117
3118        // All methods should work
3119        let m = parent.match_path("/api/resource", Method::Get).unwrap();
3120        assert_eq!(m.route.method, Method::Get);
3121
3122        let m = parent.match_path("/api/resource", Method::Post).unwrap();
3123        assert_eq!(m.route.method, Method::Post);
3124
3125        let m = parent.match_path("/api/resource", Method::Delete).unwrap();
3126        assert_eq!(m.route.method, Method::Delete);
3127    }
3128
3129    #[test]
3130    fn mount_root_route() {
3131        let mut child = Router::new();
3132        child.add(route(Method::Get, "/")).unwrap();
3133
3134        let parent = Router::new().mount("/api", child).unwrap();
3135
3136        // Root of child is at prefix
3137        let m = parent.match_path("/api", Method::Get).unwrap();
3138        assert_eq!(m.route.path, "/api");
3139    }
3140
3141    #[test]
3142    fn mount_trailing_slash_prefix() {
3143        let mut child = Router::new();
3144        child.add(route(Method::Get, "/users")).unwrap();
3145
3146        // Trailing slash should be normalized
3147        let parent = Router::new().mount("/api/", child).unwrap();
3148
3149        let m = parent.match_path("/api/users", Method::Get).unwrap();
3150        assert_eq!(m.route.path, "/api/users");
3151    }
3152
3153    #[test]
3154    fn mount_empty_prefix() {
3155        let mut child = Router::new();
3156        child.add(route(Method::Get, "/users")).unwrap();
3157
3158        let parent = Router::new().mount("", child).unwrap();
3159
3160        let m = parent.match_path("/users", Method::Get).unwrap();
3161        assert_eq!(m.route.path, "/users");
3162    }
3163
3164    #[test]
3165    fn mount_nested() {
3166        // Build innermost router
3167        let mut inner = Router::new();
3168        inner.add(route(Method::Get, "/items")).unwrap();
3169
3170        // Mount inner into middle
3171        let middle = Router::new().mount("/v1", inner).unwrap();
3172
3173        // Mount middle into outer
3174        let outer = Router::new().mount("/api", middle).unwrap();
3175
3176        // Nested path should work
3177        let m = outer.match_path("/api/v1/items", Method::Get).unwrap();
3178        assert_eq!(m.route.path, "/api/v1/items");
3179    }
3180
3181    #[test]
3182    fn mount_conflict_detection() {
3183        let mut child1 = Router::new();
3184        child1.add(route(Method::Get, "/users")).unwrap();
3185
3186        let mut child2 = Router::new();
3187        child2.add(route(Method::Get, "/users")).unwrap();
3188
3189        let parent = Router::new().mount("/api", child1).unwrap();
3190
3191        // Mounting another router with conflicting routes should fail
3192        let result = parent.mount("/api", child2);
3193        assert!(matches!(result, Err(RouteAddError::Conflict(_))));
3194    }
3195
3196    #[test]
3197    fn mount_no_conflict_different_prefixes() {
3198        let mut child1 = Router::new();
3199        child1.add(route(Method::Get, "/users")).unwrap();
3200
3201        let mut child2 = Router::new();
3202        child2.add(route(Method::Get, "/users")).unwrap();
3203
3204        let parent = Router::new()
3205            .mount("/api/v1", child1)
3206            .unwrap()
3207            .mount("/api/v2", child2)
3208            .unwrap();
3209
3210        // Different prefixes don't conflict
3211        let m = parent.match_path("/api/v1/users", Method::Get).unwrap();
3212        assert_eq!(m.route.path, "/api/v1/users");
3213
3214        let m = parent.match_path("/api/v2/users", Method::Get).unwrap();
3215        assert_eq!(m.route.path, "/api/v2/users");
3216    }
3217
3218    #[test]
3219    #[should_panic(expected = "route conflict when nesting router")]
3220    fn nest_panics_on_conflict() {
3221        let mut child1 = Router::new();
3222        child1.add(route(Method::Get, "/users")).unwrap();
3223
3224        let mut child2 = Router::new();
3225        child2.add(route(Method::Get, "/users")).unwrap();
3226
3227        let parent = Router::new().nest("/api", child1);
3228
3229        // nest() should panic on conflict
3230        let _ = parent.nest("/api", child2);
3231    }
3232
3233    #[test]
3234    fn mount_with_wildcard() {
3235        let mut child = Router::new();
3236        child.add(route(Method::Get, "/files/{*path}")).unwrap();
3237
3238        let parent = Router::new().mount("/static", child).unwrap();
3239
3240        // Wildcard works with prefix
3241        let m = parent
3242            .match_path("/static/files/css/style.css", Method::Get)
3243            .unwrap();
3244        assert_eq!(m.route.path, "/static/files/{*path}");
3245        assert_eq!(m.params[0], ("path", "css/style.css"));
3246    }
3247
3248    #[test]
3249    fn mount_parent_and_child_routes() {
3250        let mut parent = Router::new();
3251        parent.add(route(Method::Get, "/health")).unwrap();
3252
3253        let mut child = Router::new();
3254        child.add(route(Method::Get, "/users")).unwrap();
3255
3256        let app = parent.mount("/api", child).unwrap();
3257
3258        // Both parent and child routes accessible
3259        let m = app.match_path("/health", Method::Get).unwrap();
3260        assert_eq!(m.route.path, "/health");
3261
3262        let m = app.match_path("/api/users", Method::Get).unwrap();
3263        assert_eq!(m.route.path, "/api/users");
3264    }
3265
3266    // =========================================================================
3267    // COMPREHENSIVE EDGE CASE TESTS (bd-1osd)
3268    // =========================================================================
3269    //
3270    // These tests cover edge cases that were previously missing:
3271    // - Percent-encoding in paths
3272    // - Trailing slash handling variations
3273    // - Empty segment edge cases
3274    // - Very deep nesting (stress tests)
3275    // - Many sibling routes (stress tests)
3276    // =========================================================================
3277
3278    // -------------------------------------------------------------------------
3279    // PERCENT-ENCODING TESTS
3280    // -------------------------------------------------------------------------
3281
3282    #[test]
3283    fn percent_encoded_space_in_static_path() {
3284        let mut router = Router::new();
3285        router.add(route(Method::Get, "/hello%20world")).unwrap();
3286
3287        // Exact match with encoded space
3288        let m = router.match_path("/hello%20world", Method::Get);
3289        assert!(m.is_some());
3290        assert_eq!(m.unwrap().route.path, "/hello%20world");
3291
3292        // Unencoded space should NOT match (different path)
3293        let m = router.match_path("/hello world", Method::Get);
3294        assert!(m.is_none());
3295    }
3296
3297    #[test]
3298    fn percent_encoded_slash_in_param() {
3299        let mut router = Router::new();
3300        router.add(route(Method::Get, "/files/{name}")).unwrap();
3301
3302        // Percent-encoded slash stays as single segment
3303        let m = router.match_path("/files/a%2Fb.txt", Method::Get);
3304        assert!(m.is_some());
3305        assert_eq!(m.unwrap().params[0], ("name", "a%2Fb.txt"));
3306    }
3307
3308    #[test]
3309    fn percent_encoded_special_chars_in_param() {
3310        let mut router = Router::new();
3311        router.add(route(Method::Get, "/search/{query}")).unwrap();
3312
3313        // Various percent-encoded characters
3314        let test_cases = vec![
3315            ("/search/hello%20world", ("query", "hello%20world")),
3316            ("/search/foo%26bar", ("query", "foo%26bar")), // &
3317            ("/search/a%3Db", ("query", "a%3Db")),         // =
3318            ("/search/%23hash", ("query", "%23hash")),     // #
3319            ("/search/100%25", ("query", "100%25")),       // %
3320        ];
3321
3322        for (path, expected) in test_cases {
3323            let m = router.match_path(path, Method::Get);
3324            assert!(m.is_some(), "Failed to match: {}", path);
3325            assert_eq!(m.unwrap().params[0], expected);
3326        }
3327    }
3328
3329    #[test]
3330    fn percent_encoded_unicode_in_param() {
3331        let mut router = Router::new();
3332        router.add(route(Method::Get, "/users/{name}")).unwrap();
3333
3334        // URL-encoded UTF-8: 日本 = E6 97 A5 E6 9C AC
3335        let m = router.match_path("/users/%E6%97%A5%E6%9C%AC", Method::Get);
3336        assert!(m.is_some());
3337        assert_eq!(m.unwrap().params[0], ("name", "%E6%97%A5%E6%9C%AC"));
3338    }
3339
3340    #[test]
3341    fn percent_encoded_in_wildcard() {
3342        let mut router = Router::new();
3343        router.add(route(Method::Get, "/files/{*path}")).unwrap();
3344
3345        // Encoded characters preserved in wildcard capture
3346        let m = router.match_path("/files/dir%20name/file%20name.txt", Method::Get);
3347        assert!(m.is_some());
3348        assert_eq!(m.unwrap().params[0], ("path", "dir%20name/file%20name.txt"));
3349    }
3350
3351    #[test]
3352    fn double_percent_encoding() {
3353        let mut router = Router::new();
3354        router.add(route(Method::Get, "/data/{value}")).unwrap();
3355
3356        // Double-encoded percent sign remains encoded; the router does not percent-decode the path.
3357        let m = router.match_path("/data/%2520", Method::Get);
3358        assert!(m.is_some());
3359        assert_eq!(m.unwrap().params[0], ("value", "%2520"));
3360    }
3361
3362    // -------------------------------------------------------------------------
3363    // TRAILING SLASH COMPREHENSIVE TESTS
3364    // -------------------------------------------------------------------------
3365
3366    #[test]
3367    fn trailing_slash_strict_mode_static() {
3368        let mut router = Router::new();
3369        router.add(route(Method::Get, "/users")).unwrap();
3370        router.add(route(Method::Get, "/items/")).unwrap();
3371
3372        // Without trailing slash matches /users
3373        let m = router.match_path("/users", Method::Get);
3374        assert!(m.is_some());
3375        assert_eq!(m.unwrap().route.path, "/users");
3376
3377        // With trailing slash does NOT match /users (strict)
3378        // Note: Current implementation filters empty segments, so /users/ = /users
3379        // This test documents actual behavior
3380        let m = router.match_path("/users/", Method::Get);
3381        if let Some(m) = m {
3382            // If it matches, verify which path matched
3383            assert!(m.route.path == "/users" || m.route.path == "/users/");
3384        }
3385
3386        // /items/ registered with trailing slash
3387        let m = router.match_path("/items/", Method::Get);
3388        assert!(m.is_some());
3389    }
3390
3391    #[test]
3392    fn trailing_slash_on_param_routes() {
3393        let mut router = Router::new();
3394        router.add(route(Method::Get, "/users/{id}")).unwrap();
3395
3396        // Without trailing slash
3397        let m = router.match_path("/users/123", Method::Get);
3398        assert!(m.is_some());
3399        assert_eq!(m.unwrap().params[0], ("id", "123"));
3400
3401        // With trailing slash - behavior depends on implementation
3402        let m = router.match_path("/users/123/", Method::Get);
3403        // Document actual behavior
3404        if let Some(m) = m {
3405            assert_eq!(m.params[0].0, "id");
3406        }
3407    }
3408
3409    #[test]
3410    fn trailing_slash_on_nested_routes() {
3411        let mut router = Router::new();
3412        router.add(route(Method::Get, "/api/v1/users")).unwrap();
3413
3414        // The router treats /path and /path/ as conflicting routes because
3415        // empty segments are filtered out during parsing, making them
3416        // structurally equivalent. This is the intended behavior.
3417        let result = router.add(route(Method::Get, "/api/v1/users/"));
3418        assert!(
3419            matches!(result, Err(RouteAddError::Conflict(_))),
3420            "Routes with and without trailing slash should conflict"
3421        );
3422
3423        // Only one route was registered
3424        assert_eq!(router.routes().len(), 1);
3425    }
3426
3427    #[test]
3428    fn multiple_trailing_slashes() {
3429        let mut router = Router::new();
3430        router.add(route(Method::Get, "/data")).unwrap();
3431
3432        // Multiple trailing slashes should be normalized
3433        let m = router.match_path("/data//", Method::Get);
3434        assert!(m.is_some()); // Empty segments filtered
3435
3436        let m = router.match_path("/data///", Method::Get);
3437        assert!(m.is_some()); // Empty segments filtered
3438    }
3439
3440    // -------------------------------------------------------------------------
3441    // EMPTY SEGMENT EDGE CASES
3442    // -------------------------------------------------------------------------
3443
3444    #[test]
3445    fn empty_segment_normalization() {
3446        let mut router = Router::new();
3447        router.add(route(Method::Get, "/a/b/c")).unwrap();
3448
3449        // Various empty segment patterns that should normalize to /a/b/c
3450        let paths = vec!["/a//b/c", "/a/b//c", "//a/b/c", "/a/b/c//", "//a//b//c//"];
3451
3452        for path in paths {
3453            let m = router.match_path(path, Method::Get);
3454            assert!(m.is_some(), "Failed to match normalized path: {}", path);
3455            assert_eq!(m.unwrap().route.path, "/a/b/c");
3456        }
3457    }
3458
3459    #[test]
3460    fn empty_segment_in_middle_of_params() {
3461        let mut router = Router::new();
3462        router.add(route(Method::Get, "/a/{x}/b/{y}")).unwrap();
3463
3464        // Empty segments should be filtered before param matching
3465        let m = router.match_path("/a//1/b/2", Method::Get);
3466        // After filtering empty segments: /a/1/b/2
3467        // But /a/1/b/2 doesn't match /a/{x}/b/{y} because structure differs
3468        // This test documents actual behavior
3469        if let Some(m) = m {
3470            assert!(!m.params.is_empty());
3471        }
3472    }
3473
3474    #[test]
3475    fn only_slashes_path() {
3476        let mut router = Router::new();
3477        router.add(route(Method::Get, "/")).unwrap();
3478
3479        // Path with only slashes should match root
3480        let paths = vec!["/", "//", "///", "////"];
3481        for path in paths {
3482            let m = router.match_path(path, Method::Get);
3483            assert!(m.is_some(), "Failed to match root with: {}", path);
3484        }
3485    }
3486
3487    #[test]
3488    fn empty_path_handling() {
3489        let mut router = Router::new();
3490        router.add(route(Method::Get, "/")).unwrap();
3491
3492        // Empty string path
3493        let m = router.match_path("", Method::Get);
3494        // Behavior: empty path may or may not match root
3495        // Document actual behavior rather than assert specific outcome
3496        let _matched = m.is_some();
3497    }
3498
3499    // -------------------------------------------------------------------------
3500    // VERY DEEP NESTING STRESS TESTS
3501    // -------------------------------------------------------------------------
3502
3503    #[test]
3504    fn deep_nesting_50_levels() {
3505        let mut router = Router::new();
3506
3507        // Create a 50-level deep path
3508        let path = format!(
3509            "/{}",
3510            (0..50)
3511                .map(|i| format!("l{}", i))
3512                .collect::<Vec<_>>()
3513                .join("/")
3514        );
3515        router.add(route(Method::Get, &path)).unwrap();
3516
3517        // Should match exactly
3518        let m = router.match_path(&path, Method::Get);
3519        assert!(m.is_some());
3520        assert_eq!(m.unwrap().route.path, path);
3521    }
3522
3523    #[test]
3524    fn deep_nesting_100_levels() {
3525        let mut router = Router::new();
3526
3527        // Create a 100-level deep path
3528        let path = format!(
3529            "/{}",
3530            (0..100)
3531                .map(|i| format!("d{}", i))
3532                .collect::<Vec<_>>()
3533                .join("/")
3534        );
3535        router.add(route(Method::Get, &path)).unwrap();
3536
3537        let m = router.match_path(&path, Method::Get);
3538        assert!(m.is_some());
3539        assert_eq!(m.unwrap().route.path, path);
3540    }
3541
3542    #[test]
3543    fn deep_nesting_with_params_at_various_depths() {
3544        let mut router = Router::new();
3545
3546        // 20 levels with params at positions 5, 10, 15
3547        let mut segments = vec![];
3548        for i in 0..20 {
3549            if i == 5 || i == 10 || i == 15 {
3550                segments.push(format!("{{p{}}}", i));
3551            } else {
3552                segments.push(format!("s{}", i));
3553            }
3554        }
3555        let path = format!("/{}", segments.join("/"));
3556        router.add(route(Method::Get, &path)).unwrap();
3557
3558        // Build matching request path
3559        let mut request_segments = vec![];
3560        for i in 0..20 {
3561            if i == 5 || i == 10 || i == 15 {
3562                request_segments.push(format!("val{}", i));
3563            } else {
3564                request_segments.push(format!("s{}", i));
3565            }
3566        }
3567        let request_path = format!("/{}", request_segments.join("/"));
3568
3569        let m = router.match_path(&request_path, Method::Get);
3570        assert!(m.is_some());
3571        let m = m.unwrap();
3572        assert_eq!(m.params.len(), 3);
3573        assert_eq!(m.params[0], ("p5", "val5"));
3574        assert_eq!(m.params[1], ("p10", "val10"));
3575        assert_eq!(m.params[2], ("p15", "val15"));
3576    }
3577
3578    #[test]
3579    fn deep_nesting_with_wildcard_at_end() {
3580        let mut router = Router::new();
3581
3582        // 30 static levels then wildcard
3583        let segments: Vec<_> = (0..30).map(|i| format!("x{}", i)).collect();
3584        let prefix = format!("/{}", segments.join("/"));
3585        let path = format!("{}/{{*rest}}", prefix);
3586        router.add(route(Method::Get, &path)).unwrap();
3587
3588        // Match with extra segments after the 30 levels
3589        let request_path = format!("{}/a/b/c/d/e", prefix);
3590        let m = router.match_path(&request_path, Method::Get);
3591        assert!(m.is_some());
3592        assert_eq!(m.unwrap().params[0], ("rest", "a/b/c/d/e"));
3593    }
3594
3595    // -------------------------------------------------------------------------
3596    // MANY SIBLINGS STRESS TESTS
3597    // -------------------------------------------------------------------------
3598
3599    #[test]
3600    fn many_siblings_500_routes() {
3601        let mut router = Router::new();
3602
3603        // Add 500 sibling routes under /api/
3604        for i in 0..500 {
3605            router
3606                .add(route(Method::Get, &format!("/api/endpoint{}", i)))
3607                .unwrap();
3608        }
3609
3610        assert_eq!(router.routes().len(), 500);
3611
3612        // Verify random samples match correctly
3613        for i in [0, 50, 100, 250, 499] {
3614            let path = format!("/api/endpoint{}", i);
3615            let m = router.match_path(&path, Method::Get);
3616            assert!(m.is_some(), "Failed to match: {}", path);
3617            assert_eq!(m.unwrap().route.path, path);
3618        }
3619    }
3620
3621    #[test]
3622    fn many_siblings_with_shared_prefix() {
3623        let mut router = Router::new();
3624
3625        // Routes with increasingly long shared prefixes
3626        for i in 0..200 {
3627            router
3628                .add(route(Method::Get, &format!("/users/user{:04}", i)))
3629                .unwrap();
3630        }
3631
3632        assert_eq!(router.routes().len(), 200);
3633
3634        // All should be matchable
3635        for i in [0, 50, 100, 150, 199] {
3636            let path = format!("/users/user{:04}", i);
3637            let m = router.match_path(&path, Method::Get);
3638            assert!(m.is_some());
3639            assert_eq!(m.unwrap().route.path, path);
3640        }
3641    }
3642
3643    #[test]
3644    fn many_siblings_mixed_static_and_param() {
3645        let mut router = Router::new();
3646
3647        // Add many static routes
3648        for i in 0..100 {
3649            router
3650                .add(route(Method::Get, &format!("/items/item{}", i)))
3651                .unwrap();
3652        }
3653
3654        // Add a param route that shouldn't conflict
3655        router.add(route(Method::Get, "/items/{id}")).unwrap();
3656
3657        assert_eq!(router.routes().len(), 101);
3658
3659        // Static routes should take priority
3660        let m = router.match_path("/items/item50", Method::Get).unwrap();
3661        assert_eq!(m.route.path, "/items/item50");
3662
3663        // Non-matching static should fall to param
3664        let m = router.match_path("/items/other", Method::Get).unwrap();
3665        assert_eq!(m.route.path, "/items/{id}");
3666        assert_eq!(m.params[0], ("id", "other"));
3667    }
3668
3669    #[test]
3670    fn many_siblings_different_methods() {
3671        let mut router = Router::new();
3672
3673        // 50 routes with all methods
3674        let methods = vec![
3675            Method::Get,
3676            Method::Post,
3677            Method::Put,
3678            Method::Delete,
3679            Method::Patch,
3680        ];
3681
3682        for i in 0..50 {
3683            for method in &methods {
3684                router
3685                    .add(Route::new(*method, &format!("/resource{}", i)))
3686                    .unwrap();
3687            }
3688        }
3689
3690        assert_eq!(router.routes().len(), 250);
3691
3692        // Verify method dispatch
3693        let m = router.match_path("/resource25", Method::Get).unwrap();
3694        assert_eq!(m.route.method, Method::Get);
3695
3696        let m = router.match_path("/resource25", Method::Post).unwrap();
3697        assert_eq!(m.route.method, Method::Post);
3698
3699        let m = router.match_path("/resource25", Method::Delete).unwrap();
3700        assert_eq!(m.route.method, Method::Delete);
3701    }
3702
3703    #[test]
3704    fn stress_wide_and_deep() {
3705        let mut router = Router::new();
3706
3707        // Create a tree that's both wide and deep
3708        // 10 top-level branches, each with 10 sub-branches, each with 10 leaves
3709        for a in 0..10 {
3710            for b in 0..10 {
3711                for c in 0..10 {
3712                    let path = format!("/a{}/b{}/c{}", a, b, c);
3713                    router.add(route(Method::Get, &path)).unwrap();
3714                }
3715            }
3716        }
3717
3718        assert_eq!(router.routes().len(), 1000);
3719
3720        // Sample various paths
3721        let m = router.match_path("/a0/b0/c0", Method::Get).unwrap();
3722        assert_eq!(m.route.path, "/a0/b0/c0");
3723
3724        let m = router.match_path("/a5/b5/c5", Method::Get).unwrap();
3725        assert_eq!(m.route.path, "/a5/b5/c5");
3726
3727        let m = router.match_path("/a9/b9/c9", Method::Get).unwrap();
3728        assert_eq!(m.route.path, "/a9/b9/c9");
3729
3730        // Non-existent paths should not match
3731        assert!(router.match_path("/a10/b0/c0", Method::Get).is_none());
3732        assert!(router.match_path("/a0/b10/c0", Method::Get).is_none());
3733    }
3734
3735    // -------------------------------------------------------------------------
3736    // ADDITIONAL UNICODE EDGE CASES
3737    // -------------------------------------------------------------------------
3738
3739    #[test]
3740    fn unicode_emoji_in_path() {
3741        let mut router = Router::new();
3742        router.add(route(Method::Get, "/emoji/🎉")).unwrap();
3743
3744        let m = router.match_path("/emoji/🎉", Method::Get);
3745        assert!(m.is_some());
3746        assert_eq!(m.unwrap().route.path, "/emoji/🎉");
3747    }
3748
3749    #[test]
3750    fn unicode_rtl_characters() {
3751        let mut router = Router::new();
3752        // Arabic "مرحبا" (Hello)
3753        router.add(route(Method::Get, "/greet/مرحبا")).unwrap();
3754
3755        let m = router.match_path("/greet/مرحبا", Method::Get);
3756        assert!(m.is_some());
3757        assert_eq!(m.unwrap().route.path, "/greet/مرحبا");
3758    }
3759
3760    #[test]
3761    fn unicode_mixed_scripts() {
3762        let mut router = Router::new();
3763        // Mixed: Latin + CJK + Cyrillic
3764        router
3765            .add(route(Method::Get, "/mix/hello世界Привет"))
3766            .unwrap();
3767
3768        let m = router.match_path("/mix/hello世界Привет", Method::Get);
3769        assert!(m.is_some());
3770    }
3771
3772    #[test]
3773    fn unicode_normalization_awareness() {
3774        let mut router = Router::new();
3775        // é as single codepoint (U+00E9)
3776        router.add(route(Method::Get, "/café")).unwrap();
3777
3778        // Same visual appearance should match
3779        let m = router.match_path("/café", Method::Get);
3780        assert!(m.is_some());
3781
3782        // Note: decomposed é (e + combining acute U+0301) might not match
3783        // This test documents that the router uses byte-level comparison
3784    }
3785
3786    #[test]
3787    fn unicode_in_param_with_converter() {
3788        let mut router = Router::new();
3789        router.add(route(Method::Get, "/data/{value:str}")).unwrap();
3790
3791        // Unicode should work with str converter
3792        let m = router.match_path("/data/日本語テスト", Method::Get);
3793        assert!(m.is_some());
3794        assert_eq!(m.unwrap().params[0], ("value", "日本語テスト"));
3795    }
3796
3797    // -------------------------------------------------------------------------
3798    // EDGE CASES FOR CONVERTERS
3799    // -------------------------------------------------------------------------
3800
3801    #[test]
3802    fn int_converter_overflow() {
3803        let mut router = Router::new();
3804        router.add(route(Method::Get, "/id/{num:int}")).unwrap();
3805
3806        // Value exceeding i64 max should not match
3807        let overflow = "99999999999999999999999999999";
3808        let path = format!("/id/{}", overflow);
3809        let m = router.match_path(&path, Method::Get);
3810        assert!(m.is_none());
3811    }
3812
3813    #[test]
3814    fn float_converter_very_small() {
3815        let mut router = Router::new();
3816        router.add(route(Method::Get, "/val/{v:float}")).unwrap();
3817
3818        // Very small float
3819        let m = router.match_path("/val/1e-308", Method::Get);
3820        assert!(m.is_some());
3821    }
3822
3823    #[test]
3824    fn float_converter_very_large() {
3825        let mut router = Router::new();
3826        router.add(route(Method::Get, "/val/{v:float}")).unwrap();
3827
3828        // Very large float
3829        let m = router.match_path("/val/1e308", Method::Get);
3830        assert!(m.is_some());
3831    }
3832
3833    #[test]
3834    fn uuid_converter_nil_uuid() {
3835        let mut router = Router::new();
3836        router.add(route(Method::Get, "/obj/{id:uuid}")).unwrap();
3837
3838        // Nil UUID (all zeros)
3839        let m = router.match_path("/obj/00000000-0000-0000-0000-000000000000", Method::Get);
3840        assert!(m.is_some());
3841    }
3842
3843    #[test]
3844    fn uuid_converter_max_uuid() {
3845        let mut router = Router::new();
3846        router.add(route(Method::Get, "/obj/{id:uuid}")).unwrap();
3847
3848        // Max UUID (all f's)
3849        let m = router.match_path("/obj/ffffffff-ffff-ffff-ffff-ffffffffffff", Method::Get);
3850        assert!(m.is_some());
3851    }
3852
3853    // -------------------------------------------------------------------------
3854    // SPECIAL PATH PATTERNS
3855    // -------------------------------------------------------------------------
3856
3857    #[test]
3858    fn path_with_dots() {
3859        let mut router = Router::new();
3860        router.add(route(Method::Get, "/files/{name}")).unwrap();
3861
3862        // Multiple dots
3863        let m = router.match_path("/files/file.name.ext", Method::Get);
3864        assert!(m.is_some());
3865        assert_eq!(m.unwrap().params[0], ("name", "file.name.ext"));
3866    }
3867
3868    #[test]
3869    fn path_with_only_special_chars() {
3870        let mut router = Router::new();
3871        router.add(route(Method::Get, "/data/{val}")).unwrap();
3872
3873        // Param value is only special chars (but not slash)
3874        let m = router.match_path("/data/-._~", Method::Get);
3875        assert!(m.is_some());
3876        assert_eq!(m.unwrap().params[0], ("val", "-._~"));
3877    }
3878
3879    #[test]
3880    fn path_segment_with_colon() {
3881        let mut router = Router::new();
3882        router.add(route(Method::Get, "/time/{val}")).unwrap();
3883
3884        // Value containing colon (common in time formats)
3885        let m = router.match_path("/time/12:30:45", Method::Get);
3886        assert!(m.is_some());
3887        assert_eq!(m.unwrap().params[0], ("val", "12:30:45"));
3888    }
3889
3890    #[test]
3891    fn path_segment_with_at_sign() {
3892        let mut router = Router::new();
3893        router.add(route(Method::Get, "/user/{handle}")).unwrap();
3894
3895        // Value containing @ (common in handles)
3896        let m = router.match_path("/user/@username", Method::Get);
3897        assert!(m.is_some());
3898        assert_eq!(m.unwrap().params[0], ("handle", "@username"));
3899    }
3900
3901    #[test]
3902    fn very_long_segment() {
3903        let mut router = Router::new();
3904        router.add(route(Method::Get, "/data/{val}")).unwrap();
3905
3906        // Very long segment (1000 chars)
3907        let long_val: String = (0..1000).map(|_| 'x').collect();
3908        let path = format!("/data/{}", long_val);
3909
3910        let m = router.match_path(&path, Method::Get);
3911        assert!(m.is_some());
3912        assert_eq!(m.unwrap().params[0].1.len(), 1000);
3913    }
3914
3915    #[test]
3916    fn very_long_path_total() {
3917        let mut router = Router::new();
3918
3919        // Path with many short segments totaling > 4KB
3920        let segments: Vec<_> = (0..500).map(|i| format!("s{}", i)).collect();
3921        let path = format!("/{}", segments.join("/"));
3922        router.add(route(Method::Get, &path)).unwrap();
3923
3924        let m = router.match_path(&path, Method::Get);
3925        assert!(m.is_some());
3926    }
3927}