1use crate::handler::{into_boxed_handler, BoxedHandler, Handler};
45use crate::path_params::PathParams;
46use crate::typed_path::TypedPath;
47use http::{Extensions, Method};
48use matchit::Router as MatchitRouter;
49use rustapi_openapi::Operation;
50use std::collections::HashMap;
51use std::sync::Arc;
52
53#[derive(Debug, Clone)]
55pub struct RouteInfo {
56 pub path: String,
58 pub methods: Vec<Method>,
60}
61
62#[derive(Debug, Clone)]
64pub struct RouteConflictError {
65 pub new_path: String,
67 pub method: Option<Method>,
69 pub existing_path: String,
71 pub details: String,
73}
74
75impl std::fmt::Display for RouteConflictError {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 writeln!(
78 f,
79 "\n╭─────────────────────────────────────────────────────────────╮"
80 )?;
81 writeln!(
82 f,
83 "│ ROUTE CONFLICT DETECTED │"
84 )?;
85 writeln!(
86 f,
87 "╰─────────────────────────────────────────────────────────────╯"
88 )?;
89 writeln!(f)?;
90 writeln!(f, " Conflicting routes:")?;
91 writeln!(f, " → Existing: {}", self.existing_path)?;
92 writeln!(f, " → New: {}", self.new_path)?;
93 writeln!(f)?;
94 if let Some(ref method) = self.method {
95 writeln!(f, " HTTP Method: {}", method)?;
96 writeln!(f)?;
97 }
98 writeln!(f, " Details: {}", self.details)?;
99 writeln!(f)?;
100 writeln!(f, " How to resolve:")?;
101 writeln!(f, " 1. Use different path patterns for each route")?;
102 writeln!(
103 f,
104 " 2. If paths must be similar, ensure parameter names differ"
105 )?;
106 writeln!(
107 f,
108 " 3. Consider using different HTTP methods if appropriate"
109 )?;
110 writeln!(f)?;
111 writeln!(f, " Example:")?;
112 writeln!(f, " Instead of:")?;
113 writeln!(f, " .route(\"/users/{{id}}\", get(handler1))")?;
114 writeln!(f, " .route(\"/users/{{user_id}}\", get(handler2))")?;
115 writeln!(f)?;
116 writeln!(f, " Use:")?;
117 writeln!(f, " .route(\"/users/{{id}}\", get(handler1))")?;
118 writeln!(f, " .route(\"/users/{{id}}/profile\", get(handler2))")?;
119 Ok(())
120 }
121}
122
123impl std::error::Error for RouteConflictError {}
124
125pub struct MethodRouter {
127 handlers: HashMap<Method, BoxedHandler>,
128 pub(crate) operations: HashMap<Method, Operation>,
129 pub(crate) component_registrars: Vec<fn(&mut rustapi_openapi::OpenApiSpec)>,
130}
131
132impl Clone for MethodRouter {
133 fn clone(&self) -> Self {
134 Self {
135 handlers: self.handlers.clone(),
136 operations: self.operations.clone(),
137 component_registrars: self.component_registrars.clone(),
138 }
139 }
140}
141
142impl MethodRouter {
143 pub fn new() -> Self {
145 Self {
146 handlers: HashMap::new(),
147 operations: HashMap::new(),
148 component_registrars: Vec::new(),
149 }
150 }
151
152 fn on(
154 mut self,
155 method: Method,
156 handler: BoxedHandler,
157 operation: Operation,
158 component_registrar: fn(&mut rustapi_openapi::OpenApiSpec),
159 ) -> Self {
160 self.handlers.insert(method.clone(), handler);
161 self.operations.insert(method, operation);
162 self.component_registrars.push(component_registrar);
163 self
164 }
165
166 pub(crate) fn get_handler(&self, method: &Method) -> Option<&BoxedHandler> {
168 self.handlers.get(method)
169 }
170
171 pub(crate) fn allowed_methods(&self) -> Vec<Method> {
173 self.handlers.keys().cloned().collect()
174 }
175
176 pub(crate) fn from_boxed(handlers: HashMap<Method, BoxedHandler>) -> Self {
178 Self {
179 handlers,
180 operations: HashMap::new(), component_registrars: Vec::new(),
182 }
183 }
184
185 pub(crate) fn insert_boxed_with_operation(
189 &mut self,
190 method: Method,
191 handler: BoxedHandler,
192 operation: Operation,
193 component_registrar: fn(&mut rustapi_openapi::OpenApiSpec),
194 ) {
195 if self.handlers.contains_key(&method) {
196 panic!(
197 "Duplicate handler for method {} on the same path",
198 method.as_str()
199 );
200 }
201
202 self.handlers.insert(method.clone(), handler);
203 self.operations.insert(method, operation);
204 self.component_registrars.push(component_registrar);
205 }
206
207 pub fn get<H, T>(self, handler: H) -> Self
209 where
210 H: Handler<T>,
211 T: 'static,
212 {
213 let mut op = Operation::new();
214 H::update_operation(&mut op);
215 self.on(
216 Method::GET,
217 into_boxed_handler(handler),
218 op,
219 <H as Handler<T>>::register_components,
220 )
221 }
222
223 pub fn post<H, T>(self, handler: H) -> Self
225 where
226 H: Handler<T>,
227 T: 'static,
228 {
229 let mut op = Operation::new();
230 H::update_operation(&mut op);
231 self.on(
232 Method::POST,
233 into_boxed_handler(handler),
234 op,
235 <H as Handler<T>>::register_components,
236 )
237 }
238
239 pub fn put<H, T>(self, handler: H) -> Self
241 where
242 H: Handler<T>,
243 T: 'static,
244 {
245 let mut op = Operation::new();
246 H::update_operation(&mut op);
247 self.on(
248 Method::PUT,
249 into_boxed_handler(handler),
250 op,
251 <H as Handler<T>>::register_components,
252 )
253 }
254
255 pub fn patch<H, T>(self, handler: H) -> Self
257 where
258 H: Handler<T>,
259 T: 'static,
260 {
261 let mut op = Operation::new();
262 H::update_operation(&mut op);
263 self.on(
264 Method::PATCH,
265 into_boxed_handler(handler),
266 op,
267 <H as Handler<T>>::register_components,
268 )
269 }
270
271 pub fn delete<H, T>(self, handler: H) -> Self
273 where
274 H: Handler<T>,
275 T: 'static,
276 {
277 let mut op = Operation::new();
278 H::update_operation(&mut op);
279 self.on(
280 Method::DELETE,
281 into_boxed_handler(handler),
282 op,
283 <H as Handler<T>>::register_components,
284 )
285 }
286}
287
288impl Default for MethodRouter {
289 fn default() -> Self {
290 Self::new()
291 }
292}
293
294pub fn get<H, T>(handler: H) -> MethodRouter
296where
297 H: Handler<T>,
298 T: 'static,
299{
300 let mut op = Operation::new();
301 H::update_operation(&mut op);
302 MethodRouter::new().on(
303 Method::GET,
304 into_boxed_handler(handler),
305 op,
306 <H as Handler<T>>::register_components,
307 )
308}
309
310pub fn post<H, T>(handler: H) -> MethodRouter
312where
313 H: Handler<T>,
314 T: 'static,
315{
316 let mut op = Operation::new();
317 H::update_operation(&mut op);
318 MethodRouter::new().on(
319 Method::POST,
320 into_boxed_handler(handler),
321 op,
322 <H as Handler<T>>::register_components,
323 )
324}
325
326pub fn put<H, T>(handler: H) -> MethodRouter
328where
329 H: Handler<T>,
330 T: 'static,
331{
332 let mut op = Operation::new();
333 H::update_operation(&mut op);
334 MethodRouter::new().on(
335 Method::PUT,
336 into_boxed_handler(handler),
337 op,
338 <H as Handler<T>>::register_components,
339 )
340}
341
342pub fn patch<H, T>(handler: H) -> MethodRouter
344where
345 H: Handler<T>,
346 T: 'static,
347{
348 let mut op = Operation::new();
349 H::update_operation(&mut op);
350 MethodRouter::new().on(
351 Method::PATCH,
352 into_boxed_handler(handler),
353 op,
354 <H as Handler<T>>::register_components,
355 )
356}
357
358pub fn delete<H, T>(handler: H) -> MethodRouter
360where
361 H: Handler<T>,
362 T: 'static,
363{
364 let mut op = Operation::new();
365 H::update_operation(&mut op);
366 MethodRouter::new().on(
367 Method::DELETE,
368 into_boxed_handler(handler),
369 op,
370 <H as Handler<T>>::register_components,
371 )
372}
373
374#[derive(Clone)]
376pub struct Router {
377 inner: MatchitRouter<MethodRouter>,
378 state: Arc<Extensions>,
379 registered_routes: HashMap<String, RouteInfo>,
381 method_routers: HashMap<String, MethodRouter>,
383 state_type_ids: Vec<std::any::TypeId>,
386}
387
388impl Router {
389 pub fn new() -> Self {
391 Self {
392 inner: MatchitRouter::new(),
393 state: Arc::new(Extensions::new()),
394 registered_routes: HashMap::new(),
395 method_routers: HashMap::new(),
396 state_type_ids: Vec::new(),
397 }
398 }
399
400 pub fn typed<P: TypedPath>(self, method_router: MethodRouter) -> Self {
402 self.route(P::PATH, method_router)
403 }
404
405 pub fn route(mut self, path: &str, method_router: MethodRouter) -> Self {
407 let matchit_path = convert_path_params(path);
409
410 let methods: Vec<Method> = method_router.handlers.keys().cloned().collect();
412
413 self.method_routers
415 .insert(matchit_path.clone(), method_router.clone());
416
417 match self.inner.insert(matchit_path.clone(), method_router) {
418 Ok(_) => {
419 self.registered_routes.insert(
421 matchit_path.clone(),
422 RouteInfo {
423 path: path.to_string(),
424 methods,
425 },
426 );
427 }
428 Err(e) => {
429 self.method_routers.remove(&matchit_path);
431
432 let existing_path = self
434 .find_conflicting_route(&matchit_path)
435 .map(|info| info.path.clone())
436 .unwrap_or_else(|| "<unknown>".to_string());
437
438 let conflict_error = RouteConflictError {
439 new_path: path.to_string(),
440 method: methods.first().cloned(),
441 existing_path,
442 details: e.to_string(),
443 };
444
445 panic!("{}", conflict_error);
446 }
447 }
448 self
449 }
450
451 fn find_conflicting_route(&self, matchit_path: &str) -> Option<&RouteInfo> {
453 if let Some(info) = self.registered_routes.get(matchit_path) {
455 return Some(info);
456 }
457
458 let normalized_new = normalize_path_for_comparison(matchit_path);
460
461 for (registered_path, info) in &self.registered_routes {
462 let normalized_existing = normalize_path_for_comparison(registered_path);
463 if normalized_new == normalized_existing {
464 return Some(info);
465 }
466 }
467
468 None
469 }
470
471 pub fn state<S: Clone + Send + Sync + 'static>(mut self, state: S) -> Self {
473 let type_id = std::any::TypeId::of::<S>();
474 let extensions = Arc::make_mut(&mut self.state);
475 extensions.insert(state);
476 if !self.state_type_ids.contains(&type_id) {
477 self.state_type_ids.push(type_id);
478 }
479 self
480 }
481
482 pub fn has_state<S: 'static>(&self) -> bool {
484 self.state_type_ids.contains(&std::any::TypeId::of::<S>())
485 }
486
487 pub fn state_type_ids(&self) -> &[std::any::TypeId] {
489 &self.state_type_ids
490 }
491
492 pub fn nest(mut self, prefix: &str, router: Router) -> Self {
560 let normalized_prefix = normalize_prefix(prefix);
562
563 for type_id in &router.state_type_ids {
567 if !self.state_type_ids.contains(type_id) {
568 self.state_type_ids.push(*type_id);
569 }
570 }
571
572 let nested_routes: Vec<(String, RouteInfo, MethodRouter)> = router
575 .registered_routes
576 .into_iter()
577 .filter_map(|(matchit_path, route_info)| {
578 router
579 .method_routers
580 .get(&matchit_path)
581 .map(|mr| (matchit_path, route_info, mr.clone()))
582 })
583 .collect();
584
585 for (matchit_path, route_info, method_router) in nested_routes {
587 let prefixed_matchit_path = if matchit_path == "/" {
591 normalized_prefix.clone()
592 } else {
593 format!("{}{}", normalized_prefix, matchit_path)
594 };
595
596 let prefixed_display_path = if route_info.path == "/" {
597 normalized_prefix.clone()
598 } else {
599 format!("{}{}", normalized_prefix, route_info.path)
600 };
601
602 self.method_routers
604 .insert(prefixed_matchit_path.clone(), method_router.clone());
605
606 match self
608 .inner
609 .insert(prefixed_matchit_path.clone(), method_router)
610 {
611 Ok(_) => {
612 self.registered_routes.insert(
614 prefixed_matchit_path,
615 RouteInfo {
616 path: prefixed_display_path,
617 methods: route_info.methods,
618 },
619 );
620 }
621 Err(e) => {
622 self.method_routers.remove(&prefixed_matchit_path);
624
625 let existing_path = self
627 .find_conflicting_route(&prefixed_matchit_path)
628 .map(|info| info.path.clone())
629 .unwrap_or_else(|| "<unknown>".to_string());
630
631 let conflict_error = RouteConflictError {
632 new_path: prefixed_display_path,
633 method: route_info.methods.first().cloned(),
634 existing_path,
635 details: e.to_string(),
636 };
637
638 panic!("{}", conflict_error);
639 }
640 }
641 }
642
643 self
644 }
645
646 pub fn merge_state<S: Clone + Send + Sync + 'static>(mut self, other: &Router) -> Self {
663 let type_id = std::any::TypeId::of::<S>();
664
665 if !self.state_type_ids.contains(&type_id) {
667 if let Some(state) = other.state.get::<S>() {
669 let extensions = Arc::make_mut(&mut self.state);
670 extensions.insert(state.clone());
671 self.state_type_ids.push(type_id);
672 }
673 }
674
675 self
676 }
677
678 pub fn match_route(&self, path: &str, method: &Method) -> RouteMatch<'_> {
680 match self.inner.at(path) {
681 Ok(matched) => {
682 let method_router = matched.value;
683
684 if let Some(handler) = method_router.get_handler(method) {
685 let params: PathParams = matched
687 .params
688 .iter()
689 .map(|(k, v)| (k.to_string(), v.to_string()))
690 .collect();
691
692 RouteMatch::Found { handler, params }
693 } else {
694 RouteMatch::MethodNotAllowed {
695 allowed: method_router.allowed_methods(),
696 }
697 }
698 }
699 Err(_) => RouteMatch::NotFound,
700 }
701 }
702
703 pub fn state_ref(&self) -> Arc<Extensions> {
705 self.state.clone()
706 }
707
708 pub fn registered_routes(&self) -> &HashMap<String, RouteInfo> {
710 &self.registered_routes
711 }
712
713 pub fn method_routers(&self) -> &HashMap<String, MethodRouter> {
715 &self.method_routers
716 }
717}
718
719impl Default for Router {
720 fn default() -> Self {
721 Self::new()
722 }
723}
724
725pub enum RouteMatch<'a> {
727 Found {
728 handler: &'a BoxedHandler,
729 params: PathParams,
730 },
731 NotFound,
732 MethodNotAllowed {
733 allowed: Vec<Method>,
734 },
735}
736
737fn convert_path_params(path: &str) -> String {
739 let mut result = String::with_capacity(path.len());
740
741 for ch in path.chars() {
742 match ch {
743 '{' => {
744 result.push(':');
745 }
746 '}' => {
747 }
749 _ => {
750 result.push(ch);
751 }
752 }
753 }
754
755 result
756}
757
758fn normalize_path_for_comparison(path: &str) -> String {
760 let mut result = String::with_capacity(path.len());
761 let mut in_param = false;
762
763 for ch in path.chars() {
764 match ch {
765 ':' => {
766 in_param = true;
767 result.push_str(":_");
768 }
769 '/' => {
770 in_param = false;
771 result.push('/');
772 }
773 _ if in_param => {
774 }
776 _ => {
777 result.push(ch);
778 }
779 }
780 }
781
782 result
783}
784
785pub(crate) fn normalize_prefix(prefix: &str) -> String {
802 if prefix.is_empty() {
804 return "/".to_string();
805 }
806
807 let segments: Vec<&str> = prefix.split('/').filter(|s| !s.is_empty()).collect();
809
810 if segments.is_empty() {
812 return "/".to_string();
813 }
814
815 let mut result = String::with_capacity(prefix.len() + 1);
817 for segment in segments {
818 result.push('/');
819 result.push_str(segment);
820 }
821
822 result
823}
824
825#[cfg(test)]
826mod tests {
827 use super::*;
828
829 #[test]
830 fn test_convert_path_params() {
831 assert_eq!(convert_path_params("/users/{id}"), "/users/:id");
832 assert_eq!(
833 convert_path_params("/users/{user_id}/posts/{post_id}"),
834 "/users/:user_id/posts/:post_id"
835 );
836 assert_eq!(convert_path_params("/static/path"), "/static/path");
837 }
838
839 #[test]
840 fn test_normalize_path_for_comparison() {
841 assert_eq!(normalize_path_for_comparison("/users/:id"), "/users/:_");
842 assert_eq!(
843 normalize_path_for_comparison("/users/:user_id"),
844 "/users/:_"
845 );
846 assert_eq!(
847 normalize_path_for_comparison("/users/:id/posts/:post_id"),
848 "/users/:_/posts/:_"
849 );
850 assert_eq!(
851 normalize_path_for_comparison("/static/path"),
852 "/static/path"
853 );
854 }
855
856 #[test]
857 fn test_normalize_prefix() {
858 assert_eq!(normalize_prefix("api"), "/api");
860 assert_eq!(normalize_prefix("/api"), "/api");
861 assert_eq!(normalize_prefix("/api/"), "/api");
862 assert_eq!(normalize_prefix("api/"), "/api");
863
864 assert_eq!(normalize_prefix("api/v1"), "/api/v1");
866 assert_eq!(normalize_prefix("/api/v1"), "/api/v1");
867 assert_eq!(normalize_prefix("/api/v1/"), "/api/v1");
868
869 assert_eq!(normalize_prefix(""), "/");
871 assert_eq!(normalize_prefix("/"), "/");
872
873 assert_eq!(normalize_prefix("//api"), "/api");
875 assert_eq!(normalize_prefix("api//v1"), "/api/v1");
876 assert_eq!(normalize_prefix("//api//v1//"), "/api/v1");
877 assert_eq!(normalize_prefix("///"), "/");
878 }
879
880 #[test]
881 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
882 fn test_route_conflict_detection() {
883 async fn handler1() -> &'static str {
884 "handler1"
885 }
886 async fn handler2() -> &'static str {
887 "handler2"
888 }
889
890 let _router = Router::new()
891 .route("/users/{id}", get(handler1))
892 .route("/users/{user_id}", get(handler2)); }
894
895 #[test]
896 fn test_no_conflict_different_paths() {
897 async fn handler1() -> &'static str {
898 "handler1"
899 }
900 async fn handler2() -> &'static str {
901 "handler2"
902 }
903
904 let router = Router::new()
905 .route("/users/{id}", get(handler1))
906 .route("/users/{id}/profile", get(handler2));
907
908 assert_eq!(router.registered_routes().len(), 2);
909 }
910
911 #[test]
912 fn test_route_info_tracking() {
913 async fn handler() -> &'static str {
914 "handler"
915 }
916
917 let router = Router::new().route("/users/{id}", get(handler));
918
919 let routes = router.registered_routes();
920 assert_eq!(routes.len(), 1);
921
922 let info = routes.get("/users/:id").unwrap();
923 assert_eq!(info.path, "/users/{id}");
924 assert_eq!(info.methods.len(), 1);
925 assert_eq!(info.methods[0], Method::GET);
926 }
927
928 #[test]
929 fn test_basic_router_nesting() {
930 async fn list_users() -> &'static str {
931 "list users"
932 }
933 async fn get_user() -> &'static str {
934 "get user"
935 }
936
937 let users_router = Router::new()
938 .route("/", get(list_users))
939 .route("/{id}", get(get_user));
940
941 let app = Router::new().nest("/api/users", users_router);
942
943 let routes = app.registered_routes();
944 assert_eq!(routes.len(), 2);
945
946 assert!(routes.contains_key("/api/users"));
948 assert!(routes.contains_key("/api/users/:id"));
949
950 let list_info = routes.get("/api/users").unwrap();
952 assert_eq!(list_info.path, "/api/users");
953
954 let get_info = routes.get("/api/users/:id").unwrap();
955 assert_eq!(get_info.path, "/api/users/{id}");
956 }
957
958 #[test]
959 fn test_nested_route_matching() {
960 async fn handler() -> &'static str {
961 "handler"
962 }
963
964 let users_router = Router::new().route("/{id}", get(handler));
965
966 let app = Router::new().nest("/api/users", users_router);
967
968 match app.match_route("/api/users/123", &Method::GET) {
970 RouteMatch::Found { params, .. } => {
971 assert_eq!(params.get("id"), Some(&"123".to_string()));
972 }
973 _ => panic!("Route should be found"),
974 }
975 }
976
977 #[test]
978 fn test_nested_route_matching_multiple_params() {
979 async fn handler() -> &'static str {
980 "handler"
981 }
982
983 let posts_router = Router::new().route("/{user_id}/posts/{post_id}", get(handler));
984
985 let app = Router::new().nest("/api", posts_router);
986
987 match app.match_route("/api/42/posts/100", &Method::GET) {
989 RouteMatch::Found { params, .. } => {
990 assert_eq!(params.get("user_id"), Some(&"42".to_string()));
991 assert_eq!(params.get("post_id"), Some(&"100".to_string()));
992 }
993 _ => panic!("Route should be found"),
994 }
995 }
996
997 #[test]
998 fn test_nested_route_matching_static_path() {
999 async fn handler() -> &'static str {
1000 "handler"
1001 }
1002
1003 let health_router = Router::new().route("/health", get(handler));
1004
1005 let app = Router::new().nest("/api/v1", health_router);
1006
1007 match app.match_route("/api/v1/health", &Method::GET) {
1009 RouteMatch::Found { params, .. } => {
1010 assert!(params.is_empty(), "Static path should have no params");
1011 }
1012 _ => panic!("Route should be found"),
1013 }
1014 }
1015
1016 #[test]
1017 fn test_nested_route_not_found() {
1018 async fn handler() -> &'static str {
1019 "handler"
1020 }
1021
1022 let users_router = Router::new().route("/users", get(handler));
1023
1024 let app = Router::new().nest("/api", users_router);
1025
1026 match app.match_route("/api/posts", &Method::GET) {
1028 RouteMatch::NotFound => {
1029 }
1031 _ => panic!("Route should not be found"),
1032 }
1033
1034 match app.match_route("/v2/users", &Method::GET) {
1036 RouteMatch::NotFound => {
1037 }
1039 _ => panic!("Route with wrong prefix should not be found"),
1040 }
1041 }
1042
1043 #[test]
1044 fn test_nested_route_method_not_allowed() {
1045 async fn handler() -> &'static str {
1046 "handler"
1047 }
1048
1049 let users_router = Router::new().route("/users", get(handler));
1050
1051 let app = Router::new().nest("/api", users_router);
1052
1053 match app.match_route("/api/users", &Method::POST) {
1055 RouteMatch::MethodNotAllowed { allowed } => {
1056 assert!(allowed.contains(&Method::GET));
1057 assert!(!allowed.contains(&Method::POST));
1058 }
1059 _ => panic!("Should return MethodNotAllowed"),
1060 }
1061 }
1062
1063 #[test]
1064 fn test_nested_route_multiple_methods() {
1065 async fn get_handler() -> &'static str {
1066 "get"
1067 }
1068 async fn post_handler() -> &'static str {
1069 "post"
1070 }
1071
1072 let get_router = get(get_handler);
1074 let post_router = post(post_handler);
1075 let mut combined = MethodRouter::new();
1076 for (method, handler) in get_router.handlers {
1077 combined.handlers.insert(method, handler);
1078 }
1079 for (method, handler) in post_router.handlers {
1080 combined.handlers.insert(method, handler);
1081 }
1082
1083 let users_router = Router::new().route("/users", combined);
1084 let app = Router::new().nest("/api", users_router);
1085
1086 match app.match_route("/api/users", &Method::GET) {
1088 RouteMatch::Found { .. } => {}
1089 _ => panic!("GET should be found"),
1090 }
1091
1092 match app.match_route("/api/users", &Method::POST) {
1093 RouteMatch::Found { .. } => {}
1094 _ => panic!("POST should be found"),
1095 }
1096
1097 match app.match_route("/api/users", &Method::DELETE) {
1099 RouteMatch::MethodNotAllowed { allowed } => {
1100 assert!(allowed.contains(&Method::GET));
1101 assert!(allowed.contains(&Method::POST));
1102 }
1103 _ => panic!("DELETE should return MethodNotAllowed"),
1104 }
1105 }
1106
1107 #[test]
1108 fn test_nested_router_prefix_normalization() {
1109 async fn handler() -> &'static str {
1110 "handler"
1111 }
1112
1113 let router1 = Router::new().route("/test", get(handler));
1115 let app1 = Router::new().nest("api", router1);
1116 assert!(app1.registered_routes().contains_key("/api/test"));
1117
1118 let router2 = Router::new().route("/test", get(handler));
1119 let app2 = Router::new().nest("/api/", router2);
1120 assert!(app2.registered_routes().contains_key("/api/test"));
1121
1122 let router3 = Router::new().route("/test", get(handler));
1123 let app3 = Router::new().nest("//api//", router3);
1124 assert!(app3.registered_routes().contains_key("/api/test"));
1125 }
1126
1127 #[test]
1128 fn test_state_tracking() {
1129 #[derive(Clone)]
1130 struct MyState(#[allow(dead_code)] String);
1131
1132 let router = Router::new().state(MyState("test".to_string()));
1133
1134 assert!(router.has_state::<MyState>());
1135 assert!(!router.has_state::<String>());
1136 }
1137
1138 #[test]
1139 fn test_state_merge_nested_only() {
1140 #[derive(Clone, PartialEq, Debug)]
1141 struct NestedState(String);
1142
1143 async fn handler() -> &'static str {
1144 "handler"
1145 }
1146
1147 let state_source = Router::new().state(NestedState("nested".to_string()));
1149
1150 let nested = Router::new().route("/test", get(handler));
1151
1152 let parent = Router::new()
1153 .nest("/api", nested)
1154 .merge_state::<NestedState>(&state_source);
1155
1156 assert!(parent.has_state::<NestedState>());
1158
1159 let state = parent.state.get::<NestedState>().unwrap();
1161 assert_eq!(state.0, "nested");
1162 }
1163
1164 #[test]
1165 fn test_state_merge_parent_wins() {
1166 #[derive(Clone, PartialEq, Debug)]
1167 struct SharedState(String);
1168
1169 async fn handler() -> &'static str {
1170 "handler"
1171 }
1172
1173 let state_source = Router::new().state(SharedState("nested".to_string()));
1175
1176 let nested = Router::new().route("/test", get(handler));
1177
1178 let parent = Router::new()
1179 .state(SharedState("parent".to_string()))
1180 .nest("/api", nested)
1181 .merge_state::<SharedState>(&state_source);
1182
1183 assert!(parent.has_state::<SharedState>());
1185
1186 let state = parent.state.get::<SharedState>().unwrap();
1188 assert_eq!(state.0, "parent");
1189 }
1190
1191 #[test]
1192 fn test_state_type_ids_merged_on_nest() {
1193 #[derive(Clone)]
1194 struct NestedState(#[allow(dead_code)] String);
1195
1196 async fn handler() -> &'static str {
1197 "handler"
1198 }
1199
1200 let nested = Router::new()
1201 .route("/test", get(handler))
1202 .state(NestedState("nested".to_string()));
1203
1204 let parent = Router::new().nest("/api", nested);
1205
1206 assert!(parent
1208 .state_type_ids()
1209 .contains(&std::any::TypeId::of::<NestedState>()));
1210 }
1211
1212 #[test]
1213 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
1214 fn test_nested_route_conflict_with_existing_route() {
1215 async fn handler1() -> &'static str {
1216 "handler1"
1217 }
1218 async fn handler2() -> &'static str {
1219 "handler2"
1220 }
1221
1222 let parent = Router::new().route("/api/users/{id}", get(handler1));
1224
1225 let nested = Router::new().route("/{user_id}", get(handler2));
1227
1228 let _app = parent.nest("/api/users", nested);
1230 }
1231
1232 #[test]
1233 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
1234 fn test_nested_route_conflict_same_path_different_param_names() {
1235 async fn handler1() -> &'static str {
1236 "handler1"
1237 }
1238 async fn handler2() -> &'static str {
1239 "handler2"
1240 }
1241
1242 let nested1 = Router::new().route("/{id}", get(handler1));
1244 let nested2 = Router::new().route("/{user_id}", get(handler2));
1245
1246 let _app = Router::new()
1248 .nest("/api/users", nested1)
1249 .nest("/api/users", nested2);
1250 }
1251
1252 #[test]
1253 fn test_nested_route_conflict_error_contains_both_paths() {
1254 use std::panic::{catch_unwind, AssertUnwindSafe};
1255
1256 async fn handler1() -> &'static str {
1257 "handler1"
1258 }
1259 async fn handler2() -> &'static str {
1260 "handler2"
1261 }
1262
1263 let result = catch_unwind(AssertUnwindSafe(|| {
1264 let parent = Router::new().route("/api/users/{id}", get(handler1));
1265 let nested = Router::new().route("/{user_id}", get(handler2));
1266 let _app = parent.nest("/api/users", nested);
1267 }));
1268
1269 assert!(result.is_err(), "Should have panicked due to conflict");
1270
1271 if let Err(panic_info) = result {
1272 if let Some(msg) = panic_info.downcast_ref::<String>() {
1273 assert!(
1274 msg.contains("ROUTE CONFLICT DETECTED"),
1275 "Error should contain 'ROUTE CONFLICT DETECTED'"
1276 );
1277 assert!(
1278 msg.contains("Existing:") && msg.contains("New:"),
1279 "Error should contain both 'Existing:' and 'New:' labels"
1280 );
1281 assert!(
1282 msg.contains("How to resolve:"),
1283 "Error should contain resolution guidance"
1284 );
1285 }
1286 }
1287 }
1288
1289 #[test]
1290 fn test_nested_routes_no_conflict_different_prefixes() {
1291 async fn handler1() -> &'static str {
1292 "handler1"
1293 }
1294 async fn handler2() -> &'static str {
1295 "handler2"
1296 }
1297
1298 let nested1 = Router::new().route("/{id}", get(handler1));
1300 let nested2 = Router::new().route("/{id}", get(handler2));
1301
1302 let app = Router::new()
1304 .nest("/api/users", nested1)
1305 .nest("/api/posts", nested2);
1306
1307 assert_eq!(app.registered_routes().len(), 2);
1308 assert!(app.registered_routes().contains_key("/api/users/:id"));
1309 assert!(app.registered_routes().contains_key("/api/posts/:id"));
1310 }
1311
1312 #[test]
1317 fn test_multiple_router_composition_all_routes_registered() {
1318 async fn users_list() -> &'static str {
1319 "users list"
1320 }
1321 async fn users_get() -> &'static str {
1322 "users get"
1323 }
1324 async fn posts_list() -> &'static str {
1325 "posts list"
1326 }
1327 async fn posts_get() -> &'static str {
1328 "posts get"
1329 }
1330 async fn comments_list() -> &'static str {
1331 "comments list"
1332 }
1333
1334 let users_router = Router::new()
1336 .route("/", get(users_list))
1337 .route("/{id}", get(users_get));
1338
1339 let posts_router = Router::new()
1340 .route("/", get(posts_list))
1341 .route("/{id}", get(posts_get));
1342
1343 let comments_router = Router::new().route("/", get(comments_list));
1344
1345 let app = Router::new()
1347 .nest("/api/users", users_router)
1348 .nest("/api/posts", posts_router)
1349 .nest("/api/comments", comments_router);
1350
1351 let routes = app.registered_routes();
1353 assert_eq!(routes.len(), 5, "Should have 5 routes registered");
1354
1355 assert!(
1357 routes.contains_key("/api/users"),
1358 "Should have /api/users route"
1359 );
1360 assert!(
1361 routes.contains_key("/api/users/:id"),
1362 "Should have /api/users/:id route"
1363 );
1364
1365 assert!(
1367 routes.contains_key("/api/posts"),
1368 "Should have /api/posts route"
1369 );
1370 assert!(
1371 routes.contains_key("/api/posts/:id"),
1372 "Should have /api/posts/:id route"
1373 );
1374
1375 assert!(
1377 routes.contains_key("/api/comments"),
1378 "Should have /api/comments route"
1379 );
1380 }
1381
1382 #[test]
1383 fn test_multiple_router_composition_no_interference() {
1384 async fn users_handler() -> &'static str {
1385 "users"
1386 }
1387 async fn posts_handler() -> &'static str {
1388 "posts"
1389 }
1390 async fn admin_handler() -> &'static str {
1391 "admin"
1392 }
1393
1394 let users_router = Router::new()
1396 .route("/list", get(users_handler))
1397 .route("/{id}", get(users_handler));
1398
1399 let posts_router = Router::new()
1400 .route("/list", get(posts_handler))
1401 .route("/{id}", get(posts_handler));
1402
1403 let admin_router = Router::new()
1404 .route("/list", get(admin_handler))
1405 .route("/{id}", get(admin_handler));
1406
1407 let app = Router::new()
1409 .nest("/api/v1/users", users_router)
1410 .nest("/api/v1/posts", posts_router)
1411 .nest("/admin", admin_router);
1412
1413 let routes = app.registered_routes();
1415 assert_eq!(routes.len(), 6, "Should have 6 routes registered");
1416
1417 assert!(routes.contains_key("/api/v1/users/list"));
1419 assert!(routes.contains_key("/api/v1/users/:id"));
1420 assert!(routes.contains_key("/api/v1/posts/list"));
1421 assert!(routes.contains_key("/api/v1/posts/:id"));
1422 assert!(routes.contains_key("/admin/list"));
1423 assert!(routes.contains_key("/admin/:id"));
1424
1425 match app.match_route("/api/v1/users/list", &Method::GET) {
1427 RouteMatch::Found { params, .. } => {
1428 assert!(params.is_empty(), "Static path should have no params");
1429 }
1430 _ => panic!("Should find /api/v1/users/list"),
1431 }
1432
1433 match app.match_route("/api/v1/posts/123", &Method::GET) {
1434 RouteMatch::Found { params, .. } => {
1435 assert_eq!(params.get("id"), Some(&"123".to_string()));
1436 }
1437 _ => panic!("Should find /api/v1/posts/123"),
1438 }
1439
1440 match app.match_route("/admin/456", &Method::GET) {
1441 RouteMatch::Found { params, .. } => {
1442 assert_eq!(params.get("id"), Some(&"456".to_string()));
1443 }
1444 _ => panic!("Should find /admin/456"),
1445 }
1446 }
1447
1448 #[test]
1449 fn test_multiple_router_composition_with_multiple_methods() {
1450 async fn get_handler() -> &'static str {
1451 "get"
1452 }
1453 async fn post_handler() -> &'static str {
1454 "post"
1455 }
1456 async fn put_handler() -> &'static str {
1457 "put"
1458 }
1459
1460 let get_router = get(get_handler);
1463 let post_router = post(post_handler);
1464 let mut users_root_combined = MethodRouter::new();
1465 for (method, handler) in get_router.handlers {
1466 users_root_combined.handlers.insert(method, handler);
1467 }
1468 for (method, handler) in post_router.handlers {
1469 users_root_combined.handlers.insert(method, handler);
1470 }
1471
1472 let get_router2 = get(get_handler);
1474 let put_router = put(put_handler);
1475 let mut users_id_combined = MethodRouter::new();
1476 for (method, handler) in get_router2.handlers {
1477 users_id_combined.handlers.insert(method, handler);
1478 }
1479 for (method, handler) in put_router.handlers {
1480 users_id_combined.handlers.insert(method, handler);
1481 }
1482
1483 let users_router = Router::new()
1484 .route("/", users_root_combined)
1485 .route("/{id}", users_id_combined);
1486
1487 let get_router3 = get(get_handler);
1489 let post_router2 = post(post_handler);
1490 let mut posts_root_combined = MethodRouter::new();
1491 for (method, handler) in get_router3.handlers {
1492 posts_root_combined.handlers.insert(method, handler);
1493 }
1494 for (method, handler) in post_router2.handlers {
1495 posts_root_combined.handlers.insert(method, handler);
1496 }
1497
1498 let posts_router = Router::new().route("/", posts_root_combined);
1499
1500 let app = Router::new()
1502 .nest("/users", users_router)
1503 .nest("/posts", posts_router);
1504
1505 let routes = app.registered_routes();
1507 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1508
1509 let users_root = routes.get("/users").unwrap();
1511 assert!(users_root.methods.contains(&Method::GET));
1512 assert!(users_root.methods.contains(&Method::POST));
1513
1514 let users_id = routes.get("/users/:id").unwrap();
1515 assert!(users_id.methods.contains(&Method::GET));
1516 assert!(users_id.methods.contains(&Method::PUT));
1517
1518 let posts_root = routes.get("/posts").unwrap();
1520 assert!(posts_root.methods.contains(&Method::GET));
1521 assert!(posts_root.methods.contains(&Method::POST));
1522
1523 match app.match_route("/users", &Method::GET) {
1525 RouteMatch::Found { .. } => {}
1526 _ => panic!("GET /users should be found"),
1527 }
1528 match app.match_route("/users", &Method::POST) {
1529 RouteMatch::Found { .. } => {}
1530 _ => panic!("POST /users should be found"),
1531 }
1532 match app.match_route("/users/123", &Method::PUT) {
1533 RouteMatch::Found { .. } => {}
1534 _ => panic!("PUT /users/123 should be found"),
1535 }
1536 }
1537
1538 #[test]
1539 fn test_multiple_router_composition_deep_nesting() {
1540 async fn handler() -> &'static str {
1541 "handler"
1542 }
1543
1544 let deep_router = Router::new().route("/action", get(handler));
1546
1547 let mid_router = Router::new().route("/info", get(handler));
1548
1549 let shallow_router = Router::new().route("/status", get(handler));
1550
1551 let app = Router::new()
1553 .nest("/api/v1/resources/items", deep_router)
1554 .nest("/api/v1/resources", mid_router)
1555 .nest("/api", shallow_router);
1556
1557 let routes = app.registered_routes();
1559 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1560
1561 assert!(routes.contains_key("/api/v1/resources/items/action"));
1562 assert!(routes.contains_key("/api/v1/resources/info"));
1563 assert!(routes.contains_key("/api/status"));
1564
1565 match app.match_route("/api/v1/resources/items/action", &Method::GET) {
1567 RouteMatch::Found { .. } => {}
1568 _ => panic!("Should find deep route"),
1569 }
1570 match app.match_route("/api/v1/resources/info", &Method::GET) {
1571 RouteMatch::Found { .. } => {}
1572 _ => panic!("Should find mid route"),
1573 }
1574 match app.match_route("/api/status", &Method::GET) {
1575 RouteMatch::Found { .. } => {}
1576 _ => panic!("Should find shallow route"),
1577 }
1578 }
1579}
1580
1581#[cfg(test)]
1582mod property_tests {
1583 use super::*;
1584 use proptest::prelude::*;
1585 use std::panic::{catch_unwind, AssertUnwindSafe};
1586
1587 proptest! {
1595 #![proptest_config(ProptestConfig::with_cases(100))]
1596
1597 #[test]
1602 fn prop_normalized_prefix_starts_with_single_slash(
1603 leading_slashes in prop::collection::vec(Just('/'), 0..5),
1605 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 0..4),
1606 trailing_slashes in prop::collection::vec(Just('/'), 0..5),
1607 ) {
1608 let mut prefix = String::new();
1610 for _ in &leading_slashes {
1611 prefix.push('/');
1612 }
1613 for (i, segment) in segments.iter().enumerate() {
1614 if i > 0 {
1615 prefix.push('/');
1616 }
1617 prefix.push_str(segment);
1618 }
1619 for _ in &trailing_slashes {
1620 prefix.push('/');
1621 }
1622
1623 let normalized = normalize_prefix(&prefix);
1624
1625 prop_assert!(
1627 normalized.starts_with('/'),
1628 "Normalized prefix '{}' should start with '/', input was '{}'",
1629 normalized, prefix
1630 );
1631
1632 prop_assert!(
1634 !normalized.starts_with("//"),
1635 "Normalized prefix '{}' should not start with '//', input was '{}'",
1636 normalized, prefix
1637 );
1638 }
1639
1640 #[test]
1645 fn prop_normalized_prefix_no_trailing_slash(
1646 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1647 trailing_slashes in prop::collection::vec(Just('/'), 0..5),
1648 ) {
1649 let mut prefix = String::from("/");
1651 for (i, segment) in segments.iter().enumerate() {
1652 if i > 0 {
1653 prefix.push('/');
1654 }
1655 prefix.push_str(segment);
1656 }
1657 for _ in &trailing_slashes {
1658 prefix.push('/');
1659 }
1660
1661 let normalized = normalize_prefix(&prefix);
1662
1663 prop_assert!(
1665 !normalized.ends_with('/'),
1666 "Normalized prefix '{}' should not end with '/', input was '{}'",
1667 normalized, prefix
1668 );
1669 }
1670
1671 #[test]
1676 fn prop_normalized_prefix_no_double_slashes(
1677 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1679 extra_slashes in prop::collection::vec(0..4usize, 1..4),
1680 ) {
1681 let mut prefix = String::from("/");
1683 for (i, segment) in segments.iter().enumerate() {
1684 if i > 0 {
1685 let num_slashes = extra_slashes.get(i).copied().unwrap_or(1);
1687 for _ in 0..=num_slashes {
1688 prefix.push('/');
1689 }
1690 }
1691 prefix.push_str(segment);
1692 }
1693
1694 let normalized = normalize_prefix(&prefix);
1695
1696 prop_assert!(
1698 !normalized.contains("//"),
1699 "Normalized prefix '{}' should not contain '//', input was '{}'",
1700 normalized, prefix
1701 );
1702 }
1703
1704 #[test]
1709 fn prop_normalized_prefix_preserves_segments(
1710 segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..4),
1711 ) {
1712 let prefix = format!("/{}", segments.join("/"));
1714
1715 let normalized = normalize_prefix(&prefix);
1716
1717 let normalized_segments: Vec<&str> = normalized
1719 .split('/')
1720 .filter(|s| !s.is_empty())
1721 .collect();
1722
1723 prop_assert_eq!(
1724 segments.len(),
1725 normalized_segments.len(),
1726 "Segment count should be preserved"
1727 );
1728
1729 for (original, normalized_seg) in segments.iter().zip(normalized_segments.iter()) {
1730 prop_assert_eq!(
1731 original, normalized_seg,
1732 "Segment content should be preserved"
1733 );
1734 }
1735 }
1736
1737 #[test]
1742 fn prop_empty_or_slashes_normalize_to_root(
1743 num_slashes in 0..10usize,
1744 ) {
1745 let prefix = "/".repeat(num_slashes);
1746
1747 let normalized = normalize_prefix(&prefix);
1748
1749 prop_assert_eq!(
1750 normalized, "/",
1751 "Empty or slash-only prefix '{}' should normalize to '/'",
1752 prefix
1753 );
1754 }
1755 }
1756
1757 proptest! {
1764 #![proptest_config(ProptestConfig::with_cases(100))]
1765
1766 #[test]
1771 fn prop_method_router_clone_preserves_methods(
1772 use_get in any::<bool>(),
1774 use_post in any::<bool>(),
1775 use_put in any::<bool>(),
1776 use_patch in any::<bool>(),
1777 use_delete in any::<bool>(),
1778 ) {
1779 prop_assume!(use_get || use_post || use_put || use_patch || use_delete);
1781
1782 let mut method_router = MethodRouter::new();
1784 let mut expected_methods: Vec<Method> = Vec::new();
1785
1786 async fn handler() -> &'static str { "handler" }
1787
1788 if use_get {
1789 method_router = get(handler);
1790 expected_methods.push(Method::GET);
1791 }
1792
1793 if use_post {
1794 let post_router = post(handler);
1795 for (method, handler) in post_router.handlers {
1796 method_router.handlers.insert(method.clone(), handler);
1797 if !expected_methods.contains(&method) {
1798 expected_methods.push(method);
1799 }
1800 }
1801 }
1802
1803 if use_put {
1804 let put_router = put(handler);
1805 for (method, handler) in put_router.handlers {
1806 method_router.handlers.insert(method.clone(), handler);
1807 if !expected_methods.contains(&method) {
1808 expected_methods.push(method);
1809 }
1810 }
1811 }
1812
1813 if use_patch {
1814 let patch_router = patch(handler);
1815 for (method, handler) in patch_router.handlers {
1816 method_router.handlers.insert(method.clone(), handler);
1817 if !expected_methods.contains(&method) {
1818 expected_methods.push(method);
1819 }
1820 }
1821 }
1822
1823 if use_delete {
1824 let delete_router = delete(handler);
1825 for (method, handler) in delete_router.handlers {
1826 method_router.handlers.insert(method.clone(), handler);
1827 if !expected_methods.contains(&method) {
1828 expected_methods.push(method);
1829 }
1830 }
1831 }
1832
1833 let cloned_router = method_router.clone();
1835
1836 let original_methods = method_router.allowed_methods();
1838 let cloned_methods = cloned_router.allowed_methods();
1839
1840 prop_assert_eq!(
1841 original_methods.len(),
1842 cloned_methods.len(),
1843 "Cloned router should have same number of methods"
1844 );
1845
1846 for method in &expected_methods {
1847 prop_assert!(
1848 cloned_router.get_handler(method).is_some(),
1849 "Cloned router should have handler for method {:?}",
1850 method
1851 );
1852 }
1853
1854 for method in &cloned_methods {
1856 prop_assert!(
1857 cloned_router.get_handler(method).is_some(),
1858 "Handler for {:?} should be accessible after clone",
1859 method
1860 );
1861 }
1862 }
1863 }
1864
1865 proptest! {
1873 #![proptest_config(ProptestConfig::with_cases(100))]
1874
1875 #[test]
1880 fn prop_nested_routes_have_prefix(
1881 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1883 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1885 has_param in any::<bool>(),
1886 ) {
1887 async fn handler() -> &'static str { "handler" }
1888
1889 let prefix = format!("/{}", prefix_segments.join("/"));
1891
1892 let mut route_path = format!("/{}", route_segments.join("/"));
1894 if has_param {
1895 route_path.push_str("/{id}");
1896 }
1897
1898 let nested_router = Router::new().route(&route_path, get(handler));
1900 let app = Router::new().nest(&prefix, nested_router);
1901
1902 let expected_matchit_path = if has_param {
1904 format!("{}/{}/:id", prefix, route_segments.join("/"))
1905 } else {
1906 format!("{}/{}", prefix, route_segments.join("/"))
1907 };
1908
1909 let routes = app.registered_routes();
1910
1911 prop_assert!(
1913 routes.contains_key(&expected_matchit_path),
1914 "Expected route '{}' not found. Available routes: {:?}",
1915 expected_matchit_path,
1916 routes.keys().collect::<Vec<_>>()
1917 );
1918
1919 let route_info = routes.get(&expected_matchit_path).unwrap();
1921 let expected_display_path = format!("{}{}", prefix, route_path);
1922 prop_assert_eq!(
1923 &route_info.path, &expected_display_path,
1924 "Display path should be prefix + original path"
1925 );
1926 }
1927
1928 #[test]
1933 fn prop_route_count_preserved_after_nesting(
1934 num_routes in 1..4usize,
1936 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1937 ) {
1938 async fn handler() -> &'static str { "handler" }
1939
1940 let prefix = format!("/{}", prefix_segments.join("/"));
1941
1942 let mut nested_router = Router::new();
1944 for i in 0..num_routes {
1945 let path = format!("/route{}", i);
1946 nested_router = nested_router.route(&path, get(handler));
1947 }
1948
1949 let app = Router::new().nest(&prefix, nested_router);
1950
1951 prop_assert_eq!(
1952 app.registered_routes().len(),
1953 num_routes,
1954 "Number of routes should be preserved after nesting"
1955 );
1956 }
1957
1958 #[test]
1962 fn prop_nested_routes_are_matchable(
1963 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1964 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1965 ) {
1966 async fn handler() -> &'static str { "handler" }
1967
1968 let prefix = format!("/{}", prefix_segments.join("/"));
1969 let route_path = format!("/{}", route_segments.join("/"));
1970
1971 let nested_router = Router::new().route(&route_path, get(handler));
1972 let app = Router::new().nest(&prefix, nested_router);
1973
1974 let full_path = format!("{}{}", prefix, route_path);
1976
1977 match app.match_route(&full_path, &Method::GET) {
1979 RouteMatch::Found { .. } => {
1980 }
1982 RouteMatch::NotFound => {
1983 prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
1984 }
1985 RouteMatch::MethodNotAllowed { .. } => {
1986 prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
1987 }
1988 }
1989 }
1990 }
1991
1992 proptest! {
1999 #![proptest_config(ProptestConfig::with_cases(100))]
2000
2001 #[test]
2006 fn prop_state_type_ids_merged(
2007 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2008 has_nested_state in any::<bool>(),
2009 ) {
2010 #[derive(Clone)]
2011 struct TestState(#[allow(dead_code)] i32);
2012
2013 async fn handler() -> &'static str { "handler" }
2014
2015 let prefix = format!("/{}", prefix_segments.join("/"));
2016
2017 let mut nested = Router::new().route("/test", get(handler));
2018 if has_nested_state {
2019 nested = nested.state(TestState(42));
2020 }
2021
2022 let parent = Router::new().nest(&prefix, nested);
2023
2024 if has_nested_state {
2026 prop_assert!(
2027 parent.state_type_ids().contains(&std::any::TypeId::of::<TestState>()),
2028 "Parent should track nested state type ID"
2029 );
2030 }
2031 }
2032
2033 #[test]
2038 fn prop_merge_state_adds_nested_state(
2039 state_value in any::<i32>(),
2040 ) {
2041 #[derive(Clone, PartialEq, Debug)]
2042 struct UniqueState(i32);
2043
2044 let source = Router::new().state(UniqueState(state_value));
2046
2047 let parent = Router::new().merge_state::<UniqueState>(&source);
2049
2050 prop_assert!(
2052 parent.has_state::<UniqueState>(),
2053 "Parent should have state after merge"
2054 );
2055
2056 let merged_state = parent.state.get::<UniqueState>().unwrap();
2058 prop_assert_eq!(
2059 merged_state.0, state_value,
2060 "Merged state value should match source"
2061 );
2062 }
2063 }
2064
2065 proptest! {
2072 #![proptest_config(ProptestConfig::with_cases(100))]
2073
2074 #[test]
2079 fn prop_parent_state_takes_precedence(
2080 parent_value in any::<i32>(),
2081 nested_value in any::<i32>(),
2082 ) {
2083 prop_assume!(parent_value != nested_value);
2085
2086 #[derive(Clone, PartialEq, Debug)]
2087 struct SharedState(i32);
2088
2089 let source = Router::new().state(SharedState(nested_value));
2091
2092 let parent = Router::new()
2094 .state(SharedState(parent_value))
2095 .merge_state::<SharedState>(&source);
2096
2097 prop_assert!(
2099 parent.has_state::<SharedState>(),
2100 "Parent should have state"
2101 );
2102
2103 let final_state = parent.state.get::<SharedState>().unwrap();
2105 prop_assert_eq!(
2106 final_state.0, parent_value,
2107 "Parent state value should be preserved, not overwritten by nested"
2108 );
2109 }
2110
2111 #[test]
2116 fn prop_state_precedence_consistent(
2117 parent_value in any::<i32>(),
2118 source1_value in any::<i32>(),
2119 source2_value in any::<i32>(),
2120 ) {
2121 #[derive(Clone, PartialEq, Debug)]
2122 struct ConsistentState(i32);
2123
2124 let source1 = Router::new().state(ConsistentState(source1_value));
2126 let source2 = Router::new().state(ConsistentState(source2_value));
2127
2128 let parent = Router::new()
2130 .state(ConsistentState(parent_value))
2131 .merge_state::<ConsistentState>(&source1)
2132 .merge_state::<ConsistentState>(&source2);
2133
2134 let final_state = parent.state.get::<ConsistentState>().unwrap();
2136 prop_assert_eq!(
2137 final_state.0, parent_value,
2138 "Parent state should be preserved after multiple merges"
2139 );
2140 }
2141 }
2142
2143 proptest! {
2151 #![proptest_config(ProptestConfig::with_cases(100))]
2152
2153 #[test]
2158 fn prop_same_structure_different_param_names_conflict(
2159 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
2161 param1 in "[a-z][a-z0-9]{0,5}",
2163 param2 in "[a-z][a-z0-9]{0,5}",
2164 ) {
2165 prop_assume!(param1 != param2);
2167
2168 let mut path1 = String::from("/");
2170 let mut path2 = String::from("/");
2171
2172 for segment in &segments {
2173 path1.push_str(segment);
2174 path1.push('/');
2175 path2.push_str(segment);
2176 path2.push('/');
2177 }
2178
2179 path1.push('{');
2180 path1.push_str(¶m1);
2181 path1.push('}');
2182
2183 path2.push('{');
2184 path2.push_str(¶m2);
2185 path2.push('}');
2186
2187 let result = catch_unwind(AssertUnwindSafe(|| {
2189 async fn handler1() -> &'static str { "handler1" }
2190 async fn handler2() -> &'static str { "handler2" }
2191
2192 let _router = Router::new()
2193 .route(&path1, get(handler1))
2194 .route(&path2, get(handler2));
2195 }));
2196
2197 prop_assert!(
2198 result.is_err(),
2199 "Routes '{}' and '{}' should conflict but didn't",
2200 path1, path2
2201 );
2202 }
2203
2204 #[test]
2209 fn prop_different_structures_no_conflict(
2210 segments1 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2212 segments2 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2213 has_param1 in any::<bool>(),
2215 has_param2 in any::<bool>(),
2216 ) {
2217 let mut path1 = String::from("/");
2219 let mut path2 = String::from("/");
2220
2221 for segment in &segments1 {
2222 path1.push_str(segment);
2223 path1.push('/');
2224 }
2225 path1.pop(); for segment in &segments2 {
2228 path2.push_str(segment);
2229 path2.push('/');
2230 }
2231 path2.pop(); if has_param1 {
2234 path1.push_str("/{id}");
2235 }
2236
2237 if has_param2 {
2238 path2.push_str("/{id}");
2239 }
2240
2241 let norm1 = normalize_path_for_comparison(&convert_path_params(&path1));
2243 let norm2 = normalize_path_for_comparison(&convert_path_params(&path2));
2244
2245 prop_assume!(norm1 != norm2);
2247
2248 let result = catch_unwind(AssertUnwindSafe(|| {
2250 async fn handler1() -> &'static str { "handler1" }
2251 async fn handler2() -> &'static str { "handler2" }
2252
2253 let router = Router::new()
2254 .route(&path1, get(handler1))
2255 .route(&path2, get(handler2));
2256
2257 router.registered_routes().len()
2258 }));
2259
2260 prop_assert!(
2261 result.is_ok(),
2262 "Routes '{}' and '{}' should not conflict but did",
2263 path1, path2
2264 );
2265
2266 if let Ok(count) = result {
2267 prop_assert_eq!(count, 2, "Should have registered 2 routes");
2268 }
2269 }
2270
2271 #[test]
2276 fn prop_conflict_error_contains_both_paths(
2277 segment in "[a-z][a-z0-9]{1,5}",
2279 param1 in "[a-z][a-z0-9]{1,5}",
2280 param2 in "[a-z][a-z0-9]{1,5}",
2281 ) {
2282 prop_assume!(param1 != param2);
2283
2284 let path1 = format!("/{}/{{{}}}", segment, param1);
2285 let path2 = format!("/{}/{{{}}}", segment, param2);
2286
2287 let result = catch_unwind(AssertUnwindSafe(|| {
2288 async fn handler1() -> &'static str { "handler1" }
2289 async fn handler2() -> &'static str { "handler2" }
2290
2291 let _router = Router::new()
2292 .route(&path1, get(handler1))
2293 .route(&path2, get(handler2));
2294 }));
2295
2296 prop_assert!(result.is_err(), "Should have panicked due to conflict");
2297
2298 if let Err(panic_info) = result {
2300 if let Some(msg) = panic_info.downcast_ref::<String>() {
2301 prop_assert!(
2302 msg.contains("ROUTE CONFLICT DETECTED"),
2303 "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
2304 msg
2305 );
2306 prop_assert!(
2307 msg.contains("Existing:") && msg.contains("New:"),
2308 "Error should contain both 'Existing:' and 'New:' labels, got: {}",
2309 msg
2310 );
2311 prop_assert!(
2312 msg.contains("How to resolve:"),
2313 "Error should contain resolution guidance, got: {}",
2314 msg
2315 );
2316 }
2317 }
2318 }
2319
2320 #[test]
2324 fn prop_exact_duplicate_paths_conflict(
2325 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
2327 has_param in any::<bool>(),
2328 ) {
2329 let mut path = String::from("/");
2331
2332 for segment in &segments {
2333 path.push_str(segment);
2334 path.push('/');
2335 }
2336 path.pop(); if has_param {
2339 path.push_str("/{id}");
2340 }
2341
2342 let result = catch_unwind(AssertUnwindSafe(|| {
2344 async fn handler1() -> &'static str { "handler1" }
2345 async fn handler2() -> &'static str { "handler2" }
2346
2347 let _router = Router::new()
2348 .route(&path, get(handler1))
2349 .route(&path, get(handler2));
2350 }));
2351
2352 prop_assert!(
2353 result.is_err(),
2354 "Registering path '{}' twice should conflict but didn't",
2355 path
2356 );
2357 }
2358 }
2359
2360 proptest! {
2367 #![proptest_config(ProptestConfig::with_cases(100))]
2368
2369 #[test]
2374 fn prop_nested_route_with_params_matches(
2375 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2376 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
2377 param_value in "[a-z0-9]{1,10}",
2378 ) {
2379 async fn handler() -> &'static str { "handler" }
2380
2381 let prefix = format!("/{}", prefix_segments.join("/"));
2382 let route_path = if route_segments.is_empty() {
2383 "/{id}".to_string()
2384 } else {
2385 format!("/{}/{{id}}", route_segments.join("/"))
2386 };
2387
2388 let nested_router = Router::new().route(&route_path, get(handler));
2389 let app = Router::new().nest(&prefix, nested_router);
2390
2391 let full_path = if route_segments.is_empty() {
2393 format!("{}/{}", prefix, param_value)
2394 } else {
2395 format!("{}/{}/{}", prefix, route_segments.join("/"), param_value)
2396 };
2397
2398 match app.match_route(&full_path, &Method::GET) {
2400 RouteMatch::Found { params, .. } => {
2401 prop_assert!(
2403 params.contains_key("id"),
2404 "Should have 'id' parameter, got: {:?}",
2405 params
2406 );
2407 prop_assert_eq!(
2408 params.get("id").unwrap(),
2409 ¶m_value,
2410 "Parameter value should match"
2411 );
2412 }
2413 RouteMatch::NotFound => {
2414 prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
2415 }
2416 RouteMatch::MethodNotAllowed { .. } => {
2417 prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
2418 }
2419 }
2420 }
2421
2422 #[test]
2427 fn prop_nested_route_matches_correct_method(
2428 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2429 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2430 use_get in any::<bool>(),
2431 ) {
2432 async fn handler() -> &'static str { "handler" }
2433
2434 let prefix = format!("/{}", prefix_segments.join("/"));
2435 let route_path = format!("/{}", route_segments.join("/"));
2436
2437 let method_router = if use_get { get(handler) } else { post(handler) };
2439 let nested_router = Router::new().route(&route_path, method_router);
2440 let app = Router::new().nest(&prefix, nested_router);
2441
2442 let full_path = format!("{}{}", prefix, route_path);
2443 let registered_method = if use_get { Method::GET } else { Method::POST };
2444 let other_method = if use_get { Method::POST } else { Method::GET };
2445
2446 match app.match_route(&full_path, ®istered_method) {
2448 RouteMatch::Found { .. } => {
2449 }
2451 other => {
2452 prop_assert!(false, "Route should be found for registered method, got: {:?}",
2453 match other {
2454 RouteMatch::NotFound => "NotFound",
2455 RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
2456 _ => "Found",
2457 }
2458 );
2459 }
2460 }
2461
2462 match app.match_route(&full_path, &other_method) {
2464 RouteMatch::MethodNotAllowed { allowed } => {
2465 prop_assert!(
2466 allowed.contains(®istered_method),
2467 "Allowed methods should contain {:?}",
2468 registered_method
2469 );
2470 }
2471 other => {
2472 prop_assert!(false, "Route should return MethodNotAllowed for other method, got: {:?}",
2473 match other {
2474 RouteMatch::NotFound => "NotFound",
2475 RouteMatch::Found { .. } => "Found",
2476 _ => "MethodNotAllowed",
2477 }
2478 );
2479 }
2480 }
2481 }
2482 }
2483
2484 proptest! {
2491 #![proptest_config(ProptestConfig::with_cases(100))]
2492
2493 #[test]
2498 fn prop_single_param_extraction(
2499 prefix in "[a-z][a-z0-9]{1,5}",
2500 param_name in "[a-z][a-z0-9]{1,5}",
2501 param_value in "[a-z0-9]{1,10}",
2502 ) {
2503 async fn handler() -> &'static str { "handler" }
2504
2505 let prefix = format!("/{}", prefix);
2506 let route_path = format!("/{{{}}}", param_name);
2507
2508 let nested_router = Router::new().route(&route_path, get(handler));
2509 let app = Router::new().nest(&prefix, nested_router);
2510
2511 let full_path = format!("{}/{}", prefix, param_value);
2512
2513 match app.match_route(&full_path, &Method::GET) {
2514 RouteMatch::Found { params, .. } => {
2515 prop_assert!(
2516 params.contains_key(¶m_name),
2517 "Should have '{}' parameter, got: {:?}",
2518 param_name, params
2519 );
2520 prop_assert_eq!(
2521 params.get(¶m_name).unwrap(),
2522 ¶m_value,
2523 "Parameter '{}' value should be '{}'",
2524 param_name, param_value
2525 );
2526 }
2527 _ => {
2528 prop_assert!(false, "Route should be found");
2529 }
2530 }
2531 }
2532
2533 #[test]
2538 fn prop_multiple_params_extraction(
2539 prefix in "[a-z][a-z0-9]{1,5}",
2540 param1_name in "[a-z]{1,5}",
2541 param1_value in "[a-z0-9]{1,10}",
2542 param2_name in "[a-z]{1,5}",
2543 param2_value in "[a-z0-9]{1,10}",
2544 ) {
2545 prop_assume!(param1_name != param2_name);
2547
2548 async fn handler() -> &'static str { "handler" }
2549
2550 let prefix = format!("/{}", prefix);
2551 let route_path = format!("/{{{}}}/items/{{{}}}", param1_name, param2_name);
2552
2553 let nested_router = Router::new().route(&route_path, get(handler));
2554 let app = Router::new().nest(&prefix, nested_router);
2555
2556 let full_path = format!("{}/{}/items/{}", prefix, param1_value, param2_value);
2557
2558 match app.match_route(&full_path, &Method::GET) {
2559 RouteMatch::Found { params, .. } => {
2560 prop_assert!(
2562 params.contains_key(¶m1_name),
2563 "Should have '{}' parameter, got: {:?}",
2564 param1_name, params
2565 );
2566 prop_assert_eq!(
2567 params.get(¶m1_name).unwrap(),
2568 ¶m1_value,
2569 "Parameter '{}' value should be '{}'",
2570 param1_name, param1_value
2571 );
2572
2573 prop_assert!(
2575 params.contains_key(¶m2_name),
2576 "Should have '{}' parameter, got: {:?}",
2577 param2_name, params
2578 );
2579 prop_assert_eq!(
2580 params.get(¶m2_name).unwrap(),
2581 ¶m2_value,
2582 "Parameter '{}' value should be '{}'",
2583 param2_name, param2_value
2584 );
2585 }
2586 _ => {
2587 prop_assert!(false, "Route should be found");
2588 }
2589 }
2590 }
2591
2592 #[test]
2597 fn prop_param_value_preservation(
2598 prefix in "[a-z]{1,5}",
2599 param_value in "[a-zA-Z0-9_-]{1,15}",
2601 ) {
2602 async fn handler() -> &'static str { "handler" }
2603
2604 let prefix = format!("/{}", prefix);
2605 let route_path = "/{id}".to_string();
2606
2607 let nested_router = Router::new().route(&route_path, get(handler));
2608 let app = Router::new().nest(&prefix, nested_router);
2609
2610 let full_path = format!("{}/{}", prefix, param_value);
2611
2612 match app.match_route(&full_path, &Method::GET) {
2613 RouteMatch::Found { params, .. } => {
2614 prop_assert_eq!(
2615 params.get("id").unwrap(),
2616 ¶m_value,
2617 "Parameter value should be preserved exactly"
2618 );
2619 }
2620 _ => {
2621 prop_assert!(false, "Route should be found");
2622 }
2623 }
2624 }
2625 }
2626
2627 proptest! {
2634 #![proptest_config(ProptestConfig::with_cases(100))]
2635
2636 #[test]
2641 fn prop_unregistered_path_returns_not_found(
2642 prefix in "[a-z][a-z0-9]{1,5}",
2643 route_segment in "[a-z][a-z0-9]{1,5}",
2644 unregistered_segment in "[a-z][a-z0-9]{6,10}",
2645 ) {
2646 prop_assume!(route_segment != unregistered_segment);
2648
2649 async fn handler() -> &'static str { "handler" }
2650
2651 let prefix = format!("/{}", prefix);
2652 let route_path = format!("/{}", route_segment);
2653
2654 let nested_router = Router::new().route(&route_path, get(handler));
2655 let app = Router::new().nest(&prefix, nested_router);
2656
2657 let unregistered_path = format!("{}/{}", prefix, unregistered_segment);
2659
2660 match app.match_route(&unregistered_path, &Method::GET) {
2661 RouteMatch::NotFound => {
2662 }
2664 RouteMatch::Found { .. } => {
2665 prop_assert!(false, "Path '{}' should not be found", unregistered_path);
2666 }
2667 RouteMatch::MethodNotAllowed { .. } => {
2668 prop_assert!(false, "Path '{}' should return NotFound, not MethodNotAllowed", unregistered_path);
2669 }
2670 }
2671 }
2672
2673 #[test]
2677 fn prop_wrong_prefix_returns_not_found(
2678 prefix1 in "[a-z][a-z0-9]{1,5}",
2679 prefix2 in "[a-z][a-z0-9]{6,10}",
2680 route_segment in "[a-z][a-z0-9]{1,5}",
2681 ) {
2682 prop_assume!(prefix1 != prefix2);
2684
2685 async fn handler() -> &'static str { "handler" }
2686
2687 let prefix = format!("/{}", prefix1);
2688 let route_path = format!("/{}", route_segment);
2689
2690 let nested_router = Router::new().route(&route_path, get(handler));
2691 let app = Router::new().nest(&prefix, nested_router);
2692
2693 let wrong_prefix_path = format!("/{}/{}", prefix2, route_segment);
2695
2696 match app.match_route(&wrong_prefix_path, &Method::GET) {
2697 RouteMatch::NotFound => {
2698 }
2700 _ => {
2701 prop_assert!(false, "Path '{}' with wrong prefix should return NotFound", wrong_prefix_path);
2702 }
2703 }
2704 }
2705
2706 #[test]
2711 fn prop_partial_path_returns_not_found(
2712 prefix in "[a-z][a-z0-9]{1,5}",
2713 segment1 in "[a-z][a-z0-9]{1,5}",
2714 segment2 in "[a-z][a-z0-9]{1,5}",
2715 ) {
2716 async fn handler() -> &'static str { "handler" }
2717
2718 let prefix = format!("/{}", prefix);
2719 let route_path = format!("/{}/{}", segment1, segment2);
2720
2721 let nested_router = Router::new().route(&route_path, get(handler));
2722 let app = Router::new().nest(&prefix, nested_router);
2723
2724 let partial_path = format!("{}/{}", prefix, segment1);
2726
2727 match app.match_route(&partial_path, &Method::GET) {
2728 RouteMatch::NotFound => {
2729 }
2731 _ => {
2732 prop_assert!(false, "Partial path '{}' should return NotFound", partial_path);
2733 }
2734 }
2735 }
2736 }
2737
2738 proptest! {
2745 #![proptest_config(ProptestConfig::with_cases(100))]
2746
2747 #[test]
2753 fn prop_unregistered_method_returns_method_not_allowed(
2754 prefix in "[a-z][a-z0-9]{1,5}",
2755 route_segment in "[a-z][a-z0-9]{1,5}",
2756 ) {
2757 async fn handler() -> &'static str { "handler" }
2758
2759 let prefix = format!("/{}", prefix);
2760 let route_path = format!("/{}", route_segment);
2761
2762 let nested_router = Router::new().route(&route_path, get(handler));
2764 let app = Router::new().nest(&prefix, nested_router);
2765
2766 let full_path = format!("{}{}", prefix, route_path);
2767
2768 match app.match_route(&full_path, &Method::POST) {
2770 RouteMatch::MethodNotAllowed { allowed } => {
2771 prop_assert!(
2772 allowed.contains(&Method::GET),
2773 "Allowed methods should contain GET, got: {:?}",
2774 allowed
2775 );
2776 prop_assert!(
2777 !allowed.contains(&Method::POST),
2778 "Allowed methods should not contain POST"
2779 );
2780 }
2781 RouteMatch::Found { .. } => {
2782 prop_assert!(false, "POST should not be found on GET-only route");
2783 }
2784 RouteMatch::NotFound => {
2785 prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
2786 }
2787 }
2788 }
2789
2790 #[test]
2795 fn prop_multiple_methods_in_allowed_list(
2796 prefix in "[a-z][a-z0-9]{1,5}",
2797 route_segment in "[a-z][a-z0-9]{1,5}",
2798 use_get in any::<bool>(),
2799 use_post in any::<bool>(),
2800 use_put in any::<bool>(),
2801 ) {
2802 prop_assume!(use_get || use_post || use_put);
2804
2805 async fn handler() -> &'static str { "handler" }
2806
2807 let prefix = format!("/{}", prefix);
2808 let route_path = format!("/{}", route_segment);
2809
2810 let mut method_router = MethodRouter::new();
2812 let mut expected_methods: Vec<Method> = Vec::new();
2813
2814 if use_get {
2815 let get_router = get(handler);
2816 for (method, h) in get_router.handlers {
2817 method_router.handlers.insert(method.clone(), h);
2818 expected_methods.push(method);
2819 }
2820 }
2821 if use_post {
2822 let post_router = post(handler);
2823 for (method, h) in post_router.handlers {
2824 method_router.handlers.insert(method.clone(), h);
2825 expected_methods.push(method);
2826 }
2827 }
2828 if use_put {
2829 let put_router = put(handler);
2830 for (method, h) in put_router.handlers {
2831 method_router.handlers.insert(method.clone(), h);
2832 expected_methods.push(method);
2833 }
2834 }
2835
2836 let nested_router = Router::new().route(&route_path, method_router);
2837 let app = Router::new().nest(&prefix, nested_router);
2838
2839 let full_path = format!("{}{}", prefix, route_path);
2840
2841 match app.match_route(&full_path, &Method::DELETE) {
2843 RouteMatch::MethodNotAllowed { allowed } => {
2844 for method in &expected_methods {
2846 prop_assert!(
2847 allowed.contains(method),
2848 "Allowed methods should contain {:?}, got: {:?}",
2849 method, allowed
2850 );
2851 }
2852 prop_assert!(
2854 !allowed.contains(&Method::DELETE),
2855 "Allowed methods should not contain DELETE"
2856 );
2857 }
2858 RouteMatch::Found { .. } => {
2859 prop_assert!(false, "DELETE should not be found");
2860 }
2861 RouteMatch::NotFound => {
2862 prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
2863 }
2864 }
2865 }
2866 }
2867
2868 proptest! {
2882 #![proptest_config(ProptestConfig::with_cases(100))]
2883
2884 #[test]
2890 fn prop_multiple_routers_all_routes_registered(
2891 prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2893 prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2894 num_routes1 in 1..4usize,
2896 num_routes2 in 1..4usize,
2897 ) {
2898 let prefix1 = format!("/{}", prefix1_segments.join("/"));
2900 let prefix2 = format!("/{}", prefix2_segments.join("/"));
2901
2902 prop_assume!(prefix1 != prefix2);
2904
2905 async fn handler() -> &'static str { "handler" }
2906
2907 let mut router1 = Router::new();
2909 for i in 0..num_routes1 {
2910 let path = format!("/route1_{}", i);
2911 router1 = router1.route(&path, get(handler));
2912 }
2913
2914 let mut router2 = Router::new();
2916 for i in 0..num_routes2 {
2917 let path = format!("/route2_{}", i);
2918 router2 = router2.route(&path, get(handler));
2919 }
2920
2921 let app = Router::new()
2923 .nest(&prefix1, router1)
2924 .nest(&prefix2, router2);
2925
2926 let routes = app.registered_routes();
2927
2928 let expected_count = num_routes1 + num_routes2;
2930 prop_assert_eq!(
2931 routes.len(),
2932 expected_count,
2933 "Should have {} routes ({}+{}), got {}",
2934 expected_count, num_routes1, num_routes2, routes.len()
2935 );
2936
2937 for i in 0..num_routes1 {
2939 let expected_path = format!("{}/route1_{}", prefix1, i);
2940 let matchit_path = convert_path_params(&expected_path);
2941 prop_assert!(
2942 routes.contains_key(&matchit_path),
2943 "Route '{}' should be registered",
2944 expected_path
2945 );
2946 }
2947
2948 for i in 0..num_routes2 {
2950 let expected_path = format!("{}/route2_{}", prefix2, i);
2951 let matchit_path = convert_path_params(&expected_path);
2952 prop_assert!(
2953 routes.contains_key(&matchit_path),
2954 "Route '{}' should be registered",
2955 expected_path
2956 );
2957 }
2958 }
2959
2960 #[test]
2965 fn prop_multiple_routers_no_interference(
2966 prefix1 in "[a-z][a-z0-9]{1,5}",
2967 prefix2 in "[a-z][a-z0-9]{1,5}",
2968 route_segment in "[a-z][a-z0-9]{1,5}",
2969 param_value1 in "[a-z0-9]{1,10}",
2970 param_value2 in "[a-z0-9]{1,10}",
2971 ) {
2972 prop_assume!(prefix1 != prefix2);
2974
2975 let prefix1 = format!("/{}", prefix1);
2976 let prefix2 = format!("/{}", prefix2);
2977
2978 async fn handler() -> &'static str { "handler" }
2979
2980 let router1 = Router::new()
2982 .route(&format!("/{}", route_segment), get(handler))
2983 .route("/{id}", get(handler));
2984
2985 let router2 = Router::new()
2986 .route(&format!("/{}", route_segment), get(handler))
2987 .route("/{id}", get(handler));
2988
2989 let app = Router::new()
2991 .nest(&prefix1, router1)
2992 .nest(&prefix2, router2);
2993
2994 let path1_static = format!("{}/{}", prefix1, route_segment);
2996 match app.match_route(&path1_static, &Method::GET) {
2997 RouteMatch::Found { params, .. } => {
2998 prop_assert!(params.is_empty(), "Static path should have no params");
2999 }
3000 _ => {
3001 prop_assert!(false, "Route '{}' should be found", path1_static);
3002 }
3003 }
3004
3005 let path1_param = format!("{}/{}", prefix1, param_value1);
3006 match app.match_route(&path1_param, &Method::GET) {
3007 RouteMatch::Found { params, .. } => {
3008 prop_assert_eq!(
3009 params.get("id"),
3010 Some(¶m_value1.to_string()),
3011 "Parameter should be extracted correctly"
3012 );
3013 }
3014 _ => {
3015 prop_assert!(false, "Route '{}' should be found", path1_param);
3016 }
3017 }
3018
3019 let path2_static = format!("{}/{}", prefix2, route_segment);
3021 match app.match_route(&path2_static, &Method::GET) {
3022 RouteMatch::Found { params, .. } => {
3023 prop_assert!(params.is_empty(), "Static path should have no params");
3024 }
3025 _ => {
3026 prop_assert!(false, "Route '{}' should be found", path2_static);
3027 }
3028 }
3029
3030 let path2_param = format!("{}/{}", prefix2, param_value2);
3031 match app.match_route(&path2_param, &Method::GET) {
3032 RouteMatch::Found { params, .. } => {
3033 prop_assert_eq!(
3034 params.get("id"),
3035 Some(¶m_value2.to_string()),
3036 "Parameter should be extracted correctly"
3037 );
3038 }
3039 _ => {
3040 prop_assert!(false, "Route '{}' should be found", path2_param);
3041 }
3042 }
3043 }
3044
3045 #[test]
3050 fn prop_multiple_routers_preserve_methods(
3051 prefix1 in "[a-z][a-z0-9]{1,5}",
3052 prefix2 in "[a-z][a-z0-9]{1,5}",
3053 route_segment in "[a-z][a-z0-9]{1,5}",
3054 router1_use_get in any::<bool>(),
3055 router1_use_post in any::<bool>(),
3056 router2_use_get in any::<bool>(),
3057 router2_use_put in any::<bool>(),
3058 ) {
3059 prop_assume!(router1_use_get || router1_use_post);
3061 prop_assume!(router2_use_get || router2_use_put);
3062 prop_assume!(prefix1 != prefix2);
3064
3065 let prefix1 = format!("/{}", prefix1);
3066 let prefix2 = format!("/{}", prefix2);
3067 let route_path = format!("/{}", route_segment);
3068
3069 async fn handler() -> &'static str { "handler" }
3070
3071 let mut method_router1 = MethodRouter::new();
3073 let mut expected_methods1: Vec<Method> = Vec::new();
3074 if router1_use_get {
3075 let get_router = get(handler);
3076 for (method, h) in get_router.handlers {
3077 method_router1.handlers.insert(method.clone(), h);
3078 expected_methods1.push(method);
3079 }
3080 }
3081 if router1_use_post {
3082 let post_router = post(handler);
3083 for (method, h) in post_router.handlers {
3084 method_router1.handlers.insert(method.clone(), h);
3085 expected_methods1.push(method);
3086 }
3087 }
3088
3089 let mut method_router2 = MethodRouter::new();
3091 let mut expected_methods2: Vec<Method> = Vec::new();
3092 if router2_use_get {
3093 let get_router = get(handler);
3094 for (method, h) in get_router.handlers {
3095 method_router2.handlers.insert(method.clone(), h);
3096 expected_methods2.push(method);
3097 }
3098 }
3099 if router2_use_put {
3100 let put_router = put(handler);
3101 for (method, h) in put_router.handlers {
3102 method_router2.handlers.insert(method.clone(), h);
3103 expected_methods2.push(method);
3104 }
3105 }
3106
3107 let router1 = Router::new().route(&route_path, method_router1);
3108 let router2 = Router::new().route(&route_path, method_router2);
3109
3110 let app = Router::new()
3111 .nest(&prefix1, router1)
3112 .nest(&prefix2, router2);
3113
3114 let full_path1 = format!("{}{}", prefix1, route_path);
3115 let full_path2 = format!("{}{}", prefix2, route_path);
3116
3117 for method in &expected_methods1 {
3119 match app.match_route(&full_path1, method) {
3120 RouteMatch::Found { .. } => {}
3121 _ => {
3122 prop_assert!(false, "Method {:?} should be found for {}", method, full_path1);
3123 }
3124 }
3125 }
3126
3127 for method in &expected_methods2 {
3129 match app.match_route(&full_path2, method) {
3130 RouteMatch::Found { .. } => {}
3131 _ => {
3132 prop_assert!(false, "Method {:?} should be found for {}", method, full_path2);
3133 }
3134 }
3135 }
3136
3137 if !expected_methods1.contains(&Method::DELETE) {
3139 match app.match_route(&full_path1, &Method::DELETE) {
3140 RouteMatch::MethodNotAllowed { allowed } => {
3141 for method in &expected_methods1 {
3142 prop_assert!(
3143 allowed.contains(method),
3144 "Allowed methods for {} should contain {:?}",
3145 full_path1, method
3146 );
3147 }
3148 }
3149 _ => {
3150 prop_assert!(false, "DELETE should return MethodNotAllowed for {}", full_path1);
3151 }
3152 }
3153 }
3154 }
3155
3156 #[test]
3161 fn prop_three_routers_composition(
3162 prefix1 in "[a-z]{1,3}",
3163 prefix2 in "[a-z]{4,6}",
3164 prefix3 in "[a-z]{7,9}",
3165 num_routes in 1..3usize,
3166 ) {
3167 let prefix1 = format!("/{}", prefix1);
3168 let prefix2 = format!("/{}", prefix2);
3169 let prefix3 = format!("/{}", prefix3);
3170
3171 async fn handler() -> &'static str { "handler" }
3172
3173 let mut router1 = Router::new();
3175 let mut router2 = Router::new();
3176 let mut router3 = Router::new();
3177
3178 for i in 0..num_routes {
3179 let path = format!("/item{}", i);
3180 router1 = router1.route(&path, get(handler));
3181 router2 = router2.route(&path, get(handler));
3182 router3 = router3.route(&path, get(handler));
3183 }
3184
3185 let app = Router::new()
3187 .nest(&prefix1, router1)
3188 .nest(&prefix2, router2)
3189 .nest(&prefix3, router3);
3190
3191 let routes = app.registered_routes();
3192
3193 let expected_count = 3 * num_routes;
3195 prop_assert_eq!(
3196 routes.len(),
3197 expected_count,
3198 "Should have {} routes, got {}",
3199 expected_count, routes.len()
3200 );
3201
3202 for i in 0..num_routes {
3204 let path1 = format!("{}/item{}", prefix1, i);
3205 let path2 = format!("{}/item{}", prefix2, i);
3206 let path3 = format!("{}/item{}", prefix3, i);
3207
3208 match app.match_route(&path1, &Method::GET) {
3209 RouteMatch::Found { .. } => {}
3210 _ => prop_assert!(false, "Route '{}' should be found", path1),
3211 }
3212 match app.match_route(&path2, &Method::GET) {
3213 RouteMatch::Found { .. } => {}
3214 _ => prop_assert!(false, "Route '{}' should be found", path2),
3215 }
3216 match app.match_route(&path3, &Method::GET) {
3217 RouteMatch::Found { .. } => {}
3218 _ => prop_assert!(false, "Route '{}' should be found", path3),
3219 }
3220 }
3221 }
3222 }
3223 proptest! {
3224 #![proptest_config(ProptestConfig::with_cases(100))]
3225
3226 #[test]
3232 fn prop_nested_route_conflict_different_param_names(
3233 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3234 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
3235 param1 in "[a-z][a-z0-9]{1,5}",
3236 param2 in "[a-z][a-z0-9]{1,5}",
3237 ) {
3238 prop_assume!(param1 != param2);
3240
3241 async fn handler1() -> &'static str { "handler1" }
3242 async fn handler2() -> &'static str { "handler2" }
3243
3244 let prefix = format!("/{}", prefix_segments.join("/"));
3245
3246 let existing_path = if route_segments.is_empty() {
3248 format!("{}/{{{}}}", prefix, param1)
3249 } else {
3250 format!("{}/{}/{{{}}}", prefix, route_segments.join("/"), param1)
3251 };
3252
3253 let nested_path = if route_segments.is_empty() {
3255 format!("/{{{}}}", param2)
3256 } else {
3257 format!("/{}/{{{}}}", route_segments.join("/"), param2)
3258 };
3259
3260 let result = catch_unwind(AssertUnwindSafe(|| {
3262 let parent = Router::new().route(&existing_path, get(handler1));
3263 let nested = Router::new().route(&nested_path, get(handler2));
3264 let _app = parent.nest(&prefix, nested);
3265 }));
3266
3267 prop_assert!(
3269 result.is_err(),
3270 "Nested route '{}{}' should conflict with existing route '{}' but didn't",
3271 prefix, nested_path, existing_path
3272 );
3273
3274 if let Err(panic_info) = result {
3276 if let Some(msg) = panic_info.downcast_ref::<String>() {
3277 prop_assert!(
3278 msg.contains("ROUTE CONFLICT DETECTED"),
3279 "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
3280 msg
3281 );
3282 prop_assert!(
3283 msg.contains("Existing:") && msg.contains("New:"),
3284 "Error should contain both 'Existing:' and 'New:' labels, got: {}",
3285 msg
3286 );
3287 }
3288 }
3289 }
3290
3291 #[test]
3296 fn prop_nested_route_conflict_exact_same_path(
3297 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3298 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3299 ) {
3300 async fn handler1() -> &'static str { "handler1" }
3301 async fn handler2() -> &'static str { "handler2" }
3302
3303 let prefix = format!("/{}", prefix_segments.join("/"));
3304 let route_path = format!("/{}", route_segments.join("/"));
3305
3306 let existing_path = format!("{}{}", prefix, route_path);
3308
3309 let result = catch_unwind(AssertUnwindSafe(|| {
3311 let parent = Router::new().route(&existing_path, get(handler1));
3312 let nested = Router::new().route(&route_path, get(handler2));
3313 let _app = parent.nest(&prefix, nested);
3314 }));
3315
3316 prop_assert!(
3318 result.is_err(),
3319 "Nested route '{}{}' should conflict with existing route '{}' but didn't",
3320 prefix, route_path, existing_path
3321 );
3322 }
3323
3324 #[test]
3329 fn prop_nested_routes_different_prefixes_no_conflict(
3330 prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3331 prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3332 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3333 has_param in any::<bool>(),
3334 ) {
3335 let prefix1 = format!("/{}", prefix1_segments.join("/"));
3337 let prefix2 = format!("/{}", prefix2_segments.join("/"));
3338
3339 prop_assume!(prefix1 != prefix2);
3341
3342 async fn handler1() -> &'static str { "handler1" }
3343 async fn handler2() -> &'static str { "handler2" }
3344
3345 let route_path = if has_param {
3347 format!("/{}/{{id}}", route_segments.join("/"))
3348 } else {
3349 format!("/{}", route_segments.join("/"))
3350 };
3351
3352 let result = catch_unwind(AssertUnwindSafe(|| {
3354 let nested1 = Router::new().route(&route_path, get(handler1));
3355 let nested2 = Router::new().route(&route_path, get(handler2));
3356
3357 let app = Router::new()
3358 .nest(&prefix1, nested1)
3359 .nest(&prefix2, nested2);
3360
3361 app.registered_routes().len()
3362 }));
3363
3364 prop_assert!(
3366 result.is_ok(),
3367 "Routes under different prefixes '{}' and '{}' should not conflict",
3368 prefix1, prefix2
3369 );
3370
3371 if let Ok(count) = result {
3372 prop_assert_eq!(count, 2, "Should have registered 2 routes");
3373 }
3374 }
3375
3376 #[test]
3381 fn prop_nested_conflict_error_contains_guidance(
3382 prefix in "[a-z][a-z0-9]{1,5}",
3383 segment in "[a-z][a-z0-9]{1,5}",
3384 param1 in "[a-z][a-z0-9]{1,5}",
3385 param2 in "[a-z][a-z0-9]{1,5}",
3386 ) {
3387 prop_assume!(param1 != param2);
3388
3389 async fn handler1() -> &'static str { "handler1" }
3390 async fn handler2() -> &'static str { "handler2" }
3391
3392 let prefix = format!("/{}", prefix);
3393 let existing_path = format!("{}/{}/{{{}}}", prefix, segment, param1);
3394 let nested_path = format!("/{}/{{{}}}", segment, param2);
3395
3396 let result = catch_unwind(AssertUnwindSafe(|| {
3397 let parent = Router::new().route(&existing_path, get(handler1));
3398 let nested = Router::new().route(&nested_path, get(handler2));
3399 let _app = parent.nest(&prefix, nested);
3400 }));
3401
3402 prop_assert!(result.is_err(), "Should have detected conflict");
3403
3404 if let Err(panic_info) = result {
3405 if let Some(msg) = panic_info.downcast_ref::<String>() {
3406 prop_assert!(
3407 msg.contains("How to resolve:"),
3408 "Error should contain 'How to resolve:' guidance, got: {}",
3409 msg
3410 );
3411 prop_assert!(
3412 msg.contains("Use different path patterns") ||
3413 msg.contains("different path patterns"),
3414 "Error should suggest using different path patterns, got: {}",
3415 msg
3416 );
3417 }
3418 }
3419 }
3420 }
3421}