1use crate::r#match::{AllowedMethods, RouteLookup, RouteMatch};
47use fastapi_types::Method;
48use std::collections::HashMap;
49use std::fmt;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
53pub enum Converter {
54 #[default]
56 Str,
57 Int,
59 Float,
61 Uuid,
63 Path,
67}
68
69#[derive(Debug, Clone, PartialEq)]
71pub enum ParamValue {
72 Str(String),
74 Int(i64),
76 Float(f64),
78 Uuid(String),
80 Path(String),
82}
83
84impl ParamValue {
85 #[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 ""
94 }
95 }
96 }
97
98 #[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 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq)]
128pub enum ConversionError {
129 InvalidInt {
131 value: String,
133 param: String,
135 },
136 InvalidFloat {
138 value: String,
140 param: String,
142 },
143 InvalidUuid {
145 value: String,
147 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 #[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 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 #[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
239fn is_uuid(s: &str) -> bool {
243 if s.len() != 36 {
245 return false;
246 }
247
248 let bytes = s.as_bytes();
249
250 if bytes[8] != b'-' || bytes[13] != b'-' || bytes[18] != b'-' || bytes[23] != b'-' {
252 return false;
253 }
254
255 bytes.iter().enumerate().all(|(i, &b)| {
257 if i == 8 || i == 13 || i == 18 || i == 23 {
258 true } else {
260 b.is_ascii_hexdigit()
261 }
262 })
263}
264
265#[derive(Debug, Clone, Default)]
267pub struct ParamInfo {
268 pub name: String,
270 pub converter: Converter,
272 pub title: Option<String>,
274 pub description: Option<String>,
276 pub deprecated: bool,
278 pub example: Option<serde_json::Value>,
280 pub examples: Vec<(String, serde_json::Value)>,
282}
283
284impl ParamInfo {
285 #[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 #[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 #[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 #[must_use]
315 pub fn deprecated(mut self) -> Self {
316 self.deprecated = true;
317 self
318 }
319
320 #[must_use]
322 pub fn with_example(mut self, example: serde_json::Value) -> Self {
323 self.example = Some(example);
324 self
325 }
326
327 #[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#[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 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
392fn 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#[derive(Debug, Clone)]
420pub struct RouteResponse {
421 pub status: u16,
423 pub schema_name: String,
425 pub description: String,
427 pub content_type: String,
429}
430
431impl RouteResponse {
432 #[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 #[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#[derive(Debug, Clone, Default)]
459pub struct RouteSecurityRequirement {
460 pub scheme: String,
462 pub scopes: Vec<String>,
464}
465
466impl RouteSecurityRequirement {
467 #[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 #[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#[derive(Clone)]
504pub struct Route {
505 pub path: String,
507 pub method: Method,
509 pub operation_id: String,
511 pub summary: Option<String>,
513 pub description: Option<String>,
515 pub tags: Vec<String>,
517 pub deprecated: bool,
519 pub path_params: Vec<ParamInfo>,
521 pub request_body_schema: Option<String>,
523 pub request_body_content_type: Option<String>,
525 pub request_body_required: bool,
527 pub security: Vec<RouteSecurityRequirement>,
532 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#[derive(Debug, Clone)]
580pub struct RouteConflictError {
581 pub method: Method,
583 pub new_path: String,
585 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#[derive(Debug, Clone)]
603pub struct InvalidRouteError {
604 pub path: String,
606 pub message: String,
608}
609
610impl InvalidRouteError {
611 #[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#[derive(Debug, Clone)]
631pub enum RouteAddError {
632 Conflict(RouteConflictError),
634 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 pub fn new(method: Method, path: impl Into<String>) -> Self {
685 let path = path.into();
686 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 #[must_use]
715 pub fn with_placeholder_handler(method: Method, path: impl Into<String>) -> Self {
716 Self::new(method, path)
717 }
718
719 #[must_use]
721 pub fn summary(mut self, summary: impl Into<String>) -> Self {
722 self.summary = Some(summary.into());
723 self
724 }
725
726 #[must_use]
728 pub fn description(mut self, description: impl Into<String>) -> Self {
729 self.description = Some(description.into());
730 self
731 }
732
733 #[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 #[must_use]
742 pub fn tag(mut self, tag: impl Into<String>) -> Self {
743 self.tags.push(tag.into());
744 self
745 }
746
747 #[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 #[must_use]
756 pub fn deprecated(mut self) -> Self {
757 self.deprecated = true;
758 self
759 }
760
761 #[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 #[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 #[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 #[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 #[must_use]
863 pub fn has_responses(&self) -> bool {
864 !self.responses.is_empty()
865 }
866
867 #[must_use]
869 pub fn has_request_body(&self) -> bool {
870 self.request_body_schema.is_some()
871 }
872
873 #[must_use]
875 pub fn has_path_params(&self) -> bool {
876 !self.path_params.is_empty()
877 }
878
879 #[must_use]
881 pub fn has_security(&self) -> bool {
882 !self.security.is_empty()
883 }
884}
885
886struct 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
915pub struct Router {
917 root: Node,
918 routes: Vec<Route>,
919}
920
921impl Router {
922 #[must_use]
924 pub fn new() -> Self {
925 Self {
926 root: Node::new(""),
927 routes: Vec::new(),
928 }
929 }
930
931 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 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 #[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 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 #[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 #[must_use]
1023 pub fn routes(&self) -> &[Route] {
1024 &self.routes
1025 }
1026
1027 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 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 #[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 let mut range_iter = SegmentRangeIter::new(path);
1136
1137 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 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 if let Some(child) = node.find_static(segment) {
1175 node = child;
1176 continue;
1177 }
1178
1179 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 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 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
1268struct 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 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 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 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 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 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 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 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 let m = router.match_path("/items/-456", Method::Get);
1463 assert!(m.is_some());
1464
1465 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 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 let m = router.match_path("/values/42", Method::Get);
1484 assert!(m.is_some());
1485
1486 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 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 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 let m = router.match_path("/items", Method::Get);
1541 assert!(m.is_some());
1542 assert_eq!(m.unwrap().route.method, Method::Get);
1543
1544 let m = router.match_path("/items", Method::Post);
1546 assert!(m.is_some());
1547 assert_eq!(m.unwrap().route.method, Method::Post);
1548
1549 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 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 router.add(route(Method::Get, "/users/me")).unwrap();
1617 router.add(route(Method::Get, "/users/{id}")).unwrap();
1618
1619 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 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 #[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 #[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 #[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 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 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 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 #[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 let m = router.match_path("/users/", Method::Get);
2003 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 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 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 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 let m = router.match_path("/users//posts", Method::Get);
2068 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 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 #[test]
2108 fn head_matches_get_route() {
2109 let mut router = Router::new();
2110 router.add(route(Method::Get, "/users")).unwrap();
2111
2112 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 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 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 #[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 let m = router.match_path("/items/0", Method::Get);
2157 assert!(m.is_some());
2158
2159 let m = router.match_path("/items/9223372036854775807", Method::Get);
2161 assert!(m.is_some());
2162
2163 let m = router.match_path("/items/-9223372036854775808", Method::Get);
2165 assert!(m.is_some());
2166
2167 let m = router.match_path("/items/007", Method::Get);
2169 assert!(m.is_some());
2170
2171 let m = router.match_path("/items/+123", Method::Get);
2173 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 let m = router.match_path("/values/1e10", Method::Get);
2186 assert!(m.is_some());
2187
2188 let m = router.match_path("/values/1e-10", Method::Get);
2190 assert!(m.is_some());
2191
2192 let m = router.match_path("/values/inf", Method::Get);
2194 assert!(m.is_some());
2195
2196 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 let m = router.match_path("/objects/550e8400-e29b-41d4-a716-446655440000", Method::Get);
2210 assert!(m.is_some());
2211
2212 let m = router.match_path("/objects/550E8400-E29B-41D4-A716-446655440000", Method::Get);
2214 assert!(m.is_some());
2215
2216 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 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 assert!(
2245 router
2246 .match_path("/objects/550e8400e29b41d4a716446655440000", Method::Get)
2247 .is_none()
2248 );
2249
2250 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 #[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 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 assert_eq!(*converter, Converter::Str);
2307 }
2308 _ => panic!("Expected param segment"),
2309 }
2310 }
2311
2312 #[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 )); }
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 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 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 #[test]
2414 fn allowed_methods_deduplication() {
2415 let allowed = AllowedMethods::new(vec![Method::Get, Method::Get, Method::Post]);
2417 assert_eq!(allowed.methods().len(), 3); }
2419
2420 #[test]
2421 fn allowed_methods_sorting() {
2422 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); 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 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 #[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 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 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 router.add(route(Method::Get, "/api/users")).unwrap();
2537 router.add(route(Method::Get, "/api/posts")).unwrap();
2538 router.add(route(Method::Get, "/{*route}")).unwrap();
2540
2541 let m = router.match_path("/api/users", Method::Get).unwrap();
2543 assert_eq!(m.route.path, "/api/users");
2544
2545 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 router.add(route(Method::Get, "/users/me")).unwrap();
2578 router.add(route(Method::Get, "/users/{id}")).unwrap();
2580 router.add(route(Method::Get, "/{*path}")).unwrap();
2582
2583 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 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 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 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 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 #[test]
2661 fn priority_static_before_param() {
2662 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 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 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 let mut router = Router::new();
2683 router.add(route(Method::Get, "/files/{name}")).unwrap();
2684
2685 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 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 let m = router.match_path("/files/foo.txt", Method::Get).unwrap();
2702 assert_eq!(m.route.path, "/files/{name}");
2703
2704 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 let mut router = Router::new();
2715 router.add(route(Method::Get, "/{*path}")).unwrap();
2716 router.add(route(Method::Get, "/api/users")).unwrap();
2717
2718 let m = router.match_path("/api/users", Method::Get).unwrap();
2720 assert_eq!(m.route.path, "/api/users");
2721
2722 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 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 let m = router.match_path("/api/v1/users", Method::Get).unwrap();
2739 assert_eq!(m.route.path, "/api/v1/users");
2740
2741 let m = router.match_path("/api/v1/items", Method::Get).unwrap();
2743 assert_eq!(m.route.path, "/api/v1/{resource}");
2744
2745 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 let mut router = Router::new();
2756
2757 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 let m = router.match_path("/users/me", Method::Get).unwrap();
2767 assert_eq!(m.route.path, "/users/me");
2768
2769 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 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 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 #[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 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 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 #[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 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 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 assert_eq!(m.get_param("id"), Some("42"));
2980 assert_eq!(m.get_param("val"), Some("99.99"));
2981
2982 assert_eq!(m.get_param_int("id"), Some(Ok(42)));
2984 assert_eq!(m.get_param_float("val"), Some(Ok(99.99)));
2985
2986 assert!(m.get_param_int("missing").is_none());
2988
2989 let result = m.get_param_int("val");
2991 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 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 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 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 #[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 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 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 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 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 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 let mut inner = Router::new();
3168 inner.add(route(Method::Get, "/items")).unwrap();
3169
3170 let middle = Router::new().mount("/v1", inner).unwrap();
3172
3173 let outer = Router::new().mount("/api", middle).unwrap();
3175
3176 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 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 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 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 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 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 #[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 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 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 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 let test_cases = vec![
3315 ("/search/hello%20world", ("query", "hello%20world")),
3316 ("/search/foo%26bar", ("query", "foo%26bar")), ("/search/a%3Db", ("query", "a%3Db")), ("/search/%23hash", ("query", "%23hash")), ("/search/100%25", ("query", "100%25")), ];
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 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 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 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 #[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 let m = router.match_path("/users", Method::Get);
3374 assert!(m.is_some());
3375 assert_eq!(m.unwrap().route.path, "/users");
3376
3377 let m = router.match_path("/users/", Method::Get);
3381 if let Some(m) = m {
3382 assert!(m.route.path == "/users" || m.route.path == "/users/");
3384 }
3385
3386 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 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 let m = router.match_path("/users/123/", Method::Get);
3403 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 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 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 let m = router.match_path("/data//", Method::Get);
3434 assert!(m.is_some()); let m = router.match_path("/data///", Method::Get);
3437 assert!(m.is_some()); }
3439
3440 #[test]
3445 fn empty_segment_normalization() {
3446 let mut router = Router::new();
3447 router.add(route(Method::Get, "/a/b/c")).unwrap();
3448
3449 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 let m = router.match_path("/a//1/b/2", Method::Get);
3466 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 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 let m = router.match_path("", Method::Get);
3494 let _matched = m.is_some();
3497 }
3498
3499 #[test]
3504 fn deep_nesting_50_levels() {
3505 let mut router = Router::new();
3506
3507 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 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 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 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 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 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 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 #[test]
3600 fn many_siblings_500_routes() {
3601 let mut router = Router::new();
3602
3603 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 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 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 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 for i in 0..100 {
3649 router
3650 .add(route(Method::Get, &format!("/items/item{}", i)))
3651 .unwrap();
3652 }
3653
3654 router.add(route(Method::Get, "/items/{id}")).unwrap();
3656
3657 assert_eq!(router.routes().len(), 101);
3658
3659 let m = router.match_path("/items/item50", Method::Get).unwrap();
3661 assert_eq!(m.route.path, "/items/item50");
3662
3663 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 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 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 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 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 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 #[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 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 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 router.add(route(Method::Get, "/café")).unwrap();
3777
3778 let m = router.match_path("/café", Method::Get);
3780 assert!(m.is_some());
3781
3782 }
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 let m = router.match_path("/data/日本語テスト", Method::Get);
3793 assert!(m.is_some());
3794 assert_eq!(m.unwrap().params[0], ("value", "日本語テスト"));
3795 }
3796
3797 #[test]
3802 fn int_converter_overflow() {
3803 let mut router = Router::new();
3804 router.add(route(Method::Get, "/id/{num:int}")).unwrap();
3805
3806 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 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 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 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 let m = router.match_path("/obj/ffffffff-ffff-ffff-ffff-ffffffffffff", Method::Get);
3850 assert!(m.is_some());
3851 }
3852
3853 #[test]
3858 fn path_with_dots() {
3859 let mut router = Router::new();
3860 router.add(route(Method::Get, "/files/{name}")).unwrap();
3861
3862 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 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 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 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 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 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}