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 if let Some(&idx) = node.routes.get(&Method::Get) {
997 return RouteLookup::Match(RouteMatch {
998 route: &self.routes[idx],
999 params,
1000 });
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 if let Some(ref info) = child.param {
1182 if info.converter == Converter::Path {
1183 let value = &path[start..last_end];
1184 params.push((info.name.as_str(), value));
1185 node = child;
1186 return Some((node, params));
1188 }
1189 if info.converter.matches(segment) {
1190 params.push((info.name.as_str(), segment));
1191 node = child;
1192 continue;
1193 }
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 {
1258 if idx + 1 != segments.len() {
1259 return Err(InvalidRouteError::new(
1260 path,
1261 format!(
1262 "wildcard '{{*{name}}}' or '{{{name}:path}}' must be the final segment"
1263 ),
1264 ));
1265 }
1266 }
1267 }
1268 Ok(())
1269}
1270
1271struct SegmentRangeIter<'a> {
1275 bytes: &'a [u8],
1276 idx: usize,
1277}
1278
1279impl<'a> SegmentRangeIter<'a> {
1280 fn new(path: &'a str) -> Self {
1281 Self {
1282 bytes: path.as_bytes(),
1283 idx: 0,
1284 }
1285 }
1286}
1287
1288impl Iterator for SegmentRangeIter<'_> {
1289 type Item = (usize, usize);
1290
1291 #[inline]
1292 fn next(&mut self) -> Option<Self::Item> {
1293 while self.idx < self.bytes.len() && self.bytes[self.idx] == b'/' {
1295 self.idx += 1;
1296 }
1297 if self.idx >= self.bytes.len() {
1298 return None;
1299 }
1300 let start = self.idx;
1301 while self.idx < self.bytes.len() && self.bytes[self.idx] != b'/' {
1303 self.idx += 1;
1304 }
1305 Some((start, self.idx))
1306 }
1307
1308 fn size_hint(&self) -> (usize, Option<usize>) {
1309 let remaining = self.bytes.len().saturating_sub(self.idx);
1311 (0, Some(remaining / 2 + 1))
1312 }
1313}
1314
1315fn paths_conflict(a: &str, b: &str) -> bool {
1316 let a_segments = parse_path(a);
1317 let b_segments = parse_path(b);
1318
1319 let a_has_path = matches!(
1320 a_segments.last(),
1321 Some(PathSegment::Param {
1322 converter: Converter::Path,
1323 ..
1324 })
1325 );
1326 let b_has_path = matches!(
1327 b_segments.last(),
1328 Some(PathSegment::Param {
1329 converter: Converter::Path,
1330 ..
1331 })
1332 );
1333 let min_len = a_segments.len().min(b_segments.len());
1334 let mut param_mismatch = false;
1335
1336 for (left, right) in a_segments.iter().take(min_len).zip(b_segments.iter()) {
1337 match (left, right) {
1338 (PathSegment::Static(a), PathSegment::Static(b)) => {
1339 if a != b {
1340 return false;
1341 }
1342 }
1343 (PathSegment::Static(_), PathSegment::Param { .. })
1344 | (PathSegment::Param { .. }, PathSegment::Static(_)) => {
1345 return false;
1347 }
1348 (
1349 PathSegment::Param {
1350 name: left_name,
1351 converter: left_conv,
1352 },
1353 PathSegment::Param {
1354 name: right_name,
1355 converter: right_conv,
1356 },
1357 ) => {
1358 if left_name != right_name || left_conv != right_conv {
1359 param_mismatch = true;
1360 }
1361 }
1362 }
1363 }
1364
1365 if a_segments.len() == b_segments.len() {
1366 return true;
1367 }
1368
1369 if param_mismatch {
1370 return true;
1371 }
1372
1373 if a_has_path && a_segments.len() == min_len {
1374 return true;
1375 }
1376
1377 if b_has_path && b_segments.len() == min_len {
1378 return true;
1379 }
1380
1381 false
1382}
1383
1384#[cfg(test)]
1385mod tests {
1386 use super::*;
1387
1388 fn route(method: Method, path: &str) -> Route {
1390 Route::new(method, path)
1391 }
1392
1393 #[test]
1394 fn static_route_match() {
1395 let mut router = Router::new();
1396 router.add(route(Method::Get, "/users")).unwrap();
1397 router.add(route(Method::Get, "/items")).unwrap();
1398
1399 let m = router.match_path("/users", Method::Get);
1400 assert!(m.is_some());
1401 assert_eq!(m.unwrap().route.path, "/users");
1402
1403 let m = router.match_path("/items", Method::Get);
1404 assert!(m.is_some());
1405 assert_eq!(m.unwrap().route.path, "/items");
1406
1407 assert!(router.match_path("/other", Method::Get).is_none());
1409 }
1410
1411 #[test]
1412 fn nested_static_routes() {
1413 let mut router = Router::new();
1414 router.add(route(Method::Get, "/api/v1/users")).unwrap();
1415 router.add(route(Method::Get, "/api/v2/users")).unwrap();
1416
1417 let m = router.match_path("/api/v1/users", Method::Get);
1418 assert!(m.is_some());
1419 assert_eq!(m.unwrap().route.path, "/api/v1/users");
1420
1421 let m = router.match_path("/api/v2/users", Method::Get);
1422 assert!(m.is_some());
1423 assert_eq!(m.unwrap().route.path, "/api/v2/users");
1424 }
1425
1426 #[test]
1427 fn parameter_extraction() {
1428 let mut router = Router::new();
1429 router.add(route(Method::Get, "/users/{user_id}")).unwrap();
1430
1431 let m = router.match_path("/users/123", Method::Get);
1432 assert!(m.is_some());
1433 let m = m.unwrap();
1434 assert_eq!(m.route.path, "/users/{user_id}");
1435 assert_eq!(m.params.len(), 1);
1436 assert_eq!(m.params[0], ("user_id", "123"));
1437 }
1438
1439 #[test]
1440 fn multiple_parameters() {
1441 let mut router = Router::new();
1442 router
1443 .add(route(Method::Get, "/users/{user_id}/posts/{post_id}"))
1444 .unwrap();
1445
1446 let m = router.match_path("/users/42/posts/99", Method::Get);
1447 assert!(m.is_some());
1448 let m = m.unwrap();
1449 assert_eq!(m.params.len(), 2);
1450 assert_eq!(m.params[0], ("user_id", "42"));
1451 assert_eq!(m.params[1], ("post_id", "99"));
1452 }
1453
1454 #[test]
1455 fn int_converter() {
1456 let mut router = Router::new();
1457 router.add(route(Method::Get, "/items/{id:int}")).unwrap();
1458
1459 let m = router.match_path("/items/123", Method::Get);
1461 assert!(m.is_some());
1462 assert_eq!(m.unwrap().params[0], ("id", "123"));
1463
1464 let m = router.match_path("/items/-456", Method::Get);
1466 assert!(m.is_some());
1467
1468 assert!(router.match_path("/items/abc", Method::Get).is_none());
1470 assert!(router.match_path("/items/12.34", Method::Get).is_none());
1471 }
1472
1473 #[test]
1474 fn float_converter() {
1475 let mut router = Router::new();
1476 router
1477 .add(route(Method::Get, "/values/{val:float}"))
1478 .unwrap();
1479
1480 let m = router.match_path("/values/3.14", Method::Get);
1482 assert!(m.is_some());
1483 assert_eq!(m.unwrap().params[0], ("val", "3.14"));
1484
1485 let m = router.match_path("/values/42", Method::Get);
1487 assert!(m.is_some());
1488
1489 assert!(router.match_path("/values/abc", Method::Get).is_none());
1491 }
1492
1493 #[test]
1494 fn uuid_converter() {
1495 let mut router = Router::new();
1496 router
1497 .add(route(Method::Get, "/objects/{id:uuid}"))
1498 .unwrap();
1499
1500 let m = router.match_path("/objects/550e8400-e29b-41d4-a716-446655440000", Method::Get);
1502 assert!(m.is_some());
1503 assert_eq!(
1504 m.unwrap().params[0],
1505 ("id", "550e8400-e29b-41d4-a716-446655440000")
1506 );
1507
1508 assert!(
1510 router
1511 .match_path("/objects/not-a-uuid", Method::Get)
1512 .is_none()
1513 );
1514 assert!(router.match_path("/objects/123", Method::Get).is_none());
1515 }
1516
1517 #[test]
1518 fn path_converter_captures_slashes() {
1519 let mut router = Router::new();
1520 router
1521 .add(route(Method::Get, "/files/{path:path}"))
1522 .unwrap();
1523
1524 let m = router.match_path("/files/a/b/c.txt", Method::Get).unwrap();
1525 assert_eq!(m.params[0], ("path", "a/b/c.txt"));
1526 }
1527
1528 #[test]
1529 fn path_converter_must_be_terminal() {
1530 let mut router = Router::new();
1531 let result = router.add(route(Method::Get, "/files/{path:path}/edit"));
1532 assert!(matches!(result, Err(RouteAddError::InvalidPath(_))));
1533 }
1534
1535 #[test]
1536 fn method_dispatch() {
1537 let mut router = Router::new();
1538 router.add(route(Method::Get, "/items")).unwrap();
1539 router.add(route(Method::Post, "/items")).unwrap();
1540 router.add(route(Method::Delete, "/items/{id}")).unwrap();
1541
1542 let m = router.match_path("/items", Method::Get);
1544 assert!(m.is_some());
1545 assert_eq!(m.unwrap().route.method, Method::Get);
1546
1547 let m = router.match_path("/items", Method::Post);
1549 assert!(m.is_some());
1550 assert_eq!(m.unwrap().route.method, Method::Post);
1551
1552 let m = router.match_path("/items/123", Method::Delete);
1554 assert!(m.is_some());
1555 assert_eq!(m.unwrap().route.method, Method::Delete);
1556
1557 assert!(router.match_path("/items", Method::Put).is_none());
1559 }
1560
1561 #[test]
1562 fn lookup_method_not_allowed_includes_head() {
1563 let mut router = Router::new();
1564 router.add(route(Method::Get, "/users")).unwrap();
1565
1566 let result = router.lookup("/users", Method::Post);
1567 match result {
1568 RouteLookup::MethodNotAllowed { allowed } => {
1569 assert!(allowed.contains(Method::Get));
1570 assert!(allowed.contains(Method::Head));
1571 assert_eq!(allowed.header_value(), "GET, HEAD");
1572 }
1573 _ => panic!("expected MethodNotAllowed"),
1574 }
1575 }
1576
1577 #[test]
1578 fn lookup_method_not_allowed_multiple_methods() {
1579 let mut router = Router::new();
1580 router.add(route(Method::Get, "/users")).unwrap();
1581 router.add(route(Method::Post, "/users")).unwrap();
1582 router.add(route(Method::Delete, "/users")).unwrap();
1583
1584 let result = router.lookup("/users", Method::Put);
1585 match result {
1586 RouteLookup::MethodNotAllowed { allowed } => {
1587 assert_eq!(allowed.header_value(), "GET, HEAD, POST, DELETE");
1588 }
1589 _ => panic!("expected MethodNotAllowed"),
1590 }
1591 }
1592
1593 #[test]
1594 fn lookup_not_found_when_path_missing() {
1595 let mut router = Router::new();
1596 router.add(route(Method::Get, "/users")).unwrap();
1597
1598 assert!(matches!(
1599 router.lookup("/missing", Method::Get),
1600 RouteLookup::NotFound
1601 ));
1602 }
1603
1604 #[test]
1605 fn lookup_not_found_when_converter_mismatch() {
1606 let mut router = Router::new();
1607 router.add(route(Method::Get, "/items/{id:int}")).unwrap();
1608
1609 assert!(matches!(
1610 router.lookup("/items/abc", Method::Get),
1611 RouteLookup::NotFound
1612 ));
1613 }
1614
1615 #[test]
1616 fn static_takes_priority_over_param() {
1617 let mut router = Router::new();
1618 router.add(route(Method::Get, "/users/me")).unwrap();
1620 router.add(route(Method::Get, "/users/{id}")).unwrap();
1621
1622 let m = router.match_path("/users/me", Method::Get);
1624 assert!(m.is_some());
1625 let m = m.unwrap();
1626 assert_eq!(m.route.path, "/users/me");
1627 assert!(m.params.is_empty());
1628
1629 let m = router.match_path("/users/123", Method::Get);
1631 assert!(m.is_some());
1632 let m = m.unwrap();
1633 assert_eq!(m.route.path, "/users/{id}");
1634 assert_eq!(m.params[0], ("id", "123"));
1635 }
1636
1637 #[test]
1638 fn route_match_get_param() {
1639 let mut router = Router::new();
1640 router
1641 .add(route(Method::Get, "/users/{user_id}/posts/{post_id}"))
1642 .unwrap();
1643
1644 let m = router
1645 .match_path("/users/42/posts/99", Method::Get)
1646 .unwrap();
1647
1648 assert_eq!(m.get_param("user_id"), Some("42"));
1649 assert_eq!(m.get_param("post_id"), Some("99"));
1650 assert_eq!(m.get_param("unknown"), None);
1651 }
1652
1653 #[test]
1654 fn converter_matches() {
1655 assert!(Converter::Str.matches("anything"));
1656 assert!(Converter::Str.matches("123"));
1657
1658 assert!(Converter::Int.matches("123"));
1659 assert!(Converter::Int.matches("-456"));
1660 assert!(!Converter::Int.matches("12.34"));
1661 assert!(!Converter::Int.matches("abc"));
1662
1663 assert!(Converter::Float.matches("3.14"));
1664 assert!(Converter::Float.matches("42"));
1665 assert!(!Converter::Float.matches("abc"));
1666
1667 assert!(Converter::Uuid.matches("550e8400-e29b-41d4-a716-446655440000"));
1668 assert!(!Converter::Uuid.matches("not-a-uuid"));
1669
1670 assert!(Converter::Path.matches("any/path/here"));
1671 }
1672
1673 #[test]
1674 fn parse_path_segments() {
1675 let segments = parse_path("/users/{id}/posts/{post_id:int}");
1676 assert_eq!(segments.len(), 4);
1677
1678 match &segments[0] {
1679 PathSegment::Static(s) => assert_eq!(*s, "users"),
1680 _ => panic!("Expected static segment"),
1681 }
1682
1683 match &segments[1] {
1684 PathSegment::Param { name, converter } => {
1685 assert_eq!(*name, "id");
1686 assert_eq!(*converter, Converter::Str);
1687 }
1688 _ => panic!("Expected param segment"),
1689 }
1690
1691 match &segments[2] {
1692 PathSegment::Static(s) => assert_eq!(*s, "posts"),
1693 _ => panic!("Expected static segment"),
1694 }
1695
1696 match &segments[3] {
1697 PathSegment::Param { name, converter } => {
1698 assert_eq!(*name, "post_id");
1699 assert_eq!(*converter, Converter::Int);
1700 }
1701 _ => panic!("Expected param segment"),
1702 }
1703 }
1704
1705 #[test]
1710 fn extract_path_params_simple() {
1711 let params = extract_path_params("/users/{id}");
1712 assert_eq!(params.len(), 1);
1713 assert_eq!(params[0].name, "id");
1714 assert_eq!(params[0].converter, Converter::Str);
1715 }
1716
1717 #[test]
1718 fn extract_path_params_multiple() {
1719 let params = extract_path_params("/users/{id}/posts/{post_id}");
1720 assert_eq!(params.len(), 2);
1721 assert_eq!(params[0].name, "id");
1722 assert_eq!(params[1].name, "post_id");
1723 }
1724
1725 #[test]
1726 fn extract_path_params_typed_int() {
1727 let params = extract_path_params("/items/{item_id:int}");
1728 assert_eq!(params.len(), 1);
1729 assert_eq!(params[0].name, "item_id");
1730 assert_eq!(params[0].converter, Converter::Int);
1731 }
1732
1733 #[test]
1734 fn extract_path_params_typed_float() {
1735 let params = extract_path_params("/prices/{value:float}");
1736 assert_eq!(params.len(), 1);
1737 assert_eq!(params[0].name, "value");
1738 assert_eq!(params[0].converter, Converter::Float);
1739 }
1740
1741 #[test]
1742 fn extract_path_params_typed_uuid() {
1743 let params = extract_path_params("/resources/{uuid:uuid}");
1744 assert_eq!(params.len(), 1);
1745 assert_eq!(params[0].name, "uuid");
1746 assert_eq!(params[0].converter, Converter::Uuid);
1747 }
1748
1749 #[test]
1750 fn extract_path_params_wildcard_asterisk() {
1751 let params = extract_path_params("/files/{*filepath}");
1752 assert_eq!(params.len(), 1);
1753 assert_eq!(params[0].name, "filepath");
1754 assert_eq!(params[0].converter, Converter::Path);
1755 }
1756
1757 #[test]
1758 fn extract_path_params_wildcard_path_converter() {
1759 let params = extract_path_params("/static/{path:path}");
1760 assert_eq!(params.len(), 1);
1761 assert_eq!(params[0].name, "path");
1762 assert_eq!(params[0].converter, Converter::Path);
1763 }
1764
1765 #[test]
1766 fn extract_path_params_mixed_types() {
1767 let params = extract_path_params("/api/{version}/items/{id:int}/details/{slug}");
1768 assert_eq!(params.len(), 3);
1769 assert_eq!(params[0].name, "version");
1770 assert_eq!(params[0].converter, Converter::Str);
1771 assert_eq!(params[1].name, "id");
1772 assert_eq!(params[1].converter, Converter::Int);
1773 assert_eq!(params[2].name, "slug");
1774 assert_eq!(params[2].converter, Converter::Str);
1775 }
1776
1777 #[test]
1778 fn extract_path_params_no_params() {
1779 let params = extract_path_params("/static/path/no/params");
1780 assert!(params.is_empty());
1781 }
1782
1783 #[test]
1784 fn extract_path_params_root() {
1785 let params = extract_path_params("/");
1786 assert!(params.is_empty());
1787 }
1788
1789 #[test]
1794 fn param_info_new() {
1795 let info = ParamInfo::new("id", Converter::Int);
1796 assert_eq!(info.name, "id");
1797 assert_eq!(info.converter, Converter::Int);
1798 assert!(info.title.is_none());
1799 assert!(info.description.is_none());
1800 assert!(!info.deprecated);
1801 assert!(info.example.is_none());
1802 assert!(info.examples.is_empty());
1803 }
1804
1805 #[test]
1806 fn param_info_with_title() {
1807 let info = ParamInfo::new("id", Converter::Str).with_title("User ID");
1808 assert_eq!(info.title.as_deref(), Some("User ID"));
1809 }
1810
1811 #[test]
1812 fn param_info_with_description() {
1813 let info =
1814 ParamInfo::new("page", Converter::Int).with_description("Page number for pagination");
1815 assert_eq!(
1816 info.description.as_deref(),
1817 Some("Page number for pagination")
1818 );
1819 }
1820
1821 #[test]
1822 fn param_info_deprecated() {
1823 let info = ParamInfo::new("old", Converter::Str).deprecated();
1824 assert!(info.deprecated);
1825 }
1826
1827 #[test]
1828 fn param_info_with_example() {
1829 let info = ParamInfo::new("id", Converter::Int).with_example(serde_json::json!(42));
1830 assert_eq!(info.example, Some(serde_json::json!(42)));
1831 }
1832
1833 #[test]
1834 fn param_info_with_named_examples() {
1835 let info = ParamInfo::new("status", Converter::Str)
1836 .with_named_example("active", serde_json::json!("active"))
1837 .with_named_example("inactive", serde_json::json!("inactive"));
1838 assert_eq!(info.examples.len(), 2);
1839 assert_eq!(info.examples[0].0, "active");
1840 assert_eq!(info.examples[1].0, "inactive");
1841 }
1842
1843 #[test]
1844 fn param_info_builder_chain() {
1845 let info = ParamInfo::new("item_id", Converter::Int)
1846 .with_title("Item ID")
1847 .with_description("The unique item identifier")
1848 .deprecated()
1849 .with_example(serde_json::json!(123));
1850
1851 assert_eq!(info.name, "item_id");
1852 assert_eq!(info.converter, Converter::Int);
1853 assert_eq!(info.title.as_deref(), Some("Item ID"));
1854 assert_eq!(
1855 info.description.as_deref(),
1856 Some("The unique item identifier")
1857 );
1858 assert!(info.deprecated);
1859 assert_eq!(info.example, Some(serde_json::json!(123)));
1860 }
1861
1862 #[test]
1863 fn empty_router() {
1864 let router = Router::new();
1865 assert!(router.match_path("/anything", Method::Get).is_none());
1866 assert!(router.routes().is_empty());
1867 }
1868
1869 #[test]
1870 fn routes_accessor() {
1871 let mut router = Router::new();
1872 let _ = router.add(route(Method::Get, "/a"));
1873 let _ = router.add(route(Method::Post, "/b"));
1874
1875 assert_eq!(router.routes().len(), 2);
1876 assert_eq!(router.routes()[0].path, "/a");
1877 assert_eq!(router.routes()[1].path, "/b");
1878 }
1879
1880 #[test]
1885 fn conflict_same_method_same_path() {
1886 let mut router = Router::new();
1887 router.add(route(Method::Get, "/users")).unwrap();
1888
1889 let result = router.add(route(Method::Get, "/users"));
1890 assert!(result.is_err());
1891 let err = match result.unwrap_err() {
1892 RouteAddError::Conflict(err) => err,
1893 RouteAddError::InvalidPath(err) => {
1894 panic!("unexpected invalid path error: {err}")
1895 }
1896 };
1897 assert_eq!(err.method, Method::Get);
1898 assert_eq!(err.new_path, "/users");
1899 assert_eq!(err.existing_path, "/users");
1900 }
1901
1902 #[test]
1903 fn conflict_same_method_same_param_pattern() {
1904 let mut router = Router::new();
1905 router.add(route(Method::Get, "/users/{id}")).unwrap();
1906
1907 let result = router.add(route(Method::Get, "/users/{user_id}"));
1909 assert!(result.is_err());
1910 let err = match result.unwrap_err() {
1911 RouteAddError::Conflict(err) => err,
1912 RouteAddError::InvalidPath(err) => {
1913 panic!("unexpected invalid path error: {err}")
1914 }
1915 };
1916 assert_eq!(err.existing_path, "/users/{id}");
1917 assert_eq!(err.new_path, "/users/{user_id}");
1918 }
1919
1920 #[test]
1921 fn conflict_param_name_mismatch_across_lengths() {
1922 let mut router = Router::new();
1923 router.add(route(Method::Get, "/users/{id}/posts")).unwrap();
1924
1925 let result = router.add(route(Method::Get, "/users/{user_id}"));
1926 assert!(matches!(result, Err(RouteAddError::Conflict(_))));
1927 }
1928
1929 #[test]
1930 fn conflict_different_converter_same_position() {
1931 let mut router = Router::new();
1932 router.add(route(Method::Get, "/items/{id:int}")).unwrap();
1933
1934 let result = router.add(route(Method::Get, "/items/{id:uuid}"));
1936 assert!(matches!(result, Err(RouteAddError::Conflict(_))));
1937 }
1938
1939 #[test]
1940 fn no_conflict_different_methods() {
1941 let mut router = Router::new();
1942 router.add(route(Method::Get, "/users")).unwrap();
1943 router.add(route(Method::Post, "/users")).unwrap();
1944 router.add(route(Method::Put, "/users")).unwrap();
1945 router.add(route(Method::Delete, "/users")).unwrap();
1946 router.add(route(Method::Patch, "/users")).unwrap();
1947
1948 assert_eq!(router.routes().len(), 5);
1949 }
1950
1951 #[test]
1952 fn no_conflict_static_vs_param() {
1953 let mut router = Router::new();
1954 router.add(route(Method::Get, "/users/me")).unwrap();
1955 router.add(route(Method::Get, "/users/{id}")).unwrap();
1956
1957 assert_eq!(router.routes().len(), 2);
1959 }
1960
1961 #[test]
1962 fn no_conflict_different_path_lengths() {
1963 let mut router = Router::new();
1964 router.add(route(Method::Get, "/users")).unwrap();
1965 router.add(route(Method::Get, "/users/{id}")).unwrap();
1966 router.add(route(Method::Get, "/users/{id}/posts")).unwrap();
1967
1968 assert_eq!(router.routes().len(), 3);
1969 }
1970
1971 #[test]
1972 fn conflict_error_display() {
1973 let err = RouteConflictError {
1974 method: Method::Get,
1975 new_path: "/new".to_string(),
1976 existing_path: "/existing".to_string(),
1977 };
1978 let msg = format!("{}", err);
1979 assert!(msg.contains("GET"));
1980 assert!(msg.contains("/new"));
1981 assert!(msg.contains("/existing"));
1982 }
1983
1984 #[test]
1989 fn root_path() {
1990 let mut router = Router::new();
1991 router.add(route(Method::Get, "/")).unwrap();
1992
1993 let m = router.match_path("/", Method::Get);
1994 assert!(m.is_some());
1995 assert_eq!(m.unwrap().route.path, "/");
1996 }
1997
1998 #[test]
1999 fn trailing_slash_handling() {
2000 let mut router = Router::new();
2001 router.add(route(Method::Get, "/users")).unwrap();
2002
2003 let m = router.match_path("/users/", Method::Get);
2006 assert!(m.is_none() || m.is_some());
2008 }
2009
2010 #[test]
2011 fn multiple_consecutive_slashes() {
2012 let mut router = Router::new();
2013 router.add(route(Method::Get, "/users")).unwrap();
2014
2015 let m = router.match_path("//users", Method::Get);
2018 assert!(m.is_some());
2019 assert_eq!(m.unwrap().route.path, "/users");
2020 }
2021
2022 #[test]
2023 fn unicode_in_static_path() {
2024 let mut router = Router::new();
2025 router.add(route(Method::Get, "/用户")).unwrap();
2026 router.add(route(Method::Get, "/données")).unwrap();
2027
2028 let m = router.match_path("/用户", Method::Get);
2029 assert!(m.is_some());
2030 assert_eq!(m.unwrap().route.path, "/用户");
2031
2032 let m = router.match_path("/données", Method::Get);
2033 assert!(m.is_some());
2034 assert_eq!(m.unwrap().route.path, "/données");
2035 }
2036
2037 #[test]
2038 fn unicode_in_param_value() {
2039 let mut router = Router::new();
2040 router.add(route(Method::Get, "/users/{name}")).unwrap();
2041
2042 let m = router.match_path("/users/田中", Method::Get);
2043 assert!(m.is_some());
2044 let m = m.unwrap();
2045 assert_eq!(m.params[0], ("name", "田中"));
2046 }
2047
2048 #[test]
2049 fn special_characters_in_param_value() {
2050 let mut router = Router::new();
2051 router.add(route(Method::Get, "/files/{name}")).unwrap();
2052
2053 let m = router.match_path("/files/my-file_v2", Method::Get);
2055 assert!(m.is_some());
2056 assert_eq!(m.unwrap().params[0], ("name", "my-file_v2"));
2057
2058 let m = router.match_path("/files/document.pdf", Method::Get);
2060 assert!(m.is_some());
2061 assert_eq!(m.unwrap().params[0], ("name", "document.pdf"));
2062 }
2063
2064 #[test]
2065 fn empty_param_value() {
2066 let mut router = Router::new();
2067 router.add(route(Method::Get, "/users/{id}/posts")).unwrap();
2068
2069 let m = router.match_path("/users//posts", Method::Get);
2071 assert!(m.is_none());
2073 }
2074
2075 #[test]
2076 fn very_long_path() {
2077 let mut router = Router::new();
2078 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";
2079 router.add(route(Method::Get, long_path)).unwrap();
2080
2081 let m = router.match_path(long_path, Method::Get);
2082 assert!(m.is_some());
2083 assert_eq!(m.unwrap().route.path, long_path);
2084 }
2085
2086 #[test]
2087 fn many_routes_same_prefix() {
2088 let mut router = Router::new();
2089 for i in 0..100 {
2090 router
2091 .add(route(Method::Get, &format!("/api/v{}", i)))
2092 .unwrap();
2093 }
2094
2095 assert_eq!(router.routes().len(), 100);
2096
2097 for i in 0..100 {
2099 let path = format!("/api/v{}", i);
2100 let m = router.match_path(&path, Method::Get);
2101 assert!(m.is_some());
2102 assert_eq!(m.unwrap().route.path, path);
2103 }
2104 }
2105
2106 #[test]
2111 fn head_matches_get_route() {
2112 let mut router = Router::new();
2113 router.add(route(Method::Get, "/users")).unwrap();
2114
2115 let m = router.match_path("/users", Method::Head);
2117 assert!(m.is_some());
2118 assert_eq!(m.unwrap().route.method, Method::Get);
2119 }
2120
2121 #[test]
2122 fn head_with_explicit_head_route() {
2123 let mut router = Router::new();
2124 router.add(route(Method::Get, "/users")).unwrap();
2125 router.add(route(Method::Head, "/users")).unwrap();
2126
2127 let m = router.match_path("/users", Method::Head);
2129 assert!(m.is_some());
2130 assert_eq!(m.unwrap().route.method, Method::Head);
2131 }
2132
2133 #[test]
2134 fn head_does_not_match_non_get() {
2135 let mut router = Router::new();
2136 router.add(route(Method::Post, "/users")).unwrap();
2137
2138 let result = router.lookup("/users", Method::Head);
2140 match result {
2141 RouteLookup::MethodNotAllowed { allowed } => {
2142 assert!(!allowed.contains(Method::Head));
2143 assert!(allowed.contains(Method::Post));
2144 }
2145 _ => panic!("expected MethodNotAllowed"),
2146 }
2147 }
2148
2149 #[test]
2154 fn int_converter_edge_cases() {
2155 let mut router = Router::new();
2156 router.add(route(Method::Get, "/items/{id:int}")).unwrap();
2157
2158 let m = router.match_path("/items/0", Method::Get);
2160 assert!(m.is_some());
2161
2162 let m = router.match_path("/items/9223372036854775807", Method::Get);
2164 assert!(m.is_some());
2165
2166 let m = router.match_path("/items/-9223372036854775808", Method::Get);
2168 assert!(m.is_some());
2169
2170 let m = router.match_path("/items/007", Method::Get);
2172 assert!(m.is_some());
2173
2174 let m = router.match_path("/items/+123", Method::Get);
2176 assert!(m.is_some());
2178 }
2179
2180 #[test]
2181 fn float_converter_edge_cases() {
2182 let mut router = Router::new();
2183 router
2184 .add(route(Method::Get, "/values/{val:float}"))
2185 .unwrap();
2186
2187 let m = router.match_path("/values/1e10", Method::Get);
2189 assert!(m.is_some());
2190
2191 let m = router.match_path("/values/1e-10", Method::Get);
2193 assert!(m.is_some());
2194
2195 let m = router.match_path("/values/inf", Method::Get);
2197 assert!(m.is_some());
2198
2199 let m = router.match_path("/values/NaN", Method::Get);
2201 assert!(m.is_some());
2202 }
2203
2204 #[test]
2205 fn uuid_converter_case_sensitivity() {
2206 let mut router = Router::new();
2207 router
2208 .add(route(Method::Get, "/objects/{id:uuid}"))
2209 .unwrap();
2210
2211 let m = router.match_path("/objects/550e8400-e29b-41d4-a716-446655440000", Method::Get);
2213 assert!(m.is_some());
2214
2215 let m = router.match_path("/objects/550E8400-E29B-41D4-A716-446655440000", Method::Get);
2217 assert!(m.is_some());
2218
2219 let m = router.match_path("/objects/550e8400-E29B-41d4-A716-446655440000", Method::Get);
2221 assert!(m.is_some());
2222 }
2223
2224 #[test]
2225 fn uuid_converter_invalid_formats() {
2226 let mut router = Router::new();
2227 router
2228 .add(route(Method::Get, "/objects/{id:uuid}"))
2229 .unwrap();
2230
2231 assert!(
2233 router
2234 .match_path("/objects/550e8400-e29b-41d4-a716-44665544000", Method::Get)
2235 .is_none()
2236 );
2237 assert!(
2238 router
2239 .match_path(
2240 "/objects/550e8400-e29b-41d4-a716-4466554400000",
2241 Method::Get
2242 )
2243 .is_none()
2244 );
2245
2246 assert!(
2248 router
2249 .match_path("/objects/550e8400e29b41d4a716446655440000", Method::Get)
2250 .is_none()
2251 );
2252
2253 assert!(
2255 router
2256 .match_path("/objects/550g8400-e29b-41d4-a716-446655440000", Method::Get)
2257 .is_none()
2258 );
2259 }
2260
2261 #[test]
2262 fn unknown_converter_defaults_to_str() {
2263 let segments = parse_path("/items/{id:custom}");
2264 assert_eq!(segments.len(), 2);
2265 match &segments[1] {
2266 PathSegment::Param { name, converter } => {
2267 assert_eq!(*name, "id");
2268 assert_eq!(*converter, Converter::Str);
2269 }
2270 _ => panic!("Expected param segment"),
2271 }
2272 }
2273
2274 #[test]
2279 fn parse_empty_path() {
2280 let segments = parse_path("");
2281 assert!(segments.is_empty());
2282 }
2283
2284 #[test]
2285 fn parse_root_only() {
2286 let segments = parse_path("/");
2287 assert!(segments.is_empty());
2288 }
2289
2290 #[test]
2291 fn parse_leading_trailing_slashes() {
2292 let segments = parse_path("///users///");
2293 assert_eq!(segments.len(), 1);
2294 match &segments[0] {
2295 PathSegment::Static(s) => assert_eq!(*s, "users"),
2296 _ => panic!("Expected static segment"),
2297 }
2298 }
2299
2300 #[test]
2301 fn parse_param_with_colon_no_type() {
2302 let segments = parse_path("/items/{id:}");
2304 assert_eq!(segments.len(), 2);
2305 match &segments[1] {
2306 PathSegment::Param { name, converter } => {
2307 assert_eq!(*name, "id");
2308 assert_eq!(*converter, Converter::Str);
2310 }
2311 _ => panic!("Expected param segment"),
2312 }
2313 }
2314
2315 #[test]
2320 fn lookup_404_empty_router() {
2321 let router = Router::new();
2322 assert!(matches!(
2323 router.lookup("/anything", Method::Get),
2324 RouteLookup::NotFound
2325 ));
2326 }
2327
2328 #[test]
2329 fn lookup_404_no_matching_path() {
2330 let mut router = Router::new();
2331 router.add(route(Method::Get, "/users")).unwrap();
2332 router.add(route(Method::Get, "/items")).unwrap();
2333
2334 assert!(matches!(
2335 router.lookup("/other", Method::Get),
2336 RouteLookup::NotFound
2337 ));
2338 assert!(matches!(
2339 router.lookup("/user", Method::Get),
2340 RouteLookup::NotFound
2341 )); }
2343
2344 #[test]
2345 fn lookup_404_partial_path_match() {
2346 let mut router = Router::new();
2347 router.add(route(Method::Get, "/api/v1/users")).unwrap();
2348
2349 assert!(matches!(
2351 router.lookup("/api", Method::Get),
2352 RouteLookup::NotFound
2353 ));
2354 assert!(matches!(
2355 router.lookup("/api/v1", Method::Get),
2356 RouteLookup::NotFound
2357 ));
2358 }
2359
2360 #[test]
2361 fn lookup_404_extra_path_segments() {
2362 let mut router = Router::new();
2363 router.add(route(Method::Get, "/users")).unwrap();
2364
2365 assert!(matches!(
2367 router.lookup("/users/extra", Method::Get),
2368 RouteLookup::NotFound
2369 ));
2370 }
2371
2372 #[test]
2373 fn lookup_405_single_method() {
2374 let mut router = Router::new();
2375 router.add(route(Method::Get, "/users")).unwrap();
2376
2377 let result = router.lookup("/users", Method::Post);
2378 match result {
2379 RouteLookup::MethodNotAllowed { allowed } => {
2380 assert_eq!(allowed.methods(), &[Method::Get, Method::Head]);
2381 }
2382 _ => panic!("expected MethodNotAllowed"),
2383 }
2384 }
2385
2386 #[test]
2387 fn lookup_405_all_methods() {
2388 let mut router = Router::new();
2389 router.add(route(Method::Get, "/resource")).unwrap();
2390 router.add(route(Method::Post, "/resource")).unwrap();
2391 router.add(route(Method::Put, "/resource")).unwrap();
2392 router.add(route(Method::Delete, "/resource")).unwrap();
2393 router.add(route(Method::Patch, "/resource")).unwrap();
2394 router.add(route(Method::Options, "/resource")).unwrap();
2395
2396 let result = router.lookup("/resource", Method::Trace);
2397 match result {
2398 RouteLookup::MethodNotAllowed { allowed } => {
2399 let header = allowed.header_value();
2400 assert!(header.contains("GET"));
2401 assert!(header.contains("HEAD"));
2402 assert!(header.contains("POST"));
2403 assert!(header.contains("PUT"));
2404 assert!(header.contains("DELETE"));
2405 assert!(header.contains("PATCH"));
2406 assert!(header.contains("OPTIONS"));
2407 }
2408 _ => panic!("expected MethodNotAllowed"),
2409 }
2410 }
2411
2412 #[test]
2417 fn allowed_methods_deduplication() {
2418 let allowed = AllowedMethods::new(vec![Method::Get, Method::Get, Method::Post]);
2420 assert_eq!(allowed.methods().len(), 3); }
2422
2423 #[test]
2424 fn allowed_methods_sorting() {
2425 let allowed = AllowedMethods::new(vec![Method::Delete, Method::Get, Method::Post]);
2427 assert_eq!(allowed.methods()[0], Method::Get);
2428 assert_eq!(allowed.methods()[1], Method::Head); assert_eq!(allowed.methods()[2], Method::Post);
2430 assert_eq!(allowed.methods()[3], Method::Delete);
2431 }
2432
2433 #[test]
2434 fn allowed_methods_head_not_duplicated() {
2435 let allowed = AllowedMethods::new(vec![Method::Get, Method::Head]);
2437 let count = allowed
2438 .methods()
2439 .iter()
2440 .filter(|&&m| m == Method::Head)
2441 .count();
2442 assert_eq!(count, 1);
2443 }
2444
2445 #[test]
2446 fn allowed_methods_empty() {
2447 let allowed = AllowedMethods::new(vec![]);
2448 assert!(allowed.methods().is_empty());
2449 assert_eq!(allowed.header_value(), "");
2450 }
2451
2452 #[test]
2457 fn wildcard_asterisk_syntax_basic() {
2458 let mut router = Router::new();
2459 router.add(route(Method::Get, "/files/{*path}")).unwrap();
2460
2461 let m = router.match_path("/files/a.txt", Method::Get).unwrap();
2462 assert_eq!(m.params[0], ("path", "a.txt"));
2463 }
2464
2465 #[test]
2466 fn wildcard_asterisk_captures_multiple_segments() {
2467 let mut router = Router::new();
2468 router
2469 .add(route(Method::Get, "/files/{*filepath}"))
2470 .unwrap();
2471
2472 let m = router
2473 .match_path("/files/css/styles/main.css", Method::Get)
2474 .unwrap();
2475 assert_eq!(m.params[0], ("filepath", "css/styles/main.css"));
2476 }
2477
2478 #[test]
2479 fn wildcard_asterisk_with_prefix() {
2480 let mut router = Router::new();
2481 router.add(route(Method::Get, "/api/v1/{*rest}")).unwrap();
2482
2483 let m = router
2484 .match_path("/api/v1/users/123/posts", Method::Get)
2485 .unwrap();
2486 assert_eq!(m.params[0], ("rest", "users/123/posts"));
2487 }
2488
2489 #[test]
2490 fn wildcard_asterisk_empty_capture() {
2491 let mut router = Router::new();
2492 router.add(route(Method::Get, "/files/{*path}")).unwrap();
2493
2494 let m = router.match_path("/files/x", Method::Get).unwrap();
2496 assert_eq!(m.params[0], ("path", "x"));
2497 }
2498
2499 #[test]
2500 fn wildcard_asterisk_must_be_terminal() {
2501 let mut router = Router::new();
2502 let result = router.add(route(Method::Get, "/files/{*path}/edit"));
2503 assert!(matches!(result, Err(RouteAddError::InvalidPath(_))));
2504 }
2505
2506 #[test]
2507 fn wildcard_asterisk_syntax_equivalent_to_path_converter() {
2508 let segments_asterisk = parse_path("/files/{*filepath}");
2510 let segments_converter = parse_path("/files/{filepath:path}");
2511
2512 assert_eq!(segments_asterisk.len(), 2);
2513 assert_eq!(segments_converter.len(), 2);
2514
2515 match (&segments_asterisk[1], &segments_converter[1]) {
2516 (
2517 PathSegment::Param {
2518 name: n1,
2519 converter: c1,
2520 },
2521 PathSegment::Param {
2522 name: n2,
2523 converter: c2,
2524 },
2525 ) => {
2526 assert_eq!(*n1, "filepath");
2527 assert_eq!(*n2, "filepath");
2528 assert_eq!(*c1, Converter::Path);
2529 assert_eq!(*c2, Converter::Path);
2530 }
2531 _ => panic!("Expected param segments"),
2532 }
2533 }
2534
2535 #[test]
2536 fn wildcard_asterisk_spa_routing() {
2537 let mut router = Router::new();
2538 router.add(route(Method::Get, "/api/users")).unwrap();
2540 router.add(route(Method::Get, "/api/posts")).unwrap();
2541 router.add(route(Method::Get, "/{*route}")).unwrap();
2543
2544 let m = router.match_path("/api/users", Method::Get).unwrap();
2546 assert_eq!(m.route.path, "/api/users");
2547
2548 let m = router.match_path("/dashboard", Method::Get).unwrap();
2550 assert_eq!(m.params[0], ("route", "dashboard"));
2551
2552 let m = router
2553 .match_path("/users/123/profile", Method::Get)
2554 .unwrap();
2555 assert_eq!(m.params[0], ("route", "users/123/profile"));
2556 }
2557
2558 #[test]
2559 fn wildcard_asterisk_file_serving() {
2560 let mut router = Router::new();
2561 router
2562 .add(route(Method::Get, "/static/{*filepath}"))
2563 .unwrap();
2564
2565 let m = router
2566 .match_path("/static/js/app.bundle.js", Method::Get)
2567 .unwrap();
2568 assert_eq!(m.params[0], ("filepath", "js/app.bundle.js"));
2569
2570 let m = router
2571 .match_path("/static/images/logo.png", Method::Get)
2572 .unwrap();
2573 assert_eq!(m.params[0], ("filepath", "images/logo.png"));
2574 }
2575
2576 #[test]
2577 fn wildcard_asterisk_priority_lowest() {
2578 let mut router = Router::new();
2579 router.add(route(Method::Get, "/users/me")).unwrap();
2581 router.add(route(Method::Get, "/users/{id}")).unwrap();
2583 router.add(route(Method::Get, "/{*path}")).unwrap();
2585
2586 let m = router.match_path("/users/me", Method::Get).unwrap();
2588 assert_eq!(m.route.path, "/users/me");
2589 assert!(m.params.is_empty());
2590
2591 let m = router.match_path("/users/123", Method::Get).unwrap();
2593 assert_eq!(m.route.path, "/users/{id}");
2594 assert_eq!(m.params[0], ("id", "123"));
2595
2596 let m = router.match_path("/other/deep/path", Method::Get).unwrap();
2598 assert_eq!(m.route.path, "/{*path}");
2599 assert_eq!(m.params[0], ("path", "other/deep/path"));
2600 }
2601
2602 #[test]
2603 fn parse_wildcard_asterisk_syntax() {
2604 let segments = parse_path("/files/{*path}");
2605 assert_eq!(segments.len(), 2);
2606
2607 match &segments[0] {
2608 PathSegment::Static(s) => assert_eq!(*s, "files"),
2609 _ => panic!("Expected static segment"),
2610 }
2611
2612 match &segments[1] {
2613 PathSegment::Param { name, converter } => {
2614 assert_eq!(*name, "path");
2615 assert_eq!(*converter, Converter::Path);
2616 }
2617 _ => panic!("Expected param segment"),
2618 }
2619 }
2620
2621 #[test]
2622 fn wildcard_asterisk_conflict_with_path_converter() {
2623 let mut router = Router::new();
2624 router.add(route(Method::Get, "/files/{*path}")).unwrap();
2625
2626 let result = router.add(route(Method::Get, "/files/{filepath:path}"));
2628 assert!(matches!(result, Err(RouteAddError::Conflict(_))));
2629 }
2630
2631 #[test]
2632 fn wildcard_asterisk_different_methods_no_conflict() {
2633 let mut router = Router::new();
2634 router.add(route(Method::Get, "/files/{*path}")).unwrap();
2635 router.add(route(Method::Post, "/files/{*path}")).unwrap();
2636 router.add(route(Method::Delete, "/files/{*path}")).unwrap();
2637
2638 assert_eq!(router.routes().len(), 3);
2639
2640 let m = router.match_path("/files/a/b/c", Method::Get).unwrap();
2642 assert_eq!(m.route.method, Method::Get);
2643
2644 let m = router.match_path("/files/a/b/c", Method::Post).unwrap();
2645 assert_eq!(m.route.method, Method::Post);
2646
2647 let m = router.match_path("/files/a/b/c", Method::Delete).unwrap();
2648 assert_eq!(m.route.method, Method::Delete);
2649 }
2650
2651 #[test]
2664 fn priority_static_before_param() {
2665 let mut router = Router::new();
2667 router.add(route(Method::Get, "/users/{id}")).unwrap();
2668 router.add(route(Method::Get, "/users/me")).unwrap();
2669
2670 let m = router.match_path("/users/me", Method::Get).unwrap();
2672 assert_eq!(m.route.path, "/users/me");
2673 assert!(m.params.is_empty());
2674
2675 let m = router.match_path("/users/123", Method::Get).unwrap();
2677 assert_eq!(m.route.path, "/users/{id}");
2678 assert_eq!(m.params[0], ("id", "123"));
2679 }
2680
2681 #[test]
2682 fn priority_named_param_vs_wildcard_conflict() {
2683 let mut router = Router::new();
2686 router.add(route(Method::Get, "/files/{name}")).unwrap();
2687
2688 let result = router.add(route(Method::Get, "/files/{*path}"));
2690 assert!(
2691 matches!(result, Err(RouteAddError::Conflict(_))),
2692 "Named param and wildcard at same position should conflict"
2693 );
2694 }
2695
2696 #[test]
2697 fn priority_different_prefixes_no_conflict() {
2698 let mut router = Router::new();
2700 router.add(route(Method::Get, "/files/{name}")).unwrap();
2701 router.add(route(Method::Get, "/static/{*path}")).unwrap();
2702
2703 let m = router.match_path("/files/foo.txt", Method::Get).unwrap();
2705 assert_eq!(m.route.path, "/files/{name}");
2706
2707 let m = router
2709 .match_path("/static/css/main.css", Method::Get)
2710 .unwrap();
2711 assert_eq!(m.route.path, "/static/{*path}");
2712 }
2713
2714 #[test]
2715 fn priority_nested_param_before_shallow_wildcard() {
2716 let mut router = Router::new();
2718 router.add(route(Method::Get, "/{*path}")).unwrap();
2719 router.add(route(Method::Get, "/api/users")).unwrap();
2720
2721 let m = router.match_path("/api/users", Method::Get).unwrap();
2723 assert_eq!(m.route.path, "/api/users");
2724
2725 let m = router.match_path("/other/path", Method::Get).unwrap();
2727 assert_eq!(m.route.path, "/{*path}");
2728 }
2729
2730 #[test]
2731 fn priority_multiple_static_depths() {
2732 let mut router = Router::new();
2734 router.add(route(Method::Get, "/api/{*rest}")).unwrap();
2735 router.add(route(Method::Get, "/api/v1/users")).unwrap();
2736 router
2737 .add(route(Method::Get, "/api/v1/{resource}"))
2738 .unwrap();
2739
2740 let m = router.match_path("/api/v1/users", Method::Get).unwrap();
2742 assert_eq!(m.route.path, "/api/v1/users");
2743
2744 let m = router.match_path("/api/v1/items", Method::Get).unwrap();
2746 assert_eq!(m.route.path, "/api/v1/{resource}");
2747
2748 let m = router
2750 .match_path("/api/v2/anything/deep", Method::Get)
2751 .unwrap();
2752 assert_eq!(m.route.path, "/api/{*rest}");
2753 }
2754
2755 #[test]
2756 fn priority_complex_route_set() {
2757 let mut router = Router::new();
2759
2760 router.add(route(Method::Get, "/users/me")).unwrap();
2762 router
2763 .add(route(Method::Get, "/users/{user_id}/profile"))
2764 .unwrap();
2765 router.add(route(Method::Get, "/users/{user_id}")).unwrap();
2766 router.add(route(Method::Get, "/{*path}")).unwrap();
2767
2768 let m = router.match_path("/users/me", Method::Get).unwrap();
2770 assert_eq!(m.route.path, "/users/me");
2771
2772 let m = router.match_path("/users/123", Method::Get).unwrap();
2774 assert_eq!(m.route.path, "/users/{user_id}");
2775 assert_eq!(m.params[0], ("user_id", "123"));
2776
2777 let m = router
2779 .match_path("/users/123/profile", Method::Get)
2780 .unwrap();
2781 assert_eq!(m.route.path, "/users/{user_id}/profile");
2782 assert_eq!(m.params[0], ("user_id", "123"));
2783
2784 let m = router.match_path("/anything/else", Method::Get).unwrap();
2786 assert_eq!(m.route.path, "/{*path}");
2787 assert_eq!(m.params[0], ("path", "anything/else"));
2788 }
2789
2790 #[test]
2795 fn converter_convert_str() {
2796 let result = Converter::Str.convert("hello", "param");
2797 assert!(result.is_ok());
2798 assert_eq!(result.unwrap(), ParamValue::Str("hello".to_string()));
2799 }
2800
2801 #[test]
2802 fn converter_convert_int_valid() {
2803 let result = Converter::Int.convert("42", "id");
2804 assert!(result.is_ok());
2805 assert_eq!(result.unwrap(), ParamValue::Int(42));
2806 }
2807
2808 #[test]
2809 fn converter_convert_int_negative() {
2810 let result = Converter::Int.convert("-123", "id");
2811 assert!(result.is_ok());
2812 assert_eq!(result.unwrap(), ParamValue::Int(-123));
2813 }
2814
2815 #[test]
2816 fn converter_convert_int_invalid() {
2817 let result = Converter::Int.convert("abc", "id");
2818 assert!(result.is_err());
2819 match result.unwrap_err() {
2820 ConversionError::InvalidInt { value, param } => {
2821 assert_eq!(value, "abc");
2822 assert_eq!(param, "id");
2823 }
2824 _ => panic!("Expected InvalidInt error"),
2825 }
2826 }
2827
2828 #[test]
2829 #[allow(clippy::approx_constant)]
2830 fn converter_convert_float_valid() {
2831 let result = Converter::Float.convert("3.14", "val");
2832 assert!(result.is_ok());
2833 assert_eq!(result.unwrap(), ParamValue::Float(3.14));
2834 }
2835
2836 #[test]
2837 fn converter_convert_float_integer() {
2838 let result = Converter::Float.convert("42", "val");
2839 assert!(result.is_ok());
2840 assert_eq!(result.unwrap(), ParamValue::Float(42.0));
2841 }
2842
2843 #[test]
2844 fn converter_convert_float_scientific() {
2845 let result = Converter::Float.convert("1e10", "val");
2846 assert!(result.is_ok());
2847 assert_eq!(result.unwrap(), ParamValue::Float(1e10));
2848 }
2849
2850 #[test]
2851 fn converter_convert_float_invalid() {
2852 let result = Converter::Float.convert("not-a-float", "val");
2853 assert!(result.is_err());
2854 match result.unwrap_err() {
2855 ConversionError::InvalidFloat { value, param } => {
2856 assert_eq!(value, "not-a-float");
2857 assert_eq!(param, "val");
2858 }
2859 _ => panic!("Expected InvalidFloat error"),
2860 }
2861 }
2862
2863 #[test]
2864 fn converter_convert_uuid_valid() {
2865 let result = Converter::Uuid.convert("550e8400-e29b-41d4-a716-446655440000", "id");
2866 assert!(result.is_ok());
2867 assert_eq!(
2868 result.unwrap(),
2869 ParamValue::Uuid("550e8400-e29b-41d4-a716-446655440000".to_string())
2870 );
2871 }
2872
2873 #[test]
2874 fn converter_convert_uuid_invalid() {
2875 let result = Converter::Uuid.convert("not-a-uuid", "id");
2876 assert!(result.is_err());
2877 match result.unwrap_err() {
2878 ConversionError::InvalidUuid { value, param } => {
2879 assert_eq!(value, "not-a-uuid");
2880 assert_eq!(param, "id");
2881 }
2882 _ => panic!("Expected InvalidUuid error"),
2883 }
2884 }
2885
2886 #[test]
2887 fn converter_convert_path() {
2888 let result = Converter::Path.convert("a/b/c.txt", "filepath");
2889 assert!(result.is_ok());
2890 assert_eq!(result.unwrap(), ParamValue::Path("a/b/c.txt".to_string()));
2891 }
2892
2893 #[test]
2894 fn param_value_accessors() {
2895 let val = ParamValue::Str("hello".to_string());
2897 assert_eq!(val.as_str(), "hello");
2898 assert_eq!(val.as_int(), None);
2899 assert_eq!(val.as_float(), None);
2900 assert_eq!(val.into_string(), Some("hello".to_string()));
2901
2902 let val = ParamValue::Int(42);
2904 assert_eq!(val.as_int(), Some(42));
2905 assert_eq!(val.as_float(), None);
2906 assert_eq!(val.into_string(), None);
2907
2908 #[allow(clippy::approx_constant)]
2910 let val = ParamValue::Float(3.14);
2911 #[allow(clippy::approx_constant)]
2912 let expected_pi = Some(3.14);
2913 assert_eq!(val.as_float(), expected_pi);
2914 assert_eq!(val.as_int(), None);
2915 assert_eq!(val.into_string(), None);
2916
2917 let val = ParamValue::Uuid("550e8400-e29b-41d4-a716-446655440000".to_string());
2919 assert_eq!(val.as_str(), "550e8400-e29b-41d4-a716-446655440000");
2920 assert_eq!(
2921 val.into_string(),
2922 Some("550e8400-e29b-41d4-a716-446655440000".to_string())
2923 );
2924
2925 let val = ParamValue::Path("a/b/c".to_string());
2927 assert_eq!(val.as_str(), "a/b/c");
2928 assert_eq!(val.into_string(), Some("a/b/c".to_string()));
2929 }
2930
2931 #[test]
2932 fn conversion_error_display() {
2933 let err = ConversionError::InvalidInt {
2934 value: "abc".to_string(),
2935 param: "id".to_string(),
2936 };
2937 let msg = format!("{}", err);
2938 assert!(msg.contains("id"));
2939 assert!(msg.contains("abc"));
2940 assert!(msg.contains("integer"));
2941
2942 let err = ConversionError::InvalidFloat {
2943 value: "xyz".to_string(),
2944 param: "val".to_string(),
2945 };
2946 let msg = format!("{}", err);
2947 assert!(msg.contains("val"));
2948 assert!(msg.contains("xyz"));
2949 assert!(msg.contains("float"));
2950
2951 let err = ConversionError::InvalidUuid {
2952 value: "bad".to_string(),
2953 param: "uuid".to_string(),
2954 };
2955 let msg = format!("{}", err);
2956 assert!(msg.contains("uuid"));
2957 assert!(msg.contains("bad"));
2958 assert!(msg.contains("UUID"));
2959 }
2960
2961 #[test]
2962 fn converter_type_name() {
2963 assert_eq!(Converter::Str.type_name(), "string");
2964 assert_eq!(Converter::Int.type_name(), "integer");
2965 assert_eq!(Converter::Float.type_name(), "float");
2966 assert_eq!(Converter::Uuid.type_name(), "UUID");
2967 assert_eq!(Converter::Path.type_name(), "path");
2968 }
2969
2970 #[test]
2971 fn route_match_typed_getters() {
2972 let mut router = Router::new();
2973 router
2974 .add(route(Method::Get, "/items/{id:int}/price/{val:float}"))
2975 .unwrap();
2976
2977 let m = router
2978 .match_path("/items/42/price/99.99", Method::Get)
2979 .unwrap();
2980
2981 assert_eq!(m.get_param("id"), Some("42"));
2983 assert_eq!(m.get_param("val"), Some("99.99"));
2984
2985 assert_eq!(m.get_param_int("id"), Some(Ok(42)));
2987 assert_eq!(m.get_param_float("val"), Some(Ok(99.99)));
2988
2989 assert!(m.get_param_int("missing").is_none());
2991
2992 let result = m.get_param_int("val");
2994 assert!(result.is_some());
2997 assert!(result.unwrap().is_err());
2998 }
2999
3000 #[test]
3001 fn route_match_param_count() {
3002 let mut router = Router::new();
3003 router
3004 .add(route(Method::Get, "/users/{user_id}/posts/{post_id}"))
3005 .unwrap();
3006
3007 let m = router.match_path("/users/1/posts/2", Method::Get).unwrap();
3008
3009 assert_eq!(m.param_count(), 2);
3010 assert!(!m.is_empty());
3011
3012 let mut router2 = Router::new();
3014 router2.add(route(Method::Get, "/static")).unwrap();
3015 let m2 = router2.match_path("/static", Method::Get).unwrap();
3016 assert_eq!(m2.param_count(), 0);
3017 assert!(m2.is_empty());
3018 }
3019
3020 #[test]
3021 fn route_match_iter() {
3022 let mut router = Router::new();
3023 router
3024 .add(route(Method::Get, "/a/{x}/b/{y}/c/{z}"))
3025 .unwrap();
3026
3027 let m = router.match_path("/a/1/b/2/c/3", Method::Get).unwrap();
3028
3029 let params: Vec<_> = m.iter().collect();
3030 assert_eq!(params.len(), 3);
3031 assert_eq!(params[0], ("x", "1"));
3032 assert_eq!(params[1], ("y", "2"));
3033 assert_eq!(params[2], ("z", "3"));
3034 }
3035
3036 #[test]
3037 fn route_match_is_param_uuid() {
3038 let mut router = Router::new();
3039 router
3040 .add(route(Method::Get, "/objects/{id:uuid}"))
3041 .unwrap();
3042
3043 let m = router
3044 .match_path("/objects/550e8400-e29b-41d4-a716-446655440000", Method::Get)
3045 .unwrap();
3046
3047 assert_eq!(m.is_param_uuid("id"), Some(true));
3048 assert_eq!(m.is_param_uuid("missing"), None);
3049 }
3050
3051 #[test]
3052 fn route_match_integer_variants() {
3053 let mut router = Router::new();
3054 router.add(route(Method::Get, "/items/{id}")).unwrap();
3055
3056 let m = router.match_path("/items/12345", Method::Get).unwrap();
3057
3058 assert_eq!(m.get_param_int("id"), Some(Ok(12345i64)));
3060 assert_eq!(m.get_param_i32("id"), Some(Ok(12345i32)));
3061 assert_eq!(m.get_param_u64("id"), Some(Ok(12345u64)));
3062 assert_eq!(m.get_param_u32("id"), Some(Ok(12345u32)));
3063
3064 assert_eq!(m.get_param_float("id"), Some(Ok(12345.0f64)));
3066 assert_eq!(m.get_param_f32("id"), Some(Ok(12345.0f32)));
3067 }
3068
3069 #[test]
3074 fn mount_basic() {
3075 let mut child = Router::new();
3076 child.add(route(Method::Get, "/users")).unwrap();
3077 child.add(route(Method::Get, "/items")).unwrap();
3078
3079 let parent = Router::new().mount("/api/v1", child).unwrap();
3080
3081 let m = parent.match_path("/api/v1/users", Method::Get).unwrap();
3083 assert_eq!(m.route.path, "/api/v1/users");
3084
3085 let m = parent.match_path("/api/v1/items", Method::Get).unwrap();
3086 assert_eq!(m.route.path, "/api/v1/items");
3087 }
3088
3089 #[test]
3090 fn mount_with_params() {
3091 let mut child = Router::new();
3092 child.add(route(Method::Get, "/users/{id}")).unwrap();
3093 child
3094 .add(route(Method::Get, "/users/{id}/posts/{post_id}"))
3095 .unwrap();
3096
3097 let parent = Router::new().mount("/api", child).unwrap();
3098
3099 let m = parent.match_path("/api/users/42", Method::Get).unwrap();
3101 assert_eq!(m.route.path, "/api/users/{id}");
3102 assert_eq!(m.params[0], ("id", "42"));
3103
3104 let m = parent
3105 .match_path("/api/users/1/posts/99", Method::Get)
3106 .unwrap();
3107 assert_eq!(m.params.len(), 2);
3108 assert_eq!(m.params[0], ("id", "1"));
3109 assert_eq!(m.params[1], ("post_id", "99"));
3110 }
3111
3112 #[test]
3113 fn mount_preserves_methods() {
3114 let mut child = Router::new();
3115 child.add(route(Method::Get, "/resource")).unwrap();
3116 child.add(route(Method::Post, "/resource")).unwrap();
3117 child.add(route(Method::Delete, "/resource")).unwrap();
3118
3119 let parent = Router::new().mount("/api", child).unwrap();
3120
3121 let m = parent.match_path("/api/resource", Method::Get).unwrap();
3123 assert_eq!(m.route.method, Method::Get);
3124
3125 let m = parent.match_path("/api/resource", Method::Post).unwrap();
3126 assert_eq!(m.route.method, Method::Post);
3127
3128 let m = parent.match_path("/api/resource", Method::Delete).unwrap();
3129 assert_eq!(m.route.method, Method::Delete);
3130 }
3131
3132 #[test]
3133 fn mount_root_route() {
3134 let mut child = Router::new();
3135 child.add(route(Method::Get, "/")).unwrap();
3136
3137 let parent = Router::new().mount("/api", child).unwrap();
3138
3139 let m = parent.match_path("/api", Method::Get).unwrap();
3141 assert_eq!(m.route.path, "/api");
3142 }
3143
3144 #[test]
3145 fn mount_trailing_slash_prefix() {
3146 let mut child = Router::new();
3147 child.add(route(Method::Get, "/users")).unwrap();
3148
3149 let parent = Router::new().mount("/api/", child).unwrap();
3151
3152 let m = parent.match_path("/api/users", Method::Get).unwrap();
3153 assert_eq!(m.route.path, "/api/users");
3154 }
3155
3156 #[test]
3157 fn mount_empty_prefix() {
3158 let mut child = Router::new();
3159 child.add(route(Method::Get, "/users")).unwrap();
3160
3161 let parent = Router::new().mount("", child).unwrap();
3162
3163 let m = parent.match_path("/users", Method::Get).unwrap();
3164 assert_eq!(m.route.path, "/users");
3165 }
3166
3167 #[test]
3168 fn mount_nested() {
3169 let mut inner = Router::new();
3171 inner.add(route(Method::Get, "/items")).unwrap();
3172
3173 let middle = Router::new().mount("/v1", inner).unwrap();
3175
3176 let outer = Router::new().mount("/api", middle).unwrap();
3178
3179 let m = outer.match_path("/api/v1/items", Method::Get).unwrap();
3181 assert_eq!(m.route.path, "/api/v1/items");
3182 }
3183
3184 #[test]
3185 fn mount_conflict_detection() {
3186 let mut child1 = Router::new();
3187 child1.add(route(Method::Get, "/users")).unwrap();
3188
3189 let mut child2 = Router::new();
3190 child2.add(route(Method::Get, "/users")).unwrap();
3191
3192 let parent = Router::new().mount("/api", child1).unwrap();
3193
3194 let result = parent.mount("/api", child2);
3196 assert!(matches!(result, Err(RouteAddError::Conflict(_))));
3197 }
3198
3199 #[test]
3200 fn mount_no_conflict_different_prefixes() {
3201 let mut child1 = Router::new();
3202 child1.add(route(Method::Get, "/users")).unwrap();
3203
3204 let mut child2 = Router::new();
3205 child2.add(route(Method::Get, "/users")).unwrap();
3206
3207 let parent = Router::new()
3208 .mount("/api/v1", child1)
3209 .unwrap()
3210 .mount("/api/v2", child2)
3211 .unwrap();
3212
3213 let m = parent.match_path("/api/v1/users", Method::Get).unwrap();
3215 assert_eq!(m.route.path, "/api/v1/users");
3216
3217 let m = parent.match_path("/api/v2/users", Method::Get).unwrap();
3218 assert_eq!(m.route.path, "/api/v2/users");
3219 }
3220
3221 #[test]
3222 #[should_panic(expected = "route conflict when nesting router")]
3223 fn nest_panics_on_conflict() {
3224 let mut child1 = Router::new();
3225 child1.add(route(Method::Get, "/users")).unwrap();
3226
3227 let mut child2 = Router::new();
3228 child2.add(route(Method::Get, "/users")).unwrap();
3229
3230 let parent = Router::new().nest("/api", child1);
3231
3232 let _ = parent.nest("/api", child2);
3234 }
3235
3236 #[test]
3237 fn mount_with_wildcard() {
3238 let mut child = Router::new();
3239 child.add(route(Method::Get, "/files/{*path}")).unwrap();
3240
3241 let parent = Router::new().mount("/static", child).unwrap();
3242
3243 let m = parent
3245 .match_path("/static/files/css/style.css", Method::Get)
3246 .unwrap();
3247 assert_eq!(m.route.path, "/static/files/{*path}");
3248 assert_eq!(m.params[0], ("path", "css/style.css"));
3249 }
3250
3251 #[test]
3252 fn mount_parent_and_child_routes() {
3253 let mut parent = Router::new();
3254 parent.add(route(Method::Get, "/health")).unwrap();
3255
3256 let mut child = Router::new();
3257 child.add(route(Method::Get, "/users")).unwrap();
3258
3259 let app = parent.mount("/api", child).unwrap();
3260
3261 let m = app.match_path("/health", Method::Get).unwrap();
3263 assert_eq!(m.route.path, "/health");
3264
3265 let m = app.match_path("/api/users", Method::Get).unwrap();
3266 assert_eq!(m.route.path, "/api/users");
3267 }
3268
3269 #[test]
3286 fn percent_encoded_space_in_static_path() {
3287 let mut router = Router::new();
3288 router.add(route(Method::Get, "/hello%20world")).unwrap();
3289
3290 let m = router.match_path("/hello%20world", Method::Get);
3292 assert!(m.is_some());
3293 assert_eq!(m.unwrap().route.path, "/hello%20world");
3294
3295 let m = router.match_path("/hello world", Method::Get);
3297 assert!(m.is_none());
3298 }
3299
3300 #[test]
3301 fn percent_encoded_slash_in_param() {
3302 let mut router = Router::new();
3303 router.add(route(Method::Get, "/files/{name}")).unwrap();
3304
3305 let m = router.match_path("/files/a%2Fb.txt", Method::Get);
3307 assert!(m.is_some());
3308 assert_eq!(m.unwrap().params[0], ("name", "a%2Fb.txt"));
3309 }
3310
3311 #[test]
3312 fn percent_encoded_special_chars_in_param() {
3313 let mut router = Router::new();
3314 router.add(route(Method::Get, "/search/{query}")).unwrap();
3315
3316 let test_cases = vec![
3318 ("/search/hello%20world", ("query", "hello%20world")),
3319 ("/search/foo%26bar", ("query", "foo%26bar")), ("/search/a%3Db", ("query", "a%3Db")), ("/search/%23hash", ("query", "%23hash")), ("/search/100%25", ("query", "100%25")), ];
3324
3325 for (path, expected) in test_cases {
3326 let m = router.match_path(path, Method::Get);
3327 assert!(m.is_some(), "Failed to match: {}", path);
3328 assert_eq!(m.unwrap().params[0], expected);
3329 }
3330 }
3331
3332 #[test]
3333 fn percent_encoded_unicode_in_param() {
3334 let mut router = Router::new();
3335 router.add(route(Method::Get, "/users/{name}")).unwrap();
3336
3337 let m = router.match_path("/users/%E6%97%A5%E6%9C%AC", Method::Get);
3339 assert!(m.is_some());
3340 assert_eq!(m.unwrap().params[0], ("name", "%E6%97%A5%E6%9C%AC"));
3341 }
3342
3343 #[test]
3344 fn percent_encoded_in_wildcard() {
3345 let mut router = Router::new();
3346 router.add(route(Method::Get, "/files/{*path}")).unwrap();
3347
3348 let m = router.match_path("/files/dir%20name/file%20name.txt", Method::Get);
3350 assert!(m.is_some());
3351 assert_eq!(m.unwrap().params[0], ("path", "dir%20name/file%20name.txt"));
3352 }
3353
3354 #[test]
3355 fn double_percent_encoding() {
3356 let mut router = Router::new();
3357 router.add(route(Method::Get, "/data/{value}")).unwrap();
3358
3359 let m = router.match_path("/data/%2520", Method::Get);
3361 assert!(m.is_some());
3362 assert_eq!(m.unwrap().params[0], ("value", "%2520"));
3363 }
3364
3365 #[test]
3370 fn trailing_slash_strict_mode_static() {
3371 let mut router = Router::new();
3372 router.add(route(Method::Get, "/users")).unwrap();
3373 router.add(route(Method::Get, "/items/")).unwrap();
3374
3375 let m = router.match_path("/users", Method::Get);
3377 assert!(m.is_some());
3378 assert_eq!(m.unwrap().route.path, "/users");
3379
3380 let m = router.match_path("/users/", Method::Get);
3384 if let Some(m) = m {
3385 assert!(m.route.path == "/users" || m.route.path == "/users/");
3387 }
3388
3389 let m = router.match_path("/items/", Method::Get);
3391 assert!(m.is_some());
3392 }
3393
3394 #[test]
3395 fn trailing_slash_on_param_routes() {
3396 let mut router = Router::new();
3397 router.add(route(Method::Get, "/users/{id}")).unwrap();
3398
3399 let m = router.match_path("/users/123", Method::Get);
3401 assert!(m.is_some());
3402 assert_eq!(m.unwrap().params[0], ("id", "123"));
3403
3404 let m = router.match_path("/users/123/", Method::Get);
3406 if let Some(m) = m {
3408 assert_eq!(m.params[0].0, "id");
3409 }
3410 }
3411
3412 #[test]
3413 fn trailing_slash_on_nested_routes() {
3414 let mut router = Router::new();
3415 router.add(route(Method::Get, "/api/v1/users")).unwrap();
3416
3417 let result = router.add(route(Method::Get, "/api/v1/users/"));
3421 assert!(
3422 matches!(result, Err(RouteAddError::Conflict(_))),
3423 "Routes with and without trailing slash should conflict"
3424 );
3425
3426 assert_eq!(router.routes().len(), 1);
3428 }
3429
3430 #[test]
3431 fn multiple_trailing_slashes() {
3432 let mut router = Router::new();
3433 router.add(route(Method::Get, "/data")).unwrap();
3434
3435 let m = router.match_path("/data//", Method::Get);
3437 assert!(m.is_some()); let m = router.match_path("/data///", Method::Get);
3440 assert!(m.is_some()); }
3442
3443 #[test]
3448 fn empty_segment_normalization() {
3449 let mut router = Router::new();
3450 router.add(route(Method::Get, "/a/b/c")).unwrap();
3451
3452 let paths = vec!["/a//b/c", "/a/b//c", "//a/b/c", "/a/b/c//", "//a//b//c//"];
3454
3455 for path in paths {
3456 let m = router.match_path(path, Method::Get);
3457 assert!(m.is_some(), "Failed to match normalized path: {}", path);
3458 assert_eq!(m.unwrap().route.path, "/a/b/c");
3459 }
3460 }
3461
3462 #[test]
3463 fn empty_segment_in_middle_of_params() {
3464 let mut router = Router::new();
3465 router.add(route(Method::Get, "/a/{x}/b/{y}")).unwrap();
3466
3467 let m = router.match_path("/a//1/b/2", Method::Get);
3469 if let Some(m) = m {
3473 assert!(!m.params.is_empty());
3474 }
3475 }
3476
3477 #[test]
3478 fn only_slashes_path() {
3479 let mut router = Router::new();
3480 router.add(route(Method::Get, "/")).unwrap();
3481
3482 let paths = vec!["/", "//", "///", "////"];
3484 for path in paths {
3485 let m = router.match_path(path, Method::Get);
3486 assert!(m.is_some(), "Failed to match root with: {}", path);
3487 }
3488 }
3489
3490 #[test]
3491 fn empty_path_handling() {
3492 let mut router = Router::new();
3493 router.add(route(Method::Get, "/")).unwrap();
3494
3495 let m = router.match_path("", Method::Get);
3497 let _matched = m.is_some();
3500 }
3501
3502 #[test]
3507 fn deep_nesting_50_levels() {
3508 let mut router = Router::new();
3509
3510 let path = format!(
3512 "/{}",
3513 (0..50)
3514 .map(|i| format!("l{}", i))
3515 .collect::<Vec<_>>()
3516 .join("/")
3517 );
3518 router.add(route(Method::Get, &path)).unwrap();
3519
3520 let m = router.match_path(&path, Method::Get);
3522 assert!(m.is_some());
3523 assert_eq!(m.unwrap().route.path, path);
3524 }
3525
3526 #[test]
3527 fn deep_nesting_100_levels() {
3528 let mut router = Router::new();
3529
3530 let path = format!(
3532 "/{}",
3533 (0..100)
3534 .map(|i| format!("d{}", i))
3535 .collect::<Vec<_>>()
3536 .join("/")
3537 );
3538 router.add(route(Method::Get, &path)).unwrap();
3539
3540 let m = router.match_path(&path, Method::Get);
3541 assert!(m.is_some());
3542 assert_eq!(m.unwrap().route.path, path);
3543 }
3544
3545 #[test]
3546 fn deep_nesting_with_params_at_various_depths() {
3547 let mut router = Router::new();
3548
3549 let mut segments = vec![];
3551 for i in 0..20 {
3552 if i == 5 || i == 10 || i == 15 {
3553 segments.push(format!("{{p{}}}", i));
3554 } else {
3555 segments.push(format!("s{}", i));
3556 }
3557 }
3558 let path = format!("/{}", segments.join("/"));
3559 router.add(route(Method::Get, &path)).unwrap();
3560
3561 let mut request_segments = vec![];
3563 for i in 0..20 {
3564 if i == 5 || i == 10 || i == 15 {
3565 request_segments.push(format!("val{}", i));
3566 } else {
3567 request_segments.push(format!("s{}", i));
3568 }
3569 }
3570 let request_path = format!("/{}", request_segments.join("/"));
3571
3572 let m = router.match_path(&request_path, Method::Get);
3573 assert!(m.is_some());
3574 let m = m.unwrap();
3575 assert_eq!(m.params.len(), 3);
3576 assert_eq!(m.params[0], ("p5", "val5"));
3577 assert_eq!(m.params[1], ("p10", "val10"));
3578 assert_eq!(m.params[2], ("p15", "val15"));
3579 }
3580
3581 #[test]
3582 fn deep_nesting_with_wildcard_at_end() {
3583 let mut router = Router::new();
3584
3585 let segments: Vec<_> = (0..30).map(|i| format!("x{}", i)).collect();
3587 let prefix = format!("/{}", segments.join("/"));
3588 let path = format!("{}/{{*rest}}", prefix);
3589 router.add(route(Method::Get, &path)).unwrap();
3590
3591 let request_path = format!("{}/a/b/c/d/e", prefix);
3593 let m = router.match_path(&request_path, Method::Get);
3594 assert!(m.is_some());
3595 assert_eq!(m.unwrap().params[0], ("rest", "a/b/c/d/e"));
3596 }
3597
3598 #[test]
3603 fn many_siblings_500_routes() {
3604 let mut router = Router::new();
3605
3606 for i in 0..500 {
3608 router
3609 .add(route(Method::Get, &format!("/api/endpoint{}", i)))
3610 .unwrap();
3611 }
3612
3613 assert_eq!(router.routes().len(), 500);
3614
3615 for i in [0, 50, 100, 250, 499] {
3617 let path = format!("/api/endpoint{}", i);
3618 let m = router.match_path(&path, Method::Get);
3619 assert!(m.is_some(), "Failed to match: {}", path);
3620 assert_eq!(m.unwrap().route.path, path);
3621 }
3622 }
3623
3624 #[test]
3625 fn many_siblings_with_shared_prefix() {
3626 let mut router = Router::new();
3627
3628 for i in 0..200 {
3630 router
3631 .add(route(Method::Get, &format!("/users/user{:04}", i)))
3632 .unwrap();
3633 }
3634
3635 assert_eq!(router.routes().len(), 200);
3636
3637 for i in [0, 50, 100, 150, 199] {
3639 let path = format!("/users/user{:04}", i);
3640 let m = router.match_path(&path, Method::Get);
3641 assert!(m.is_some());
3642 assert_eq!(m.unwrap().route.path, path);
3643 }
3644 }
3645
3646 #[test]
3647 fn many_siblings_mixed_static_and_param() {
3648 let mut router = Router::new();
3649
3650 for i in 0..100 {
3652 router
3653 .add(route(Method::Get, &format!("/items/item{}", i)))
3654 .unwrap();
3655 }
3656
3657 router.add(route(Method::Get, "/items/{id}")).unwrap();
3659
3660 assert_eq!(router.routes().len(), 101);
3661
3662 let m = router.match_path("/items/item50", Method::Get).unwrap();
3664 assert_eq!(m.route.path, "/items/item50");
3665
3666 let m = router.match_path("/items/other", Method::Get).unwrap();
3668 assert_eq!(m.route.path, "/items/{id}");
3669 assert_eq!(m.params[0], ("id", "other"));
3670 }
3671
3672 #[test]
3673 fn many_siblings_different_methods() {
3674 let mut router = Router::new();
3675
3676 let methods = vec![
3678 Method::Get,
3679 Method::Post,
3680 Method::Put,
3681 Method::Delete,
3682 Method::Patch,
3683 ];
3684
3685 for i in 0..50 {
3686 for method in &methods {
3687 router
3688 .add(Route::new(*method, &format!("/resource{}", i)))
3689 .unwrap();
3690 }
3691 }
3692
3693 assert_eq!(router.routes().len(), 250);
3694
3695 let m = router.match_path("/resource25", Method::Get).unwrap();
3697 assert_eq!(m.route.method, Method::Get);
3698
3699 let m = router.match_path("/resource25", Method::Post).unwrap();
3700 assert_eq!(m.route.method, Method::Post);
3701
3702 let m = router.match_path("/resource25", Method::Delete).unwrap();
3703 assert_eq!(m.route.method, Method::Delete);
3704 }
3705
3706 #[test]
3707 fn stress_wide_and_deep() {
3708 let mut router = Router::new();
3709
3710 for a in 0..10 {
3713 for b in 0..10 {
3714 for c in 0..10 {
3715 let path = format!("/a{}/b{}/c{}", a, b, c);
3716 router.add(route(Method::Get, &path)).unwrap();
3717 }
3718 }
3719 }
3720
3721 assert_eq!(router.routes().len(), 1000);
3722
3723 let m = router.match_path("/a0/b0/c0", Method::Get).unwrap();
3725 assert_eq!(m.route.path, "/a0/b0/c0");
3726
3727 let m = router.match_path("/a5/b5/c5", Method::Get).unwrap();
3728 assert_eq!(m.route.path, "/a5/b5/c5");
3729
3730 let m = router.match_path("/a9/b9/c9", Method::Get).unwrap();
3731 assert_eq!(m.route.path, "/a9/b9/c9");
3732
3733 assert!(router.match_path("/a10/b0/c0", Method::Get).is_none());
3735 assert!(router.match_path("/a0/b10/c0", Method::Get).is_none());
3736 }
3737
3738 #[test]
3743 fn unicode_emoji_in_path() {
3744 let mut router = Router::new();
3745 router.add(route(Method::Get, "/emoji/🎉")).unwrap();
3746
3747 let m = router.match_path("/emoji/🎉", Method::Get);
3748 assert!(m.is_some());
3749 assert_eq!(m.unwrap().route.path, "/emoji/🎉");
3750 }
3751
3752 #[test]
3753 fn unicode_rtl_characters() {
3754 let mut router = Router::new();
3755 router.add(route(Method::Get, "/greet/مرحبا")).unwrap();
3757
3758 let m = router.match_path("/greet/مرحبا", Method::Get);
3759 assert!(m.is_some());
3760 assert_eq!(m.unwrap().route.path, "/greet/مرحبا");
3761 }
3762
3763 #[test]
3764 fn unicode_mixed_scripts() {
3765 let mut router = Router::new();
3766 router
3768 .add(route(Method::Get, "/mix/hello世界Привет"))
3769 .unwrap();
3770
3771 let m = router.match_path("/mix/hello世界Привет", Method::Get);
3772 assert!(m.is_some());
3773 }
3774
3775 #[test]
3776 fn unicode_normalization_awareness() {
3777 let mut router = Router::new();
3778 router.add(route(Method::Get, "/café")).unwrap();
3780
3781 let m = router.match_path("/café", Method::Get);
3783 assert!(m.is_some());
3784
3785 }
3788
3789 #[test]
3790 fn unicode_in_param_with_converter() {
3791 let mut router = Router::new();
3792 router.add(route(Method::Get, "/data/{value:str}")).unwrap();
3793
3794 let m = router.match_path("/data/日本語テスト", Method::Get);
3796 assert!(m.is_some());
3797 assert_eq!(m.unwrap().params[0], ("value", "日本語テスト"));
3798 }
3799
3800 #[test]
3805 fn int_converter_overflow() {
3806 let mut router = Router::new();
3807 router.add(route(Method::Get, "/id/{num:int}")).unwrap();
3808
3809 let overflow = "99999999999999999999999999999";
3811 let path = format!("/id/{}", overflow);
3812 let m = router.match_path(&path, Method::Get);
3813 assert!(m.is_none());
3814 }
3815
3816 #[test]
3817 fn float_converter_very_small() {
3818 let mut router = Router::new();
3819 router.add(route(Method::Get, "/val/{v:float}")).unwrap();
3820
3821 let m = router.match_path("/val/1e-308", Method::Get);
3823 assert!(m.is_some());
3824 }
3825
3826 #[test]
3827 fn float_converter_very_large() {
3828 let mut router = Router::new();
3829 router.add(route(Method::Get, "/val/{v:float}")).unwrap();
3830
3831 let m = router.match_path("/val/1e308", Method::Get);
3833 assert!(m.is_some());
3834 }
3835
3836 #[test]
3837 fn uuid_converter_nil_uuid() {
3838 let mut router = Router::new();
3839 router.add(route(Method::Get, "/obj/{id:uuid}")).unwrap();
3840
3841 let m = router.match_path("/obj/00000000-0000-0000-0000-000000000000", Method::Get);
3843 assert!(m.is_some());
3844 }
3845
3846 #[test]
3847 fn uuid_converter_max_uuid() {
3848 let mut router = Router::new();
3849 router.add(route(Method::Get, "/obj/{id:uuid}")).unwrap();
3850
3851 let m = router.match_path("/obj/ffffffff-ffff-ffff-ffff-ffffffffffff", Method::Get);
3853 assert!(m.is_some());
3854 }
3855
3856 #[test]
3861 fn path_with_dots() {
3862 let mut router = Router::new();
3863 router.add(route(Method::Get, "/files/{name}")).unwrap();
3864
3865 let m = router.match_path("/files/file.name.ext", Method::Get);
3867 assert!(m.is_some());
3868 assert_eq!(m.unwrap().params[0], ("name", "file.name.ext"));
3869 }
3870
3871 #[test]
3872 fn path_with_only_special_chars() {
3873 let mut router = Router::new();
3874 router.add(route(Method::Get, "/data/{val}")).unwrap();
3875
3876 let m = router.match_path("/data/-._~", Method::Get);
3878 assert!(m.is_some());
3879 assert_eq!(m.unwrap().params[0], ("val", "-._~"));
3880 }
3881
3882 #[test]
3883 fn path_segment_with_colon() {
3884 let mut router = Router::new();
3885 router.add(route(Method::Get, "/time/{val}")).unwrap();
3886
3887 let m = router.match_path("/time/12:30:45", Method::Get);
3889 assert!(m.is_some());
3890 assert_eq!(m.unwrap().params[0], ("val", "12:30:45"));
3891 }
3892
3893 #[test]
3894 fn path_segment_with_at_sign() {
3895 let mut router = Router::new();
3896 router.add(route(Method::Get, "/user/{handle}")).unwrap();
3897
3898 let m = router.match_path("/user/@username", Method::Get);
3900 assert!(m.is_some());
3901 assert_eq!(m.unwrap().params[0], ("handle", "@username"));
3902 }
3903
3904 #[test]
3905 fn very_long_segment() {
3906 let mut router = Router::new();
3907 router.add(route(Method::Get, "/data/{val}")).unwrap();
3908
3909 let long_val: String = (0..1000).map(|_| 'x').collect();
3911 let path = format!("/data/{}", long_val);
3912
3913 let m = router.match_path(&path, Method::Get);
3914 assert!(m.is_some());
3915 assert_eq!(m.unwrap().params[0].1.len(), 1000);
3916 }
3917
3918 #[test]
3919 fn very_long_path_total() {
3920 let mut router = Router::new();
3921
3922 let segments: Vec<_> = (0..500).map(|i| format!("s{}", i)).collect();
3924 let path = format!("/{}", segments.join("/"));
3925 router.add(route(Method::Get, &path)).unwrap();
3926
3927 let m = router.match_path(&path, Method::Get);
3928 assert!(m.is_some());
3929 }
3930}