1use crate::handler::{into_boxed_handler, BoxedHandler, Handler};
45use http::{Extensions, Method};
46use matchit::Router as MatchitRouter;
47use rustapi_openapi::Operation;
48use std::collections::HashMap;
49use std::sync::Arc;
50
51#[derive(Debug, Clone)]
53pub struct RouteInfo {
54 pub path: String,
56 pub methods: Vec<Method>,
58}
59
60#[derive(Debug, Clone)]
62pub struct RouteConflictError {
63 pub new_path: String,
65 pub method: Option<Method>,
67 pub existing_path: String,
69 pub details: String,
71}
72
73impl std::fmt::Display for RouteConflictError {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 writeln!(
76 f,
77 "\n╭─────────────────────────────────────────────────────────────╮"
78 )?;
79 writeln!(
80 f,
81 "│ ROUTE CONFLICT DETECTED │"
82 )?;
83 writeln!(
84 f,
85 "╰─────────────────────────────────────────────────────────────╯"
86 )?;
87 writeln!(f)?;
88 writeln!(f, " Conflicting routes:")?;
89 writeln!(f, " → Existing: {}", self.existing_path)?;
90 writeln!(f, " → New: {}", self.new_path)?;
91 writeln!(f)?;
92 if let Some(ref method) = self.method {
93 writeln!(f, " HTTP Method: {}", method)?;
94 writeln!(f)?;
95 }
96 writeln!(f, " Details: {}", self.details)?;
97 writeln!(f)?;
98 writeln!(f, " How to resolve:")?;
99 writeln!(f, " 1. Use different path patterns for each route")?;
100 writeln!(
101 f,
102 " 2. If paths must be similar, ensure parameter names differ"
103 )?;
104 writeln!(
105 f,
106 " 3. Consider using different HTTP methods if appropriate"
107 )?;
108 writeln!(f)?;
109 writeln!(f, " Example:")?;
110 writeln!(f, " Instead of:")?;
111 writeln!(f, " .route(\"/users/{{id}}\", get(handler1))")?;
112 writeln!(f, " .route(\"/users/{{user_id}}\", get(handler2))")?;
113 writeln!(f)?;
114 writeln!(f, " Use:")?;
115 writeln!(f, " .route(\"/users/{{id}}\", get(handler1))")?;
116 writeln!(f, " .route(\"/users/{{id}}/profile\", get(handler2))")?;
117 Ok(())
118 }
119}
120
121impl std::error::Error for RouteConflictError {}
122
123pub struct MethodRouter {
125 handlers: HashMap<Method, BoxedHandler>,
126 pub(crate) operations: HashMap<Method, Operation>,
127}
128
129impl Clone for MethodRouter {
130 fn clone(&self) -> Self {
131 Self {
132 handlers: self.handlers.clone(),
133 operations: self.operations.clone(),
134 }
135 }
136}
137
138impl MethodRouter {
139 pub fn new() -> Self {
141 Self {
142 handlers: HashMap::new(),
143 operations: HashMap::new(),
144 }
145 }
146
147 fn on(mut self, method: Method, handler: BoxedHandler, operation: Operation) -> Self {
149 self.handlers.insert(method.clone(), handler);
150 self.operations.insert(method, operation);
151 self
152 }
153
154 pub(crate) fn get_handler(&self, method: &Method) -> Option<&BoxedHandler> {
156 self.handlers.get(method)
157 }
158
159 pub(crate) fn allowed_methods(&self) -> Vec<Method> {
161 self.handlers.keys().cloned().collect()
162 }
163
164 pub(crate) fn from_boxed(handlers: HashMap<Method, BoxedHandler>) -> Self {
166 Self {
167 handlers,
168 operations: HashMap::new(), }
170 }
171
172 pub(crate) fn insert_boxed_with_operation(
176 &mut self,
177 method: Method,
178 handler: BoxedHandler,
179 operation: Operation,
180 ) {
181 if self.handlers.contains_key(&method) {
182 panic!(
183 "Duplicate handler for method {} on the same path",
184 method.as_str()
185 );
186 }
187
188 self.handlers.insert(method.clone(), handler);
189 self.operations.insert(method, operation);
190 }
191}
192
193impl Default for MethodRouter {
194 fn default() -> Self {
195 Self::new()
196 }
197}
198
199pub fn get<H, T>(handler: H) -> MethodRouter
201where
202 H: Handler<T>,
203 T: 'static,
204{
205 let mut op = Operation::new();
206 H::update_operation(&mut op);
207 MethodRouter::new().on(Method::GET, into_boxed_handler(handler), op)
208}
209
210pub fn post<H, T>(handler: H) -> MethodRouter
212where
213 H: Handler<T>,
214 T: 'static,
215{
216 let mut op = Operation::new();
217 H::update_operation(&mut op);
218 MethodRouter::new().on(Method::POST, into_boxed_handler(handler), op)
219}
220
221pub fn put<H, T>(handler: H) -> MethodRouter
223where
224 H: Handler<T>,
225 T: 'static,
226{
227 let mut op = Operation::new();
228 H::update_operation(&mut op);
229 MethodRouter::new().on(Method::PUT, into_boxed_handler(handler), op)
230}
231
232pub fn patch<H, T>(handler: H) -> MethodRouter
234where
235 H: Handler<T>,
236 T: 'static,
237{
238 let mut op = Operation::new();
239 H::update_operation(&mut op);
240 MethodRouter::new().on(Method::PATCH, into_boxed_handler(handler), op)
241}
242
243pub fn delete<H, T>(handler: H) -> MethodRouter
245where
246 H: Handler<T>,
247 T: 'static,
248{
249 let mut op = Operation::new();
250 H::update_operation(&mut op);
251 MethodRouter::new().on(Method::DELETE, into_boxed_handler(handler), op)
252}
253
254pub struct Router {
256 inner: MatchitRouter<MethodRouter>,
257 state: Arc<Extensions>,
258 registered_routes: HashMap<String, RouteInfo>,
260 method_routers: HashMap<String, MethodRouter>,
262 state_type_ids: Vec<std::any::TypeId>,
265}
266
267impl Router {
268 pub fn new() -> Self {
270 Self {
271 inner: MatchitRouter::new(),
272 state: Arc::new(Extensions::new()),
273 registered_routes: HashMap::new(),
274 method_routers: HashMap::new(),
275 state_type_ids: Vec::new(),
276 }
277 }
278
279 pub fn route(mut self, path: &str, method_router: MethodRouter) -> Self {
281 let matchit_path = convert_path_params(path);
283
284 let methods: Vec<Method> = method_router.handlers.keys().cloned().collect();
286
287 self.method_routers
289 .insert(matchit_path.clone(), method_router.clone());
290
291 match self.inner.insert(matchit_path.clone(), method_router) {
292 Ok(_) => {
293 self.registered_routes.insert(
295 matchit_path.clone(),
296 RouteInfo {
297 path: path.to_string(),
298 methods,
299 },
300 );
301 }
302 Err(e) => {
303 self.method_routers.remove(&matchit_path);
305
306 let existing_path = self
308 .find_conflicting_route(&matchit_path)
309 .map(|info| info.path.clone())
310 .unwrap_or_else(|| "<unknown>".to_string());
311
312 let conflict_error = RouteConflictError {
313 new_path: path.to_string(),
314 method: methods.first().cloned(),
315 existing_path,
316 details: e.to_string(),
317 };
318
319 panic!("{}", conflict_error);
320 }
321 }
322 self
323 }
324
325 fn find_conflicting_route(&self, matchit_path: &str) -> Option<&RouteInfo> {
327 if let Some(info) = self.registered_routes.get(matchit_path) {
329 return Some(info);
330 }
331
332 let normalized_new = normalize_path_for_comparison(matchit_path);
334
335 for (registered_path, info) in &self.registered_routes {
336 let normalized_existing = normalize_path_for_comparison(registered_path);
337 if normalized_new == normalized_existing {
338 return Some(info);
339 }
340 }
341
342 None
343 }
344
345 pub fn state<S: Clone + Send + Sync + 'static>(mut self, state: S) -> Self {
347 let type_id = std::any::TypeId::of::<S>();
348 let extensions = Arc::make_mut(&mut self.state);
349 extensions.insert(state);
350 if !self.state_type_ids.contains(&type_id) {
351 self.state_type_ids.push(type_id);
352 }
353 self
354 }
355
356 pub fn has_state<S: 'static>(&self) -> bool {
358 self.state_type_ids.contains(&std::any::TypeId::of::<S>())
359 }
360
361 pub fn state_type_ids(&self) -> &[std::any::TypeId] {
363 &self.state_type_ids
364 }
365
366 pub fn nest(mut self, prefix: &str, router: Router) -> Self {
402 let normalized_prefix = normalize_prefix(prefix);
404
405 for type_id in &router.state_type_ids {
409 if !self.state_type_ids.contains(type_id) {
410 self.state_type_ids.push(*type_id);
411 }
412 }
413
414 let nested_routes: Vec<(String, RouteInfo, MethodRouter)> = router
417 .registered_routes
418 .into_iter()
419 .filter_map(|(matchit_path, route_info)| {
420 router
421 .method_routers
422 .get(&matchit_path)
423 .map(|mr| (matchit_path, route_info, mr.clone()))
424 })
425 .collect();
426
427 for (matchit_path, route_info, method_router) in nested_routes {
429 let prefixed_matchit_path = if matchit_path == "/" {
433 normalized_prefix.clone()
434 } else {
435 format!("{}{}", normalized_prefix, matchit_path)
436 };
437
438 let prefixed_display_path = if route_info.path == "/" {
439 normalized_prefix.clone()
440 } else {
441 format!("{}{}", normalized_prefix, route_info.path)
442 };
443
444 self.method_routers
446 .insert(prefixed_matchit_path.clone(), method_router.clone());
447
448 match self.inner.insert(prefixed_matchit_path.clone(), method_router) {
450 Ok(_) => {
451 self.registered_routes.insert(
453 prefixed_matchit_path,
454 RouteInfo {
455 path: prefixed_display_path,
456 methods: route_info.methods,
457 },
458 );
459 }
460 Err(e) => {
461 self.method_routers.remove(&prefixed_matchit_path);
463
464 let existing_path = self
466 .find_conflicting_route(&prefixed_matchit_path)
467 .map(|info| info.path.clone())
468 .unwrap_or_else(|| "<unknown>".to_string());
469
470 let conflict_error = RouteConflictError {
471 new_path: prefixed_display_path,
472 method: route_info.methods.first().cloned(),
473 existing_path,
474 details: e.to_string(),
475 };
476
477 panic!("{}", conflict_error);
478 }
479 }
480 }
481
482 self
483 }
484
485 pub fn merge_state<S: Clone + Send + Sync + 'static>(mut self, other: &Router) -> Self {
502 let type_id = std::any::TypeId::of::<S>();
503
504 if !self.state_type_ids.contains(&type_id) {
506 if let Some(state) = other.state.get::<S>() {
508 let extensions = Arc::make_mut(&mut self.state);
509 extensions.insert(state.clone());
510 self.state_type_ids.push(type_id);
511 }
512 }
513
514 self
515 }
516
517 pub(crate) fn match_route(&self, path: &str, method: &Method) -> RouteMatch<'_> {
519 match self.inner.at(path) {
520 Ok(matched) => {
521 let method_router = matched.value;
522
523 if let Some(handler) = method_router.get_handler(method) {
524 let params: HashMap<String, String> = matched
526 .params
527 .iter()
528 .map(|(k, v)| (k.to_string(), v.to_string()))
529 .collect();
530
531 RouteMatch::Found { handler, params }
532 } else {
533 RouteMatch::MethodNotAllowed {
534 allowed: method_router.allowed_methods(),
535 }
536 }
537 }
538 Err(_) => RouteMatch::NotFound,
539 }
540 }
541
542 pub(crate) fn state_ref(&self) -> Arc<Extensions> {
544 self.state.clone()
545 }
546
547 pub fn registered_routes(&self) -> &HashMap<String, RouteInfo> {
549 &self.registered_routes
550 }
551
552 pub fn method_routers(&self) -> &HashMap<String, MethodRouter> {
554 &self.method_routers
555 }
556}
557
558impl Default for Router {
559 fn default() -> Self {
560 Self::new()
561 }
562}
563
564pub(crate) enum RouteMatch<'a> {
566 Found {
567 handler: &'a BoxedHandler,
568 params: HashMap<String, String>,
569 },
570 NotFound,
571 MethodNotAllowed {
572 allowed: Vec<Method>,
573 },
574}
575
576fn convert_path_params(path: &str) -> String {
578 let mut result = String::with_capacity(path.len());
579
580 for ch in path.chars() {
581 match ch {
582 '{' => {
583 result.push(':');
584 }
585 '}' => {
586 }
588 _ => {
589 result.push(ch);
590 }
591 }
592 }
593
594 result
595}
596
597fn normalize_path_for_comparison(path: &str) -> String {
599 let mut result = String::with_capacity(path.len());
600 let mut in_param = false;
601
602 for ch in path.chars() {
603 match ch {
604 ':' => {
605 in_param = true;
606 result.push_str(":_");
607 }
608 '/' => {
609 in_param = false;
610 result.push('/');
611 }
612 _ if in_param => {
613 }
615 _ => {
616 result.push(ch);
617 }
618 }
619 }
620
621 result
622}
623
624pub(crate) fn normalize_prefix(prefix: &str) -> String {
641 if prefix.is_empty() {
643 return "/".to_string();
644 }
645
646 let segments: Vec<&str> = prefix.split('/').filter(|s| !s.is_empty()).collect();
648
649 if segments.is_empty() {
651 return "/".to_string();
652 }
653
654 let mut result = String::with_capacity(prefix.len() + 1);
656 for segment in segments {
657 result.push('/');
658 result.push_str(segment);
659 }
660
661 result
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667
668 #[test]
669 fn test_convert_path_params() {
670 assert_eq!(convert_path_params("/users/{id}"), "/users/:id");
671 assert_eq!(
672 convert_path_params("/users/{user_id}/posts/{post_id}"),
673 "/users/:user_id/posts/:post_id"
674 );
675 assert_eq!(convert_path_params("/static/path"), "/static/path");
676 }
677
678 #[test]
679 fn test_normalize_path_for_comparison() {
680 assert_eq!(normalize_path_for_comparison("/users/:id"), "/users/:_");
681 assert_eq!(
682 normalize_path_for_comparison("/users/:user_id"),
683 "/users/:_"
684 );
685 assert_eq!(
686 normalize_path_for_comparison("/users/:id/posts/:post_id"),
687 "/users/:_/posts/:_"
688 );
689 assert_eq!(
690 normalize_path_for_comparison("/static/path"),
691 "/static/path"
692 );
693 }
694
695 #[test]
696 fn test_normalize_prefix() {
697 assert_eq!(normalize_prefix("api"), "/api");
699 assert_eq!(normalize_prefix("/api"), "/api");
700 assert_eq!(normalize_prefix("/api/"), "/api");
701 assert_eq!(normalize_prefix("api/"), "/api");
702
703 assert_eq!(normalize_prefix("api/v1"), "/api/v1");
705 assert_eq!(normalize_prefix("/api/v1"), "/api/v1");
706 assert_eq!(normalize_prefix("/api/v1/"), "/api/v1");
707
708 assert_eq!(normalize_prefix(""), "/");
710 assert_eq!(normalize_prefix("/"), "/");
711
712 assert_eq!(normalize_prefix("//api"), "/api");
714 assert_eq!(normalize_prefix("api//v1"), "/api/v1");
715 assert_eq!(normalize_prefix("//api//v1//"), "/api/v1");
716 assert_eq!(normalize_prefix("///"), "/");
717 }
718
719 #[test]
720 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
721 fn test_route_conflict_detection() {
722 async fn handler1() -> &'static str {
723 "handler1"
724 }
725 async fn handler2() -> &'static str {
726 "handler2"
727 }
728
729 let _router = Router::new()
730 .route("/users/{id}", get(handler1))
731 .route("/users/{user_id}", get(handler2)); }
733
734 #[test]
735 fn test_no_conflict_different_paths() {
736 async fn handler1() -> &'static str {
737 "handler1"
738 }
739 async fn handler2() -> &'static str {
740 "handler2"
741 }
742
743 let router = Router::new()
744 .route("/users/{id}", get(handler1))
745 .route("/users/{id}/profile", get(handler2));
746
747 assert_eq!(router.registered_routes().len(), 2);
748 }
749
750 #[test]
751 fn test_route_info_tracking() {
752 async fn handler() -> &'static str {
753 "handler"
754 }
755
756 let router = Router::new().route("/users/{id}", get(handler));
757
758 let routes = router.registered_routes();
759 assert_eq!(routes.len(), 1);
760
761 let info = routes.get("/users/:id").unwrap();
762 assert_eq!(info.path, "/users/{id}");
763 assert_eq!(info.methods.len(), 1);
764 assert_eq!(info.methods[0], Method::GET);
765 }
766
767 #[test]
768 fn test_basic_router_nesting() {
769 async fn list_users() -> &'static str {
770 "list users"
771 }
772 async fn get_user() -> &'static str {
773 "get user"
774 }
775
776 let users_router = Router::new()
777 .route("/", get(list_users))
778 .route("/{id}", get(get_user));
779
780 let app = Router::new().nest("/api/users", users_router);
781
782 let routes = app.registered_routes();
783 assert_eq!(routes.len(), 2);
784
785 assert!(routes.contains_key("/api/users"));
787 assert!(routes.contains_key("/api/users/:id"));
788
789 let list_info = routes.get("/api/users").unwrap();
791 assert_eq!(list_info.path, "/api/users");
792
793 let get_info = routes.get("/api/users/:id").unwrap();
794 assert_eq!(get_info.path, "/api/users/{id}");
795 }
796
797 #[test]
798 fn test_nested_route_matching() {
799 async fn handler() -> &'static str {
800 "handler"
801 }
802
803 let users_router = Router::new().route("/{id}", get(handler));
804
805 let app = Router::new().nest("/api/users", users_router);
806
807 match app.match_route("/api/users/123", &Method::GET) {
809 RouteMatch::Found { params, .. } => {
810 assert_eq!(params.get("id"), Some(&"123".to_string()));
811 }
812 _ => panic!("Route should be found"),
813 }
814 }
815
816 #[test]
817 fn test_nested_route_matching_multiple_params() {
818 async fn handler() -> &'static str {
819 "handler"
820 }
821
822 let posts_router = Router::new().route("/{user_id}/posts/{post_id}", get(handler));
823
824 let app = Router::new().nest("/api", posts_router);
825
826 match app.match_route("/api/42/posts/100", &Method::GET) {
828 RouteMatch::Found { params, .. } => {
829 assert_eq!(params.get("user_id"), Some(&"42".to_string()));
830 assert_eq!(params.get("post_id"), Some(&"100".to_string()));
831 }
832 _ => panic!("Route should be found"),
833 }
834 }
835
836 #[test]
837 fn test_nested_route_matching_static_path() {
838 async fn handler() -> &'static str {
839 "handler"
840 }
841
842 let health_router = Router::new().route("/health", get(handler));
843
844 let app = Router::new().nest("/api/v1", health_router);
845
846 match app.match_route("/api/v1/health", &Method::GET) {
848 RouteMatch::Found { params, .. } => {
849 assert!(params.is_empty(), "Static path should have no params");
850 }
851 _ => panic!("Route should be found"),
852 }
853 }
854
855 #[test]
856 fn test_nested_route_not_found() {
857 async fn handler() -> &'static str {
858 "handler"
859 }
860
861 let users_router = Router::new().route("/users", get(handler));
862
863 let app = Router::new().nest("/api", users_router);
864
865 match app.match_route("/api/posts", &Method::GET) {
867 RouteMatch::NotFound => {
868 }
870 _ => panic!("Route should not be found"),
871 }
872
873 match app.match_route("/v2/users", &Method::GET) {
875 RouteMatch::NotFound => {
876 }
878 _ => panic!("Route with wrong prefix should not be found"),
879 }
880 }
881
882 #[test]
883 fn test_nested_route_method_not_allowed() {
884 async fn handler() -> &'static str {
885 "handler"
886 }
887
888 let users_router = Router::new().route("/users", get(handler));
889
890 let app = Router::new().nest("/api", users_router);
891
892 match app.match_route("/api/users", &Method::POST) {
894 RouteMatch::MethodNotAllowed { allowed } => {
895 assert!(allowed.contains(&Method::GET));
896 assert!(!allowed.contains(&Method::POST));
897 }
898 _ => panic!("Should return MethodNotAllowed"),
899 }
900 }
901
902 #[test]
903 fn test_nested_route_multiple_methods() {
904 async fn get_handler() -> &'static str {
905 "get"
906 }
907 async fn post_handler() -> &'static str {
908 "post"
909 }
910
911 let get_router = get(get_handler);
913 let post_router = post(post_handler);
914 let mut combined = MethodRouter::new();
915 for (method, handler) in get_router.handlers {
916 combined.handlers.insert(method, handler);
917 }
918 for (method, handler) in post_router.handlers {
919 combined.handlers.insert(method, handler);
920 }
921
922 let users_router = Router::new().route("/users", combined);
923 let app = Router::new().nest("/api", users_router);
924
925 match app.match_route("/api/users", &Method::GET) {
927 RouteMatch::Found { .. } => {}
928 _ => panic!("GET should be found"),
929 }
930
931 match app.match_route("/api/users", &Method::POST) {
932 RouteMatch::Found { .. } => {}
933 _ => panic!("POST should be found"),
934 }
935
936 match app.match_route("/api/users", &Method::DELETE) {
938 RouteMatch::MethodNotAllowed { allowed } => {
939 assert!(allowed.contains(&Method::GET));
940 assert!(allowed.contains(&Method::POST));
941 }
942 _ => panic!("DELETE should return MethodNotAllowed"),
943 }
944 }
945
946 #[test]
947 fn test_nested_router_prefix_normalization() {
948 async fn handler() -> &'static str {
949 "handler"
950 }
951
952 let router1 = Router::new().route("/test", get(handler));
954 let app1 = Router::new().nest("api", router1);
955 assert!(app1.registered_routes().contains_key("/api/test"));
956
957 let router2 = Router::new().route("/test", get(handler));
958 let app2 = Router::new().nest("/api/", router2);
959 assert!(app2.registered_routes().contains_key("/api/test"));
960
961 let router3 = Router::new().route("/test", get(handler));
962 let app3 = Router::new().nest("//api//", router3);
963 assert!(app3.registered_routes().contains_key("/api/test"));
964 }
965
966 #[test]
967 fn test_state_tracking() {
968 #[derive(Clone)]
969 struct MyState(String);
970
971 let router = Router::new().state(MyState("test".to_string()));
972
973 assert!(router.has_state::<MyState>());
974 assert!(!router.has_state::<String>());
975 }
976
977 #[test]
978 fn test_state_merge_nested_only() {
979 #[derive(Clone, PartialEq, Debug)]
980 struct NestedState(String);
981
982 async fn handler() -> &'static str {
983 "handler"
984 }
985
986 let state_source = Router::new().state(NestedState("nested".to_string()));
988
989 let nested = Router::new().route("/test", get(handler));
990
991 let parent = Router::new()
992 .nest("/api", nested)
993 .merge_state::<NestedState>(&state_source);
994
995 assert!(parent.has_state::<NestedState>());
997
998 let state = parent.state.get::<NestedState>().unwrap();
1000 assert_eq!(state.0, "nested");
1001 }
1002
1003 #[test]
1004 fn test_state_merge_parent_wins() {
1005 #[derive(Clone, PartialEq, Debug)]
1006 struct SharedState(String);
1007
1008 async fn handler() -> &'static str {
1009 "handler"
1010 }
1011
1012 let state_source = Router::new().state(SharedState("nested".to_string()));
1014
1015 let nested = Router::new().route("/test", get(handler));
1016
1017 let parent = Router::new()
1018 .state(SharedState("parent".to_string()))
1019 .nest("/api", nested)
1020 .merge_state::<SharedState>(&state_source);
1021
1022 assert!(parent.has_state::<SharedState>());
1024
1025 let state = parent.state.get::<SharedState>().unwrap();
1027 assert_eq!(state.0, "parent");
1028 }
1029
1030 #[test]
1031 fn test_state_type_ids_merged_on_nest() {
1032 #[derive(Clone)]
1033 struct NestedState(String);
1034
1035 async fn handler() -> &'static str {
1036 "handler"
1037 }
1038
1039 let nested = Router::new()
1040 .route("/test", get(handler))
1041 .state(NestedState("nested".to_string()));
1042
1043 let parent = Router::new().nest("/api", nested);
1044
1045 assert!(parent.state_type_ids().contains(&std::any::TypeId::of::<NestedState>()));
1047 }
1048
1049 #[test]
1050 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
1051 fn test_nested_route_conflict_with_existing_route() {
1052 async fn handler1() -> &'static str {
1053 "handler1"
1054 }
1055 async fn handler2() -> &'static str {
1056 "handler2"
1057 }
1058
1059 let parent = Router::new().route("/api/users/{id}", get(handler1));
1061
1062 let nested = Router::new().route("/{user_id}", get(handler2));
1064
1065 let _app = parent.nest("/api/users", nested);
1067 }
1068
1069 #[test]
1070 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
1071 fn test_nested_route_conflict_same_path_different_param_names() {
1072 async fn handler1() -> &'static str {
1073 "handler1"
1074 }
1075 async fn handler2() -> &'static str {
1076 "handler2"
1077 }
1078
1079 let nested1 = Router::new().route("/{id}", get(handler1));
1081 let nested2 = Router::new().route("/{user_id}", get(handler2));
1082
1083 let _app = Router::new()
1085 .nest("/api/users", nested1)
1086 .nest("/api/users", nested2);
1087 }
1088
1089 #[test]
1090 fn test_nested_route_conflict_error_contains_both_paths() {
1091 use std::panic::{catch_unwind, AssertUnwindSafe};
1092
1093 async fn handler1() -> &'static str {
1094 "handler1"
1095 }
1096 async fn handler2() -> &'static str {
1097 "handler2"
1098 }
1099
1100 let result = catch_unwind(AssertUnwindSafe(|| {
1101 let parent = Router::new().route("/api/users/{id}", get(handler1));
1102 let nested = Router::new().route("/{user_id}", get(handler2));
1103 let _app = parent.nest("/api/users", nested);
1104 }));
1105
1106 assert!(result.is_err(), "Should have panicked due to conflict");
1107
1108 if let Err(panic_info) = result {
1109 if let Some(msg) = panic_info.downcast_ref::<String>() {
1110 assert!(
1111 msg.contains("ROUTE CONFLICT DETECTED"),
1112 "Error should contain 'ROUTE CONFLICT DETECTED'"
1113 );
1114 assert!(
1115 msg.contains("Existing:") && msg.contains("New:"),
1116 "Error should contain both 'Existing:' and 'New:' labels"
1117 );
1118 assert!(
1119 msg.contains("How to resolve:"),
1120 "Error should contain resolution guidance"
1121 );
1122 }
1123 }
1124 }
1125
1126 #[test]
1127 fn test_nested_routes_no_conflict_different_prefixes() {
1128 async fn handler1() -> &'static str {
1129 "handler1"
1130 }
1131 async fn handler2() -> &'static str {
1132 "handler2"
1133 }
1134
1135 let nested1 = Router::new().route("/{id}", get(handler1));
1137 let nested2 = Router::new().route("/{id}", get(handler2));
1138
1139 let app = Router::new()
1141 .nest("/api/users", nested1)
1142 .nest("/api/posts", nested2);
1143
1144 assert_eq!(app.registered_routes().len(), 2);
1145 assert!(app.registered_routes().contains_key("/api/users/:id"));
1146 assert!(app.registered_routes().contains_key("/api/posts/:id"));
1147 }
1148
1149 #[test]
1154 fn test_multiple_router_composition_all_routes_registered() {
1155 async fn users_list() -> &'static str { "users list" }
1156 async fn users_get() -> &'static str { "users get" }
1157 async fn posts_list() -> &'static str { "posts list" }
1158 async fn posts_get() -> &'static str { "posts get" }
1159 async fn comments_list() -> &'static str { "comments list" }
1160
1161 let users_router = Router::new()
1163 .route("/", get(users_list))
1164 .route("/{id}", get(users_get));
1165
1166 let posts_router = Router::new()
1167 .route("/", get(posts_list))
1168 .route("/{id}", get(posts_get));
1169
1170 let comments_router = Router::new()
1171 .route("/", get(comments_list));
1172
1173 let app = Router::new()
1175 .nest("/api/users", users_router)
1176 .nest("/api/posts", posts_router)
1177 .nest("/api/comments", comments_router);
1178
1179 let routes = app.registered_routes();
1181 assert_eq!(routes.len(), 5, "Should have 5 routes registered");
1182
1183 assert!(routes.contains_key("/api/users"), "Should have /api/users route");
1185 assert!(routes.contains_key("/api/users/:id"), "Should have /api/users/:id route");
1186
1187 assert!(routes.contains_key("/api/posts"), "Should have /api/posts route");
1189 assert!(routes.contains_key("/api/posts/:id"), "Should have /api/posts/:id route");
1190
1191 assert!(routes.contains_key("/api/comments"), "Should have /api/comments route");
1193 }
1194
1195 #[test]
1196 fn test_multiple_router_composition_no_interference() {
1197 async fn users_handler() -> &'static str { "users" }
1198 async fn posts_handler() -> &'static str { "posts" }
1199 async fn admin_handler() -> &'static str { "admin" }
1200
1201 let users_router = Router::new()
1203 .route("/list", get(users_handler))
1204 .route("/{id}", get(users_handler));
1205
1206 let posts_router = Router::new()
1207 .route("/list", get(posts_handler))
1208 .route("/{id}", get(posts_handler));
1209
1210 let admin_router = Router::new()
1211 .route("/list", get(admin_handler))
1212 .route("/{id}", get(admin_handler));
1213
1214 let app = Router::new()
1216 .nest("/api/v1/users", users_router)
1217 .nest("/api/v1/posts", posts_router)
1218 .nest("/admin", admin_router);
1219
1220 let routes = app.registered_routes();
1222 assert_eq!(routes.len(), 6, "Should have 6 routes registered");
1223
1224 assert!(routes.contains_key("/api/v1/users/list"));
1226 assert!(routes.contains_key("/api/v1/users/:id"));
1227 assert!(routes.contains_key("/api/v1/posts/list"));
1228 assert!(routes.contains_key("/api/v1/posts/:id"));
1229 assert!(routes.contains_key("/admin/list"));
1230 assert!(routes.contains_key("/admin/:id"));
1231
1232 match app.match_route("/api/v1/users/list", &Method::GET) {
1234 RouteMatch::Found { params, .. } => {
1235 assert!(params.is_empty(), "Static path should have no params");
1236 }
1237 _ => panic!("Should find /api/v1/users/list"),
1238 }
1239
1240 match app.match_route("/api/v1/posts/123", &Method::GET) {
1241 RouteMatch::Found { params, .. } => {
1242 assert_eq!(params.get("id"), Some(&"123".to_string()));
1243 }
1244 _ => panic!("Should find /api/v1/posts/123"),
1245 }
1246
1247 match app.match_route("/admin/456", &Method::GET) {
1248 RouteMatch::Found { params, .. } => {
1249 assert_eq!(params.get("id"), Some(&"456".to_string()));
1250 }
1251 _ => panic!("Should find /admin/456"),
1252 }
1253 }
1254
1255 #[test]
1256 fn test_multiple_router_composition_with_multiple_methods() {
1257 async fn get_handler() -> &'static str { "get" }
1258 async fn post_handler() -> &'static str { "post" }
1259 async fn put_handler() -> &'static str { "put" }
1260
1261 let get_router = get(get_handler);
1264 let post_router = post(post_handler);
1265 let mut users_root_combined = MethodRouter::new();
1266 for (method, handler) in get_router.handlers {
1267 users_root_combined.handlers.insert(method, handler);
1268 }
1269 for (method, handler) in post_router.handlers {
1270 users_root_combined.handlers.insert(method, handler);
1271 }
1272
1273 let get_router2 = get(get_handler);
1275 let put_router = put(put_handler);
1276 let mut users_id_combined = MethodRouter::new();
1277 for (method, handler) in get_router2.handlers {
1278 users_id_combined.handlers.insert(method, handler);
1279 }
1280 for (method, handler) in put_router.handlers {
1281 users_id_combined.handlers.insert(method, handler);
1282 }
1283
1284 let users_router = Router::new()
1285 .route("/", users_root_combined)
1286 .route("/{id}", users_id_combined);
1287
1288 let get_router3 = get(get_handler);
1290 let post_router2 = post(post_handler);
1291 let mut posts_root_combined = MethodRouter::new();
1292 for (method, handler) in get_router3.handlers {
1293 posts_root_combined.handlers.insert(method, handler);
1294 }
1295 for (method, handler) in post_router2.handlers {
1296 posts_root_combined.handlers.insert(method, handler);
1297 }
1298
1299 let posts_router = Router::new()
1300 .route("/", posts_root_combined);
1301
1302 let app = Router::new()
1304 .nest("/users", users_router)
1305 .nest("/posts", posts_router);
1306
1307 let routes = app.registered_routes();
1309 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1310
1311 let users_root = routes.get("/users").unwrap();
1313 assert!(users_root.methods.contains(&Method::GET));
1314 assert!(users_root.methods.contains(&Method::POST));
1315
1316 let users_id = routes.get("/users/:id").unwrap();
1317 assert!(users_id.methods.contains(&Method::GET));
1318 assert!(users_id.methods.contains(&Method::PUT));
1319
1320 let posts_root = routes.get("/posts").unwrap();
1322 assert!(posts_root.methods.contains(&Method::GET));
1323 assert!(posts_root.methods.contains(&Method::POST));
1324
1325 match app.match_route("/users", &Method::GET) {
1327 RouteMatch::Found { .. } => {}
1328 _ => panic!("GET /users should be found"),
1329 }
1330 match app.match_route("/users", &Method::POST) {
1331 RouteMatch::Found { .. } => {}
1332 _ => panic!("POST /users should be found"),
1333 }
1334 match app.match_route("/users/123", &Method::PUT) {
1335 RouteMatch::Found { .. } => {}
1336 _ => panic!("PUT /users/123 should be found"),
1337 }
1338 }
1339
1340 #[test]
1341 fn test_multiple_router_composition_deep_nesting() {
1342 async fn handler() -> &'static str { "handler" }
1343
1344 let deep_router = Router::new()
1346 .route("/action", get(handler));
1347
1348 let mid_router = Router::new()
1349 .route("/info", get(handler));
1350
1351 let shallow_router = Router::new()
1352 .route("/status", get(handler));
1353
1354 let app = Router::new()
1356 .nest("/api/v1/resources/items", deep_router)
1357 .nest("/api/v1/resources", mid_router)
1358 .nest("/api", shallow_router);
1359
1360 let routes = app.registered_routes();
1362 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1363
1364 assert!(routes.contains_key("/api/v1/resources/items/action"));
1365 assert!(routes.contains_key("/api/v1/resources/info"));
1366 assert!(routes.contains_key("/api/status"));
1367
1368 match app.match_route("/api/v1/resources/items/action", &Method::GET) {
1370 RouteMatch::Found { .. } => {}
1371 _ => panic!("Should find deep route"),
1372 }
1373 match app.match_route("/api/v1/resources/info", &Method::GET) {
1374 RouteMatch::Found { .. } => {}
1375 _ => panic!("Should find mid route"),
1376 }
1377 match app.match_route("/api/status", &Method::GET) {
1378 RouteMatch::Found { .. } => {}
1379 _ => panic!("Should find shallow route"),
1380 }
1381 }
1382}
1383
1384#[cfg(test)]
1385mod property_tests {
1386 use super::*;
1387 use proptest::prelude::*;
1388 use std::panic::{catch_unwind, AssertUnwindSafe};
1389
1390 proptest! {
1398 #![proptest_config(ProptestConfig::with_cases(100))]
1399
1400 #[test]
1405 fn prop_normalized_prefix_starts_with_single_slash(
1406 leading_slashes in prop::collection::vec(Just('/'), 0..5),
1408 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 0..4),
1409 trailing_slashes in prop::collection::vec(Just('/'), 0..5),
1410 ) {
1411 let mut prefix = String::new();
1413 for _ in &leading_slashes {
1414 prefix.push('/');
1415 }
1416 for (i, segment) in segments.iter().enumerate() {
1417 if i > 0 {
1418 prefix.push('/');
1419 }
1420 prefix.push_str(segment);
1421 }
1422 for _ in &trailing_slashes {
1423 prefix.push('/');
1424 }
1425
1426 let normalized = normalize_prefix(&prefix);
1427
1428 prop_assert!(
1430 normalized.starts_with('/'),
1431 "Normalized prefix '{}' should start with '/', input was '{}'",
1432 normalized, prefix
1433 );
1434
1435 prop_assert!(
1437 !normalized.starts_with("//"),
1438 "Normalized prefix '{}' should not start with '//', input was '{}'",
1439 normalized, prefix
1440 );
1441 }
1442
1443 #[test]
1448 fn prop_normalized_prefix_no_trailing_slash(
1449 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1450 trailing_slashes in prop::collection::vec(Just('/'), 0..5),
1451 ) {
1452 let mut prefix = String::from("/");
1454 for (i, segment) in segments.iter().enumerate() {
1455 if i > 0 {
1456 prefix.push('/');
1457 }
1458 prefix.push_str(segment);
1459 }
1460 for _ in &trailing_slashes {
1461 prefix.push('/');
1462 }
1463
1464 let normalized = normalize_prefix(&prefix);
1465
1466 prop_assert!(
1468 !normalized.ends_with('/'),
1469 "Normalized prefix '{}' should not end with '/', input was '{}'",
1470 normalized, prefix
1471 );
1472 }
1473
1474 #[test]
1479 fn prop_normalized_prefix_no_double_slashes(
1480 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1482 extra_slashes in prop::collection::vec(0..4usize, 1..4),
1483 ) {
1484 let mut prefix = String::from("/");
1486 for (i, segment) in segments.iter().enumerate() {
1487 if i > 0 {
1488 let num_slashes = extra_slashes.get(i).copied().unwrap_or(1);
1490 for _ in 0..=num_slashes {
1491 prefix.push('/');
1492 }
1493 }
1494 prefix.push_str(segment);
1495 }
1496
1497 let normalized = normalize_prefix(&prefix);
1498
1499 prop_assert!(
1501 !normalized.contains("//"),
1502 "Normalized prefix '{}' should not contain '//', input was '{}'",
1503 normalized, prefix
1504 );
1505 }
1506
1507 #[test]
1512 fn prop_normalized_prefix_preserves_segments(
1513 segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..4),
1514 ) {
1515 let prefix = format!("/{}", segments.join("/"));
1517
1518 let normalized = normalize_prefix(&prefix);
1519
1520 let normalized_segments: Vec<&str> = normalized
1522 .split('/')
1523 .filter(|s| !s.is_empty())
1524 .collect();
1525
1526 prop_assert_eq!(
1527 segments.len(),
1528 normalized_segments.len(),
1529 "Segment count should be preserved"
1530 );
1531
1532 for (original, normalized_seg) in segments.iter().zip(normalized_segments.iter()) {
1533 prop_assert_eq!(
1534 original, normalized_seg,
1535 "Segment content should be preserved"
1536 );
1537 }
1538 }
1539
1540 #[test]
1545 fn prop_empty_or_slashes_normalize_to_root(
1546 num_slashes in 0..10usize,
1547 ) {
1548 let prefix: String = std::iter::repeat('/').take(num_slashes).collect();
1549
1550 let normalized = normalize_prefix(&prefix);
1551
1552 prop_assert_eq!(
1553 normalized, "/",
1554 "Empty or slash-only prefix '{}' should normalize to '/'",
1555 prefix
1556 );
1557 }
1558 }
1559
1560 proptest! {
1567 #![proptest_config(ProptestConfig::with_cases(100))]
1568
1569 #[test]
1574 fn prop_method_router_clone_preserves_methods(
1575 use_get in any::<bool>(),
1577 use_post in any::<bool>(),
1578 use_put in any::<bool>(),
1579 use_patch in any::<bool>(),
1580 use_delete in any::<bool>(),
1581 ) {
1582 prop_assume!(use_get || use_post || use_put || use_patch || use_delete);
1584
1585 let mut method_router = MethodRouter::new();
1587 let mut expected_methods: Vec<Method> = Vec::new();
1588
1589 async fn handler() -> &'static str { "handler" }
1590
1591 if use_get {
1592 method_router = get(handler);
1593 expected_methods.push(Method::GET);
1594 }
1595
1596 if use_post {
1597 let post_router = post(handler);
1598 for (method, handler) in post_router.handlers {
1599 method_router.handlers.insert(method.clone(), handler);
1600 if !expected_methods.contains(&method) {
1601 expected_methods.push(method);
1602 }
1603 }
1604 }
1605
1606 if use_put {
1607 let put_router = put(handler);
1608 for (method, handler) in put_router.handlers {
1609 method_router.handlers.insert(method.clone(), handler);
1610 if !expected_methods.contains(&method) {
1611 expected_methods.push(method);
1612 }
1613 }
1614 }
1615
1616 if use_patch {
1617 let patch_router = patch(handler);
1618 for (method, handler) in patch_router.handlers {
1619 method_router.handlers.insert(method.clone(), handler);
1620 if !expected_methods.contains(&method) {
1621 expected_methods.push(method);
1622 }
1623 }
1624 }
1625
1626 if use_delete {
1627 let delete_router = delete(handler);
1628 for (method, handler) in delete_router.handlers {
1629 method_router.handlers.insert(method.clone(), handler);
1630 if !expected_methods.contains(&method) {
1631 expected_methods.push(method);
1632 }
1633 }
1634 }
1635
1636 let cloned_router = method_router.clone();
1638
1639 let original_methods = method_router.allowed_methods();
1641 let cloned_methods = cloned_router.allowed_methods();
1642
1643 prop_assert_eq!(
1644 original_methods.len(),
1645 cloned_methods.len(),
1646 "Cloned router should have same number of methods"
1647 );
1648
1649 for method in &expected_methods {
1650 prop_assert!(
1651 cloned_router.get_handler(method).is_some(),
1652 "Cloned router should have handler for method {:?}",
1653 method
1654 );
1655 }
1656
1657 for method in &cloned_methods {
1659 prop_assert!(
1660 cloned_router.get_handler(method).is_some(),
1661 "Handler for {:?} should be accessible after clone",
1662 method
1663 );
1664 }
1665 }
1666 }
1667
1668 proptest! {
1676 #![proptest_config(ProptestConfig::with_cases(100))]
1677
1678 #[test]
1683 fn prop_nested_routes_have_prefix(
1684 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1686 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1688 has_param in any::<bool>(),
1689 ) {
1690 async fn handler() -> &'static str { "handler" }
1691
1692 let prefix = format!("/{}", prefix_segments.join("/"));
1694
1695 let mut route_path = format!("/{}", route_segments.join("/"));
1697 if has_param {
1698 route_path.push_str("/{id}");
1699 }
1700
1701 let nested_router = Router::new().route(&route_path, get(handler));
1703 let app = Router::new().nest(&prefix, nested_router);
1704
1705 let expected_matchit_path = if has_param {
1707 format!("{}/{}/:id", prefix, route_segments.join("/"))
1708 } else {
1709 format!("{}/{}", prefix, route_segments.join("/"))
1710 };
1711
1712 let routes = app.registered_routes();
1713
1714 prop_assert!(
1716 routes.contains_key(&expected_matchit_path),
1717 "Expected route '{}' not found. Available routes: {:?}",
1718 expected_matchit_path,
1719 routes.keys().collect::<Vec<_>>()
1720 );
1721
1722 let route_info = routes.get(&expected_matchit_path).unwrap();
1724 let expected_display_path = format!("{}{}", prefix, route_path);
1725 prop_assert_eq!(
1726 &route_info.path, &expected_display_path,
1727 "Display path should be prefix + original path"
1728 );
1729 }
1730
1731 #[test]
1736 fn prop_route_count_preserved_after_nesting(
1737 num_routes in 1..4usize,
1739 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1740 ) {
1741 async fn handler() -> &'static str { "handler" }
1742
1743 let prefix = format!("/{}", prefix_segments.join("/"));
1744
1745 let mut nested_router = Router::new();
1747 for i in 0..num_routes {
1748 let path = format!("/route{}", i);
1749 nested_router = nested_router.route(&path, get(handler));
1750 }
1751
1752 let app = Router::new().nest(&prefix, nested_router);
1753
1754 prop_assert_eq!(
1755 app.registered_routes().len(),
1756 num_routes,
1757 "Number of routes should be preserved after nesting"
1758 );
1759 }
1760
1761 #[test]
1765 fn prop_nested_routes_are_matchable(
1766 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1767 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1768 ) {
1769 async fn handler() -> &'static str { "handler" }
1770
1771 let prefix = format!("/{}", prefix_segments.join("/"));
1772 let route_path = format!("/{}", route_segments.join("/"));
1773
1774 let nested_router = Router::new().route(&route_path, get(handler));
1775 let app = Router::new().nest(&prefix, nested_router);
1776
1777 let full_path = format!("{}{}", prefix, route_path);
1779
1780 match app.match_route(&full_path, &Method::GET) {
1782 RouteMatch::Found { .. } => {
1783 }
1785 RouteMatch::NotFound => {
1786 prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
1787 }
1788 RouteMatch::MethodNotAllowed { .. } => {
1789 prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
1790 }
1791 }
1792 }
1793 }
1794
1795 proptest! {
1802 #![proptest_config(ProptestConfig::with_cases(100))]
1803
1804 #[test]
1809 fn prop_state_type_ids_merged(
1810 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1811 has_nested_state in any::<bool>(),
1812 ) {
1813 #[derive(Clone)]
1814 struct TestState(i32);
1815
1816 async fn handler() -> &'static str { "handler" }
1817
1818 let prefix = format!("/{}", prefix_segments.join("/"));
1819
1820 let mut nested = Router::new().route("/test", get(handler));
1821 if has_nested_state {
1822 nested = nested.state(TestState(42));
1823 }
1824
1825 let parent = Router::new().nest(&prefix, nested);
1826
1827 if has_nested_state {
1829 prop_assert!(
1830 parent.state_type_ids().contains(&std::any::TypeId::of::<TestState>()),
1831 "Parent should track nested state type ID"
1832 );
1833 }
1834 }
1835
1836 #[test]
1841 fn prop_merge_state_adds_nested_state(
1842 state_value in any::<i32>(),
1843 ) {
1844 #[derive(Clone, PartialEq, Debug)]
1845 struct UniqueState(i32);
1846
1847 let source = Router::new().state(UniqueState(state_value));
1849
1850 let parent = Router::new().merge_state::<UniqueState>(&source);
1852
1853 prop_assert!(
1855 parent.has_state::<UniqueState>(),
1856 "Parent should have state after merge"
1857 );
1858
1859 let merged_state = parent.state.get::<UniqueState>().unwrap();
1861 prop_assert_eq!(
1862 merged_state.0, state_value,
1863 "Merged state value should match source"
1864 );
1865 }
1866 }
1867
1868 proptest! {
1875 #![proptest_config(ProptestConfig::with_cases(100))]
1876
1877 #[test]
1882 fn prop_parent_state_takes_precedence(
1883 parent_value in any::<i32>(),
1884 nested_value in any::<i32>(),
1885 ) {
1886 prop_assume!(parent_value != nested_value);
1888
1889 #[derive(Clone, PartialEq, Debug)]
1890 struct SharedState(i32);
1891
1892 let source = Router::new().state(SharedState(nested_value));
1894
1895 let parent = Router::new()
1897 .state(SharedState(parent_value))
1898 .merge_state::<SharedState>(&source);
1899
1900 prop_assert!(
1902 parent.has_state::<SharedState>(),
1903 "Parent should have state"
1904 );
1905
1906 let final_state = parent.state.get::<SharedState>().unwrap();
1908 prop_assert_eq!(
1909 final_state.0, parent_value,
1910 "Parent state value should be preserved, not overwritten by nested"
1911 );
1912 }
1913
1914 #[test]
1919 fn prop_state_precedence_consistent(
1920 parent_value in any::<i32>(),
1921 source1_value in any::<i32>(),
1922 source2_value in any::<i32>(),
1923 ) {
1924 #[derive(Clone, PartialEq, Debug)]
1925 struct ConsistentState(i32);
1926
1927 let source1 = Router::new().state(ConsistentState(source1_value));
1929 let source2 = Router::new().state(ConsistentState(source2_value));
1930
1931 let parent = Router::new()
1933 .state(ConsistentState(parent_value))
1934 .merge_state::<ConsistentState>(&source1)
1935 .merge_state::<ConsistentState>(&source2);
1936
1937 let final_state = parent.state.get::<ConsistentState>().unwrap();
1939 prop_assert_eq!(
1940 final_state.0, parent_value,
1941 "Parent state should be preserved after multiple merges"
1942 );
1943 }
1944 }
1945
1946 proptest! {
1954 #![proptest_config(ProptestConfig::with_cases(100))]
1955
1956 #[test]
1961 fn prop_same_structure_different_param_names_conflict(
1962 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1964 param1 in "[a-z][a-z0-9]{0,5}",
1966 param2 in "[a-z][a-z0-9]{0,5}",
1967 ) {
1968 prop_assume!(param1 != param2);
1970
1971 let mut path1 = String::from("/");
1973 let mut path2 = String::from("/");
1974
1975 for segment in &segments {
1976 path1.push_str(segment);
1977 path1.push('/');
1978 path2.push_str(segment);
1979 path2.push('/');
1980 }
1981
1982 path1.push('{');
1983 path1.push_str(¶m1);
1984 path1.push('}');
1985
1986 path2.push('{');
1987 path2.push_str(¶m2);
1988 path2.push('}');
1989
1990 let result = catch_unwind(AssertUnwindSafe(|| {
1992 async fn handler1() -> &'static str { "handler1" }
1993 async fn handler2() -> &'static str { "handler2" }
1994
1995 let _router = Router::new()
1996 .route(&path1, get(handler1))
1997 .route(&path2, get(handler2));
1998 }));
1999
2000 prop_assert!(
2001 result.is_err(),
2002 "Routes '{}' and '{}' should conflict but didn't",
2003 path1, path2
2004 );
2005 }
2006
2007 #[test]
2012 fn prop_different_structures_no_conflict(
2013 segments1 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2015 segments2 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2016 has_param1 in any::<bool>(),
2018 has_param2 in any::<bool>(),
2019 ) {
2020 let mut path1 = String::from("/");
2022 let mut path2 = String::from("/");
2023
2024 for segment in &segments1 {
2025 path1.push_str(segment);
2026 path1.push('/');
2027 }
2028 path1.pop(); for segment in &segments2 {
2031 path2.push_str(segment);
2032 path2.push('/');
2033 }
2034 path2.pop(); if has_param1 {
2037 path1.push_str("/{id}");
2038 }
2039
2040 if has_param2 {
2041 path2.push_str("/{id}");
2042 }
2043
2044 let norm1 = normalize_path_for_comparison(&convert_path_params(&path1));
2046 let norm2 = normalize_path_for_comparison(&convert_path_params(&path2));
2047
2048 prop_assume!(norm1 != norm2);
2050
2051 let result = catch_unwind(AssertUnwindSafe(|| {
2053 async fn handler1() -> &'static str { "handler1" }
2054 async fn handler2() -> &'static str { "handler2" }
2055
2056 let router = Router::new()
2057 .route(&path1, get(handler1))
2058 .route(&path2, get(handler2));
2059
2060 router.registered_routes().len()
2061 }));
2062
2063 prop_assert!(
2064 result.is_ok(),
2065 "Routes '{}' and '{}' should not conflict but did",
2066 path1, path2
2067 );
2068
2069 if let Ok(count) = result {
2070 prop_assert_eq!(count, 2, "Should have registered 2 routes");
2071 }
2072 }
2073
2074 #[test]
2079 fn prop_conflict_error_contains_both_paths(
2080 segment in "[a-z][a-z0-9]{1,5}",
2082 param1 in "[a-z][a-z0-9]{1,5}",
2083 param2 in "[a-z][a-z0-9]{1,5}",
2084 ) {
2085 prop_assume!(param1 != param2);
2086
2087 let path1 = format!("/{}/{{{}}}", segment, param1);
2088 let path2 = format!("/{}/{{{}}}", segment, param2);
2089
2090 let result = catch_unwind(AssertUnwindSafe(|| {
2091 async fn handler1() -> &'static str { "handler1" }
2092 async fn handler2() -> &'static str { "handler2" }
2093
2094 let _router = Router::new()
2095 .route(&path1, get(handler1))
2096 .route(&path2, get(handler2));
2097 }));
2098
2099 prop_assert!(result.is_err(), "Should have panicked due to conflict");
2100
2101 if let Err(panic_info) = result {
2103 if let Some(msg) = panic_info.downcast_ref::<String>() {
2104 prop_assert!(
2105 msg.contains("ROUTE CONFLICT DETECTED"),
2106 "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
2107 msg
2108 );
2109 prop_assert!(
2110 msg.contains("Existing:") && msg.contains("New:"),
2111 "Error should contain both 'Existing:' and 'New:' labels, got: {}",
2112 msg
2113 );
2114 prop_assert!(
2115 msg.contains("How to resolve:"),
2116 "Error should contain resolution guidance, got: {}",
2117 msg
2118 );
2119 }
2120 }
2121 }
2122
2123 #[test]
2127 fn prop_exact_duplicate_paths_conflict(
2128 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
2130 has_param in any::<bool>(),
2131 ) {
2132 let mut path = String::from("/");
2134
2135 for segment in &segments {
2136 path.push_str(segment);
2137 path.push('/');
2138 }
2139 path.pop(); if has_param {
2142 path.push_str("/{id}");
2143 }
2144
2145 let result = catch_unwind(AssertUnwindSafe(|| {
2147 async fn handler1() -> &'static str { "handler1" }
2148 async fn handler2() -> &'static str { "handler2" }
2149
2150 let _router = Router::new()
2151 .route(&path, get(handler1))
2152 .route(&path, get(handler2));
2153 }));
2154
2155 prop_assert!(
2156 result.is_err(),
2157 "Registering path '{}' twice should conflict but didn't",
2158 path
2159 );
2160 }
2161 }
2162
2163 proptest! {
2170 #![proptest_config(ProptestConfig::with_cases(100))]
2171
2172 #[test]
2177 fn prop_nested_route_with_params_matches(
2178 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2179 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
2180 param_value in "[a-z0-9]{1,10}",
2181 ) {
2182 async fn handler() -> &'static str { "handler" }
2183
2184 let prefix = format!("/{}", prefix_segments.join("/"));
2185 let route_path = if route_segments.is_empty() {
2186 "/{id}".to_string()
2187 } else {
2188 format!("/{}/{{id}}", route_segments.join("/"))
2189 };
2190
2191 let nested_router = Router::new().route(&route_path, get(handler));
2192 let app = Router::new().nest(&prefix, nested_router);
2193
2194 let full_path = if route_segments.is_empty() {
2196 format!("{}/{}", prefix, param_value)
2197 } else {
2198 format!("{}/{}/{}", prefix, route_segments.join("/"), param_value)
2199 };
2200
2201 match app.match_route(&full_path, &Method::GET) {
2203 RouteMatch::Found { params, .. } => {
2204 prop_assert!(
2206 params.contains_key("id"),
2207 "Should have 'id' parameter, got: {:?}",
2208 params
2209 );
2210 prop_assert_eq!(
2211 params.get("id").unwrap(),
2212 ¶m_value,
2213 "Parameter value should match"
2214 );
2215 }
2216 RouteMatch::NotFound => {
2217 prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
2218 }
2219 RouteMatch::MethodNotAllowed { .. } => {
2220 prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
2221 }
2222 }
2223 }
2224
2225 #[test]
2230 fn prop_nested_route_matches_correct_method(
2231 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2232 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2233 use_get in any::<bool>(),
2234 ) {
2235 async fn handler() -> &'static str { "handler" }
2236
2237 let prefix = format!("/{}", prefix_segments.join("/"));
2238 let route_path = format!("/{}", route_segments.join("/"));
2239
2240 let method_router = if use_get { get(handler) } else { post(handler) };
2242 let nested_router = Router::new().route(&route_path, method_router);
2243 let app = Router::new().nest(&prefix, nested_router);
2244
2245 let full_path = format!("{}{}", prefix, route_path);
2246 let registered_method = if use_get { Method::GET } else { Method::POST };
2247 let other_method = if use_get { Method::POST } else { Method::GET };
2248
2249 match app.match_route(&full_path, ®istered_method) {
2251 RouteMatch::Found { .. } => {
2252 }
2254 other => {
2255 prop_assert!(false, "Route should be found for registered method, got: {:?}",
2256 match other {
2257 RouteMatch::NotFound => "NotFound",
2258 RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
2259 _ => "Found",
2260 }
2261 );
2262 }
2263 }
2264
2265 match app.match_route(&full_path, &other_method) {
2267 RouteMatch::MethodNotAllowed { allowed } => {
2268 prop_assert!(
2269 allowed.contains(®istered_method),
2270 "Allowed methods should contain {:?}",
2271 registered_method
2272 );
2273 }
2274 other => {
2275 prop_assert!(false, "Route should return MethodNotAllowed for other method, got: {:?}",
2276 match other {
2277 RouteMatch::NotFound => "NotFound",
2278 RouteMatch::Found { .. } => "Found",
2279 _ => "MethodNotAllowed",
2280 }
2281 );
2282 }
2283 }
2284 }
2285 }
2286
2287 proptest! {
2294 #![proptest_config(ProptestConfig::with_cases(100))]
2295
2296 #[test]
2301 fn prop_single_param_extraction(
2302 prefix in "[a-z][a-z0-9]{1,5}",
2303 param_name in "[a-z][a-z0-9]{1,5}",
2304 param_value in "[a-z0-9]{1,10}",
2305 ) {
2306 async fn handler() -> &'static str { "handler" }
2307
2308 let prefix = format!("/{}", prefix);
2309 let route_path = format!("/{{{}}}", param_name);
2310
2311 let nested_router = Router::new().route(&route_path, get(handler));
2312 let app = Router::new().nest(&prefix, nested_router);
2313
2314 let full_path = format!("{}/{}", prefix, param_value);
2315
2316 match app.match_route(&full_path, &Method::GET) {
2317 RouteMatch::Found { params, .. } => {
2318 prop_assert!(
2319 params.contains_key(¶m_name),
2320 "Should have '{}' parameter, got: {:?}",
2321 param_name, params
2322 );
2323 prop_assert_eq!(
2324 params.get(¶m_name).unwrap(),
2325 ¶m_value,
2326 "Parameter '{}' value should be '{}'",
2327 param_name, param_value
2328 );
2329 }
2330 _ => {
2331 prop_assert!(false, "Route should be found");
2332 }
2333 }
2334 }
2335
2336 #[test]
2341 fn prop_multiple_params_extraction(
2342 prefix in "[a-z][a-z0-9]{1,5}",
2343 param1_name in "[a-z]{1,5}",
2344 param1_value in "[a-z0-9]{1,10}",
2345 param2_name in "[a-z]{1,5}",
2346 param2_value in "[a-z0-9]{1,10}",
2347 ) {
2348 prop_assume!(param1_name != param2_name);
2350
2351 async fn handler() -> &'static str { "handler" }
2352
2353 let prefix = format!("/{}", prefix);
2354 let route_path = format!("/{{{}}}/items/{{{}}}", param1_name, param2_name);
2355
2356 let nested_router = Router::new().route(&route_path, get(handler));
2357 let app = Router::new().nest(&prefix, nested_router);
2358
2359 let full_path = format!("{}/{}/items/{}", prefix, param1_value, param2_value);
2360
2361 match app.match_route(&full_path, &Method::GET) {
2362 RouteMatch::Found { params, .. } => {
2363 prop_assert!(
2365 params.contains_key(¶m1_name),
2366 "Should have '{}' parameter, got: {:?}",
2367 param1_name, params
2368 );
2369 prop_assert_eq!(
2370 params.get(¶m1_name).unwrap(),
2371 ¶m1_value,
2372 "Parameter '{}' value should be '{}'",
2373 param1_name, param1_value
2374 );
2375
2376 prop_assert!(
2378 params.contains_key(¶m2_name),
2379 "Should have '{}' parameter, got: {:?}",
2380 param2_name, params
2381 );
2382 prop_assert_eq!(
2383 params.get(¶m2_name).unwrap(),
2384 ¶m2_value,
2385 "Parameter '{}' value should be '{}'",
2386 param2_name, param2_value
2387 );
2388 }
2389 _ => {
2390 prop_assert!(false, "Route should be found");
2391 }
2392 }
2393 }
2394
2395 #[test]
2400 fn prop_param_value_preservation(
2401 prefix in "[a-z]{1,5}",
2402 param_value in "[a-zA-Z0-9_-]{1,15}",
2404 ) {
2405 async fn handler() -> &'static str { "handler" }
2406
2407 let prefix = format!("/{}", prefix);
2408 let route_path = "/{id}".to_string();
2409
2410 let nested_router = Router::new().route(&route_path, get(handler));
2411 let app = Router::new().nest(&prefix, nested_router);
2412
2413 let full_path = format!("{}/{}", prefix, param_value);
2414
2415 match app.match_route(&full_path, &Method::GET) {
2416 RouteMatch::Found { params, .. } => {
2417 prop_assert_eq!(
2418 params.get("id").unwrap(),
2419 ¶m_value,
2420 "Parameter value should be preserved exactly"
2421 );
2422 }
2423 _ => {
2424 prop_assert!(false, "Route should be found");
2425 }
2426 }
2427 }
2428 }
2429
2430 proptest! {
2437 #![proptest_config(ProptestConfig::with_cases(100))]
2438
2439 #[test]
2444 fn prop_unregistered_path_returns_not_found(
2445 prefix in "[a-z][a-z0-9]{1,5}",
2446 route_segment in "[a-z][a-z0-9]{1,5}",
2447 unregistered_segment in "[a-z][a-z0-9]{6,10}",
2448 ) {
2449 prop_assume!(route_segment != unregistered_segment);
2451
2452 async fn handler() -> &'static str { "handler" }
2453
2454 let prefix = format!("/{}", prefix);
2455 let route_path = format!("/{}", route_segment);
2456
2457 let nested_router = Router::new().route(&route_path, get(handler));
2458 let app = Router::new().nest(&prefix, nested_router);
2459
2460 let unregistered_path = format!("{}/{}", prefix, unregistered_segment);
2462
2463 match app.match_route(&unregistered_path, &Method::GET) {
2464 RouteMatch::NotFound => {
2465 }
2467 RouteMatch::Found { .. } => {
2468 prop_assert!(false, "Path '{}' should not be found", unregistered_path);
2469 }
2470 RouteMatch::MethodNotAllowed { .. } => {
2471 prop_assert!(false, "Path '{}' should return NotFound, not MethodNotAllowed", unregistered_path);
2472 }
2473 }
2474 }
2475
2476 #[test]
2480 fn prop_wrong_prefix_returns_not_found(
2481 prefix1 in "[a-z][a-z0-9]{1,5}",
2482 prefix2 in "[a-z][a-z0-9]{6,10}",
2483 route_segment in "[a-z][a-z0-9]{1,5}",
2484 ) {
2485 prop_assume!(prefix1 != prefix2);
2487
2488 async fn handler() -> &'static str { "handler" }
2489
2490 let prefix = format!("/{}", prefix1);
2491 let route_path = format!("/{}", route_segment);
2492
2493 let nested_router = Router::new().route(&route_path, get(handler));
2494 let app = Router::new().nest(&prefix, nested_router);
2495
2496 let wrong_prefix_path = format!("/{}/{}", prefix2, route_segment);
2498
2499 match app.match_route(&wrong_prefix_path, &Method::GET) {
2500 RouteMatch::NotFound => {
2501 }
2503 _ => {
2504 prop_assert!(false, "Path '{}' with wrong prefix should return NotFound", wrong_prefix_path);
2505 }
2506 }
2507 }
2508
2509 #[test]
2514 fn prop_partial_path_returns_not_found(
2515 prefix in "[a-z][a-z0-9]{1,5}",
2516 segment1 in "[a-z][a-z0-9]{1,5}",
2517 segment2 in "[a-z][a-z0-9]{1,5}",
2518 ) {
2519 async fn handler() -> &'static str { "handler" }
2520
2521 let prefix = format!("/{}", prefix);
2522 let route_path = format!("/{}/{}", segment1, segment2);
2523
2524 let nested_router = Router::new().route(&route_path, get(handler));
2525 let app = Router::new().nest(&prefix, nested_router);
2526
2527 let partial_path = format!("{}/{}", prefix, segment1);
2529
2530 match app.match_route(&partial_path, &Method::GET) {
2531 RouteMatch::NotFound => {
2532 }
2534 _ => {
2535 prop_assert!(false, "Partial path '{}' should return NotFound", partial_path);
2536 }
2537 }
2538 }
2539 }
2540
2541 proptest! {
2548 #![proptest_config(ProptestConfig::with_cases(100))]
2549
2550 #[test]
2556 fn prop_unregistered_method_returns_method_not_allowed(
2557 prefix in "[a-z][a-z0-9]{1,5}",
2558 route_segment in "[a-z][a-z0-9]{1,5}",
2559 ) {
2560 async fn handler() -> &'static str { "handler" }
2561
2562 let prefix = format!("/{}", prefix);
2563 let route_path = format!("/{}", route_segment);
2564
2565 let nested_router = Router::new().route(&route_path, get(handler));
2567 let app = Router::new().nest(&prefix, nested_router);
2568
2569 let full_path = format!("{}{}", prefix, route_path);
2570
2571 match app.match_route(&full_path, &Method::POST) {
2573 RouteMatch::MethodNotAllowed { allowed } => {
2574 prop_assert!(
2575 allowed.contains(&Method::GET),
2576 "Allowed methods should contain GET, got: {:?}",
2577 allowed
2578 );
2579 prop_assert!(
2580 !allowed.contains(&Method::POST),
2581 "Allowed methods should not contain POST"
2582 );
2583 }
2584 RouteMatch::Found { .. } => {
2585 prop_assert!(false, "POST should not be found on GET-only route");
2586 }
2587 RouteMatch::NotFound => {
2588 prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
2589 }
2590 }
2591 }
2592
2593 #[test]
2598 fn prop_multiple_methods_in_allowed_list(
2599 prefix in "[a-z][a-z0-9]{1,5}",
2600 route_segment in "[a-z][a-z0-9]{1,5}",
2601 use_get in any::<bool>(),
2602 use_post in any::<bool>(),
2603 use_put in any::<bool>(),
2604 ) {
2605 prop_assume!(use_get || use_post || use_put);
2607
2608 async fn handler() -> &'static str { "handler" }
2609
2610 let prefix = format!("/{}", prefix);
2611 let route_path = format!("/{}", route_segment);
2612
2613 let mut method_router = MethodRouter::new();
2615 let mut expected_methods: Vec<Method> = Vec::new();
2616
2617 if use_get {
2618 let get_router = get(handler);
2619 for (method, h) in get_router.handlers {
2620 method_router.handlers.insert(method.clone(), h);
2621 expected_methods.push(method);
2622 }
2623 }
2624 if use_post {
2625 let post_router = post(handler);
2626 for (method, h) in post_router.handlers {
2627 method_router.handlers.insert(method.clone(), h);
2628 expected_methods.push(method);
2629 }
2630 }
2631 if use_put {
2632 let put_router = put(handler);
2633 for (method, h) in put_router.handlers {
2634 method_router.handlers.insert(method.clone(), h);
2635 expected_methods.push(method);
2636 }
2637 }
2638
2639 let nested_router = Router::new().route(&route_path, method_router);
2640 let app = Router::new().nest(&prefix, nested_router);
2641
2642 let full_path = format!("{}{}", prefix, route_path);
2643
2644 match app.match_route(&full_path, &Method::DELETE) {
2646 RouteMatch::MethodNotAllowed { allowed } => {
2647 for method in &expected_methods {
2649 prop_assert!(
2650 allowed.contains(method),
2651 "Allowed methods should contain {:?}, got: {:?}",
2652 method, allowed
2653 );
2654 }
2655 prop_assert!(
2657 !allowed.contains(&Method::DELETE),
2658 "Allowed methods should not contain DELETE"
2659 );
2660 }
2661 RouteMatch::Found { .. } => {
2662 prop_assert!(false, "DELETE should not be found");
2663 }
2664 RouteMatch::NotFound => {
2665 prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
2666 }
2667 }
2668 }
2669 }
2670
2671 proptest! {
2685 #![proptest_config(ProptestConfig::with_cases(100))]
2686
2687 #[test]
2693 fn prop_multiple_routers_all_routes_registered(
2694 prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2696 prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2697 num_routes1 in 1..4usize,
2699 num_routes2 in 1..4usize,
2700 ) {
2701 let prefix1 = format!("/{}", prefix1_segments.join("/"));
2703 let prefix2 = format!("/{}", prefix2_segments.join("/"));
2704
2705 prop_assume!(prefix1 != prefix2);
2707
2708 async fn handler() -> &'static str { "handler" }
2709
2710 let mut router1 = Router::new();
2712 for i in 0..num_routes1 {
2713 let path = format!("/route1_{}", i);
2714 router1 = router1.route(&path, get(handler));
2715 }
2716
2717 let mut router2 = Router::new();
2719 for i in 0..num_routes2 {
2720 let path = format!("/route2_{}", i);
2721 router2 = router2.route(&path, get(handler));
2722 }
2723
2724 let app = Router::new()
2726 .nest(&prefix1, router1)
2727 .nest(&prefix2, router2);
2728
2729 let routes = app.registered_routes();
2730
2731 let expected_count = num_routes1 + num_routes2;
2733 prop_assert_eq!(
2734 routes.len(),
2735 expected_count,
2736 "Should have {} routes ({}+{}), got {}",
2737 expected_count, num_routes1, num_routes2, routes.len()
2738 );
2739
2740 for i in 0..num_routes1 {
2742 let expected_path = format!("{}/route1_{}", prefix1, i);
2743 let matchit_path = convert_path_params(&expected_path);
2744 prop_assert!(
2745 routes.contains_key(&matchit_path),
2746 "Route '{}' should be registered",
2747 expected_path
2748 );
2749 }
2750
2751 for i in 0..num_routes2 {
2753 let expected_path = format!("{}/route2_{}", prefix2, i);
2754 let matchit_path = convert_path_params(&expected_path);
2755 prop_assert!(
2756 routes.contains_key(&matchit_path),
2757 "Route '{}' should be registered",
2758 expected_path
2759 );
2760 }
2761 }
2762
2763 #[test]
2768 fn prop_multiple_routers_no_interference(
2769 prefix1 in "[a-z][a-z0-9]{1,5}",
2770 prefix2 in "[a-z][a-z0-9]{1,5}",
2771 route_segment in "[a-z][a-z0-9]{1,5}",
2772 param_value1 in "[a-z0-9]{1,10}",
2773 param_value2 in "[a-z0-9]{1,10}",
2774 ) {
2775 prop_assume!(prefix1 != prefix2);
2777
2778 let prefix1 = format!("/{}", prefix1);
2779 let prefix2 = format!("/{}", prefix2);
2780
2781 async fn handler() -> &'static str { "handler" }
2782
2783 let router1 = Router::new()
2785 .route(&format!("/{}", route_segment), get(handler))
2786 .route("/{id}", get(handler));
2787
2788 let router2 = Router::new()
2789 .route(&format!("/{}", route_segment), get(handler))
2790 .route("/{id}", get(handler));
2791
2792 let app = Router::new()
2794 .nest(&prefix1, router1)
2795 .nest(&prefix2, router2);
2796
2797 let path1_static = format!("{}/{}", prefix1, route_segment);
2799 match app.match_route(&path1_static, &Method::GET) {
2800 RouteMatch::Found { params, .. } => {
2801 prop_assert!(params.is_empty(), "Static path should have no params");
2802 }
2803 _ => {
2804 prop_assert!(false, "Route '{}' should be found", path1_static);
2805 }
2806 }
2807
2808 let path1_param = format!("{}/{}", prefix1, param_value1);
2809 match app.match_route(&path1_param, &Method::GET) {
2810 RouteMatch::Found { params, .. } => {
2811 prop_assert_eq!(
2812 params.get("id"),
2813 Some(¶m_value1.to_string()),
2814 "Parameter should be extracted correctly"
2815 );
2816 }
2817 _ => {
2818 prop_assert!(false, "Route '{}' should be found", path1_param);
2819 }
2820 }
2821
2822 let path2_static = format!("{}/{}", prefix2, route_segment);
2824 match app.match_route(&path2_static, &Method::GET) {
2825 RouteMatch::Found { params, .. } => {
2826 prop_assert!(params.is_empty(), "Static path should have no params");
2827 }
2828 _ => {
2829 prop_assert!(false, "Route '{}' should be found", path2_static);
2830 }
2831 }
2832
2833 let path2_param = format!("{}/{}", prefix2, param_value2);
2834 match app.match_route(&path2_param, &Method::GET) {
2835 RouteMatch::Found { params, .. } => {
2836 prop_assert_eq!(
2837 params.get("id"),
2838 Some(¶m_value2.to_string()),
2839 "Parameter should be extracted correctly"
2840 );
2841 }
2842 _ => {
2843 prop_assert!(false, "Route '{}' should be found", path2_param);
2844 }
2845 }
2846 }
2847
2848 #[test]
2853 fn prop_multiple_routers_preserve_methods(
2854 prefix1 in "[a-z][a-z0-9]{1,5}",
2855 prefix2 in "[a-z][a-z0-9]{1,5}",
2856 route_segment in "[a-z][a-z0-9]{1,5}",
2857 router1_use_get in any::<bool>(),
2858 router1_use_post in any::<bool>(),
2859 router2_use_get in any::<bool>(),
2860 router2_use_put in any::<bool>(),
2861 ) {
2862 prop_assume!(router1_use_get || router1_use_post);
2864 prop_assume!(router2_use_get || router2_use_put);
2865 prop_assume!(prefix1 != prefix2);
2867
2868 let prefix1 = format!("/{}", prefix1);
2869 let prefix2 = format!("/{}", prefix2);
2870 let route_path = format!("/{}", route_segment);
2871
2872 async fn handler() -> &'static str { "handler" }
2873
2874 let mut method_router1 = MethodRouter::new();
2876 let mut expected_methods1: Vec<Method> = Vec::new();
2877 if router1_use_get {
2878 let get_router = get(handler);
2879 for (method, h) in get_router.handlers {
2880 method_router1.handlers.insert(method.clone(), h);
2881 expected_methods1.push(method);
2882 }
2883 }
2884 if router1_use_post {
2885 let post_router = post(handler);
2886 for (method, h) in post_router.handlers {
2887 method_router1.handlers.insert(method.clone(), h);
2888 expected_methods1.push(method);
2889 }
2890 }
2891
2892 let mut method_router2 = MethodRouter::new();
2894 let mut expected_methods2: Vec<Method> = Vec::new();
2895 if router2_use_get {
2896 let get_router = get(handler);
2897 for (method, h) in get_router.handlers {
2898 method_router2.handlers.insert(method.clone(), h);
2899 expected_methods2.push(method);
2900 }
2901 }
2902 if router2_use_put {
2903 let put_router = put(handler);
2904 for (method, h) in put_router.handlers {
2905 method_router2.handlers.insert(method.clone(), h);
2906 expected_methods2.push(method);
2907 }
2908 }
2909
2910 let router1 = Router::new().route(&route_path, method_router1);
2911 let router2 = Router::new().route(&route_path, method_router2);
2912
2913 let app = Router::new()
2914 .nest(&prefix1, router1)
2915 .nest(&prefix2, router2);
2916
2917 let full_path1 = format!("{}{}", prefix1, route_path);
2918 let full_path2 = format!("{}{}", prefix2, route_path);
2919
2920 for method in &expected_methods1 {
2922 match app.match_route(&full_path1, method) {
2923 RouteMatch::Found { .. } => {}
2924 _ => {
2925 prop_assert!(false, "Method {:?} should be found for {}", method, full_path1);
2926 }
2927 }
2928 }
2929
2930 for method in &expected_methods2 {
2932 match app.match_route(&full_path2, method) {
2933 RouteMatch::Found { .. } => {}
2934 _ => {
2935 prop_assert!(false, "Method {:?} should be found for {}", method, full_path2);
2936 }
2937 }
2938 }
2939
2940 if !expected_methods1.contains(&Method::DELETE) {
2942 match app.match_route(&full_path1, &Method::DELETE) {
2943 RouteMatch::MethodNotAllowed { allowed } => {
2944 for method in &expected_methods1 {
2945 prop_assert!(
2946 allowed.contains(method),
2947 "Allowed methods for {} should contain {:?}",
2948 full_path1, method
2949 );
2950 }
2951 }
2952 _ => {
2953 prop_assert!(false, "DELETE should return MethodNotAllowed for {}", full_path1);
2954 }
2955 }
2956 }
2957 }
2958
2959 #[test]
2964 fn prop_three_routers_composition(
2965 prefix1 in "[a-z]{1,3}",
2966 prefix2 in "[a-z]{4,6}",
2967 prefix3 in "[a-z]{7,9}",
2968 num_routes in 1..3usize,
2969 ) {
2970 let prefix1 = format!("/{}", prefix1);
2971 let prefix2 = format!("/{}", prefix2);
2972 let prefix3 = format!("/{}", prefix3);
2973
2974 async fn handler() -> &'static str { "handler" }
2975
2976 let mut router1 = Router::new();
2978 let mut router2 = Router::new();
2979 let mut router3 = Router::new();
2980
2981 for i in 0..num_routes {
2982 let path = format!("/item{}", i);
2983 router1 = router1.route(&path, get(handler));
2984 router2 = router2.route(&path, get(handler));
2985 router3 = router3.route(&path, get(handler));
2986 }
2987
2988 let app = Router::new()
2990 .nest(&prefix1, router1)
2991 .nest(&prefix2, router2)
2992 .nest(&prefix3, router3);
2993
2994 let routes = app.registered_routes();
2995
2996 let expected_count = 3 * num_routes;
2998 prop_assert_eq!(
2999 routes.len(),
3000 expected_count,
3001 "Should have {} routes, got {}",
3002 expected_count, routes.len()
3003 );
3004
3005 for i in 0..num_routes {
3007 let path1 = format!("{}/item{}", prefix1, i);
3008 let path2 = format!("{}/item{}", prefix2, i);
3009 let path3 = format!("{}/item{}", prefix3, i);
3010
3011 match app.match_route(&path1, &Method::GET) {
3012 RouteMatch::Found { .. } => {}
3013 _ => prop_assert!(false, "Route '{}' should be found", path1),
3014 }
3015 match app.match_route(&path2, &Method::GET) {
3016 RouteMatch::Found { .. } => {}
3017 _ => prop_assert!(false, "Route '{}' should be found", path2),
3018 }
3019 match app.match_route(&path3, &Method::GET) {
3020 RouteMatch::Found { .. } => {}
3021 _ => prop_assert!(false, "Route '{}' should be found", path3),
3022 }
3023 }
3024 }
3025 }
3026 proptest! {
3027 #![proptest_config(ProptestConfig::with_cases(100))]
3028
3029 #[test]
3035 fn prop_nested_route_conflict_different_param_names(
3036 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3037 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
3038 param1 in "[a-z][a-z0-9]{1,5}",
3039 param2 in "[a-z][a-z0-9]{1,5}",
3040 ) {
3041 prop_assume!(param1 != param2);
3043
3044 async fn handler1() -> &'static str { "handler1" }
3045 async fn handler2() -> &'static str { "handler2" }
3046
3047 let prefix = format!("/{}", prefix_segments.join("/"));
3048
3049 let existing_path = if route_segments.is_empty() {
3051 format!("{}/{{{}}}", prefix, param1)
3052 } else {
3053 format!("{}/{}/{{{}}}", prefix, route_segments.join("/"), param1)
3054 };
3055
3056 let nested_path = if route_segments.is_empty() {
3058 format!("/{{{}}}", param2)
3059 } else {
3060 format!("/{}/{{{}}}", route_segments.join("/"), param2)
3061 };
3062
3063 let result = catch_unwind(AssertUnwindSafe(|| {
3065 let parent = Router::new().route(&existing_path, get(handler1));
3066 let nested = Router::new().route(&nested_path, get(handler2));
3067 let _app = parent.nest(&prefix, nested);
3068 }));
3069
3070 prop_assert!(
3072 result.is_err(),
3073 "Nested route '{}{}' should conflict with existing route '{}' but didn't",
3074 prefix, nested_path, existing_path
3075 );
3076
3077 if let Err(panic_info) = result {
3079 if let Some(msg) = panic_info.downcast_ref::<String>() {
3080 prop_assert!(
3081 msg.contains("ROUTE CONFLICT DETECTED"),
3082 "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
3083 msg
3084 );
3085 prop_assert!(
3086 msg.contains("Existing:") && msg.contains("New:"),
3087 "Error should contain both 'Existing:' and 'New:' labels, got: {}",
3088 msg
3089 );
3090 }
3091 }
3092 }
3093
3094 #[test]
3099 fn prop_nested_route_conflict_exact_same_path(
3100 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3101 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3102 ) {
3103 async fn handler1() -> &'static str { "handler1" }
3104 async fn handler2() -> &'static str { "handler2" }
3105
3106 let prefix = format!("/{}", prefix_segments.join("/"));
3107 let route_path = format!("/{}", route_segments.join("/"));
3108
3109 let existing_path = format!("{}{}", prefix, route_path);
3111
3112 let result = catch_unwind(AssertUnwindSafe(|| {
3114 let parent = Router::new().route(&existing_path, get(handler1));
3115 let nested = Router::new().route(&route_path, get(handler2));
3116 let _app = parent.nest(&prefix, nested);
3117 }));
3118
3119 prop_assert!(
3121 result.is_err(),
3122 "Nested route '{}{}' should conflict with existing route '{}' but didn't",
3123 prefix, route_path, existing_path
3124 );
3125 }
3126
3127 #[test]
3132 fn prop_nested_routes_different_prefixes_no_conflict(
3133 prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3134 prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3135 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3136 has_param in any::<bool>(),
3137 ) {
3138 let prefix1 = format!("/{}", prefix1_segments.join("/"));
3140 let prefix2 = format!("/{}", prefix2_segments.join("/"));
3141
3142 prop_assume!(prefix1 != prefix2);
3144
3145 async fn handler1() -> &'static str { "handler1" }
3146 async fn handler2() -> &'static str { "handler2" }
3147
3148 let route_path = if has_param {
3150 format!("/{}/{{id}}", route_segments.join("/"))
3151 } else {
3152 format!("/{}", route_segments.join("/"))
3153 };
3154
3155 let result = catch_unwind(AssertUnwindSafe(|| {
3157 let nested1 = Router::new().route(&route_path, get(handler1));
3158 let nested2 = Router::new().route(&route_path, get(handler2));
3159
3160 let app = Router::new()
3161 .nest(&prefix1, nested1)
3162 .nest(&prefix2, nested2);
3163
3164 app.registered_routes().len()
3165 }));
3166
3167 prop_assert!(
3169 result.is_ok(),
3170 "Routes under different prefixes '{}' and '{}' should not conflict",
3171 prefix1, prefix2
3172 );
3173
3174 if let Ok(count) = result {
3175 prop_assert_eq!(count, 2, "Should have registered 2 routes");
3176 }
3177 }
3178
3179 #[test]
3184 fn prop_nested_conflict_error_contains_guidance(
3185 prefix in "[a-z][a-z0-9]{1,5}",
3186 segment in "[a-z][a-z0-9]{1,5}",
3187 param1 in "[a-z][a-z0-9]{1,5}",
3188 param2 in "[a-z][a-z0-9]{1,5}",
3189 ) {
3190 prop_assume!(param1 != param2);
3191
3192 async fn handler1() -> &'static str { "handler1" }
3193 async fn handler2() -> &'static str { "handler2" }
3194
3195 let prefix = format!("/{}", prefix);
3196 let existing_path = format!("{}/{}/{{{}}}", prefix, segment, param1);
3197 let nested_path = format!("/{}/{{{}}}", segment, param2);
3198
3199 let result = catch_unwind(AssertUnwindSafe(|| {
3200 let parent = Router::new().route(&existing_path, get(handler1));
3201 let nested = Router::new().route(&nested_path, get(handler2));
3202 let _app = parent.nest(&prefix, nested);
3203 }));
3204
3205 prop_assert!(result.is_err(), "Should have detected conflict");
3206
3207 if let Err(panic_info) = result {
3208 if let Some(msg) = panic_info.downcast_ref::<String>() {
3209 prop_assert!(
3210 msg.contains("How to resolve:"),
3211 "Error should contain 'How to resolve:' guidance, got: {}",
3212 msg
3213 );
3214 prop_assert!(
3215 msg.contains("Use different path patterns") ||
3216 msg.contains("different path patterns"),
3217 "Error should suggest using different path patterns, got: {}",
3218 msg
3219 );
3220 }
3221 }
3222 }
3223 }
3224}