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
450 .inner
451 .insert(prefixed_matchit_path.clone(), method_router)
452 {
453 Ok(_) => {
454 self.registered_routes.insert(
456 prefixed_matchit_path,
457 RouteInfo {
458 path: prefixed_display_path,
459 methods: route_info.methods,
460 },
461 );
462 }
463 Err(e) => {
464 self.method_routers.remove(&prefixed_matchit_path);
466
467 let existing_path = self
469 .find_conflicting_route(&prefixed_matchit_path)
470 .map(|info| info.path.clone())
471 .unwrap_or_else(|| "<unknown>".to_string());
472
473 let conflict_error = RouteConflictError {
474 new_path: prefixed_display_path,
475 method: route_info.methods.first().cloned(),
476 existing_path,
477 details: e.to_string(),
478 };
479
480 panic!("{}", conflict_error);
481 }
482 }
483 }
484
485 self
486 }
487
488 pub fn merge_state<S: Clone + Send + Sync + 'static>(mut self, other: &Router) -> Self {
505 let type_id = std::any::TypeId::of::<S>();
506
507 if !self.state_type_ids.contains(&type_id) {
509 if let Some(state) = other.state.get::<S>() {
511 let extensions = Arc::make_mut(&mut self.state);
512 extensions.insert(state.clone());
513 self.state_type_ids.push(type_id);
514 }
515 }
516
517 self
518 }
519
520 pub(crate) fn match_route(&self, path: &str, method: &Method) -> RouteMatch<'_> {
522 match self.inner.at(path) {
523 Ok(matched) => {
524 let method_router = matched.value;
525
526 if let Some(handler) = method_router.get_handler(method) {
527 let params: HashMap<String, String> = matched
529 .params
530 .iter()
531 .map(|(k, v)| (k.to_string(), v.to_string()))
532 .collect();
533
534 RouteMatch::Found { handler, params }
535 } else {
536 RouteMatch::MethodNotAllowed {
537 allowed: method_router.allowed_methods(),
538 }
539 }
540 }
541 Err(_) => RouteMatch::NotFound,
542 }
543 }
544
545 pub(crate) fn state_ref(&self) -> Arc<Extensions> {
547 self.state.clone()
548 }
549
550 pub fn registered_routes(&self) -> &HashMap<String, RouteInfo> {
552 &self.registered_routes
553 }
554
555 pub fn method_routers(&self) -> &HashMap<String, MethodRouter> {
557 &self.method_routers
558 }
559}
560
561impl Default for Router {
562 fn default() -> Self {
563 Self::new()
564 }
565}
566
567pub(crate) enum RouteMatch<'a> {
569 Found {
570 handler: &'a BoxedHandler,
571 params: HashMap<String, String>,
572 },
573 NotFound,
574 MethodNotAllowed {
575 allowed: Vec<Method>,
576 },
577}
578
579fn convert_path_params(path: &str) -> String {
581 let mut result = String::with_capacity(path.len());
582
583 for ch in path.chars() {
584 match ch {
585 '{' => {
586 result.push(':');
587 }
588 '}' => {
589 }
591 _ => {
592 result.push(ch);
593 }
594 }
595 }
596
597 result
598}
599
600fn normalize_path_for_comparison(path: &str) -> String {
602 let mut result = String::with_capacity(path.len());
603 let mut in_param = false;
604
605 for ch in path.chars() {
606 match ch {
607 ':' => {
608 in_param = true;
609 result.push_str(":_");
610 }
611 '/' => {
612 in_param = false;
613 result.push('/');
614 }
615 _ if in_param => {
616 }
618 _ => {
619 result.push(ch);
620 }
621 }
622 }
623
624 result
625}
626
627pub(crate) fn normalize_prefix(prefix: &str) -> String {
644 if prefix.is_empty() {
646 return "/".to_string();
647 }
648
649 let segments: Vec<&str> = prefix.split('/').filter(|s| !s.is_empty()).collect();
651
652 if segments.is_empty() {
654 return "/".to_string();
655 }
656
657 let mut result = String::with_capacity(prefix.len() + 1);
659 for segment in segments {
660 result.push('/');
661 result.push_str(segment);
662 }
663
664 result
665}
666
667#[cfg(test)]
668mod tests {
669 use super::*;
670
671 #[test]
672 fn test_convert_path_params() {
673 assert_eq!(convert_path_params("/users/{id}"), "/users/:id");
674 assert_eq!(
675 convert_path_params("/users/{user_id}/posts/{post_id}"),
676 "/users/:user_id/posts/:post_id"
677 );
678 assert_eq!(convert_path_params("/static/path"), "/static/path");
679 }
680
681 #[test]
682 fn test_normalize_path_for_comparison() {
683 assert_eq!(normalize_path_for_comparison("/users/:id"), "/users/:_");
684 assert_eq!(
685 normalize_path_for_comparison("/users/:user_id"),
686 "/users/:_"
687 );
688 assert_eq!(
689 normalize_path_for_comparison("/users/:id/posts/:post_id"),
690 "/users/:_/posts/:_"
691 );
692 assert_eq!(
693 normalize_path_for_comparison("/static/path"),
694 "/static/path"
695 );
696 }
697
698 #[test]
699 fn test_normalize_prefix() {
700 assert_eq!(normalize_prefix("api"), "/api");
702 assert_eq!(normalize_prefix("/api"), "/api");
703 assert_eq!(normalize_prefix("/api/"), "/api");
704 assert_eq!(normalize_prefix("api/"), "/api");
705
706 assert_eq!(normalize_prefix("api/v1"), "/api/v1");
708 assert_eq!(normalize_prefix("/api/v1"), "/api/v1");
709 assert_eq!(normalize_prefix("/api/v1/"), "/api/v1");
710
711 assert_eq!(normalize_prefix(""), "/");
713 assert_eq!(normalize_prefix("/"), "/");
714
715 assert_eq!(normalize_prefix("//api"), "/api");
717 assert_eq!(normalize_prefix("api//v1"), "/api/v1");
718 assert_eq!(normalize_prefix("//api//v1//"), "/api/v1");
719 assert_eq!(normalize_prefix("///"), "/");
720 }
721
722 #[test]
723 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
724 fn test_route_conflict_detection() {
725 async fn handler1() -> &'static str {
726 "handler1"
727 }
728 async fn handler2() -> &'static str {
729 "handler2"
730 }
731
732 let _router = Router::new()
733 .route("/users/{id}", get(handler1))
734 .route("/users/{user_id}", get(handler2)); }
736
737 #[test]
738 fn test_no_conflict_different_paths() {
739 async fn handler1() -> &'static str {
740 "handler1"
741 }
742 async fn handler2() -> &'static str {
743 "handler2"
744 }
745
746 let router = Router::new()
747 .route("/users/{id}", get(handler1))
748 .route("/users/{id}/profile", get(handler2));
749
750 assert_eq!(router.registered_routes().len(), 2);
751 }
752
753 #[test]
754 fn test_route_info_tracking() {
755 async fn handler() -> &'static str {
756 "handler"
757 }
758
759 let router = Router::new().route("/users/{id}", get(handler));
760
761 let routes = router.registered_routes();
762 assert_eq!(routes.len(), 1);
763
764 let info = routes.get("/users/:id").unwrap();
765 assert_eq!(info.path, "/users/{id}");
766 assert_eq!(info.methods.len(), 1);
767 assert_eq!(info.methods[0], Method::GET);
768 }
769
770 #[test]
771 fn test_basic_router_nesting() {
772 async fn list_users() -> &'static str {
773 "list users"
774 }
775 async fn get_user() -> &'static str {
776 "get user"
777 }
778
779 let users_router = Router::new()
780 .route("/", get(list_users))
781 .route("/{id}", get(get_user));
782
783 let app = Router::new().nest("/api/users", users_router);
784
785 let routes = app.registered_routes();
786 assert_eq!(routes.len(), 2);
787
788 assert!(routes.contains_key("/api/users"));
790 assert!(routes.contains_key("/api/users/:id"));
791
792 let list_info = routes.get("/api/users").unwrap();
794 assert_eq!(list_info.path, "/api/users");
795
796 let get_info = routes.get("/api/users/:id").unwrap();
797 assert_eq!(get_info.path, "/api/users/{id}");
798 }
799
800 #[test]
801 fn test_nested_route_matching() {
802 async fn handler() -> &'static str {
803 "handler"
804 }
805
806 let users_router = Router::new().route("/{id}", get(handler));
807
808 let app = Router::new().nest("/api/users", users_router);
809
810 match app.match_route("/api/users/123", &Method::GET) {
812 RouteMatch::Found { params, .. } => {
813 assert_eq!(params.get("id"), Some(&"123".to_string()));
814 }
815 _ => panic!("Route should be found"),
816 }
817 }
818
819 #[test]
820 fn test_nested_route_matching_multiple_params() {
821 async fn handler() -> &'static str {
822 "handler"
823 }
824
825 let posts_router = Router::new().route("/{user_id}/posts/{post_id}", get(handler));
826
827 let app = Router::new().nest("/api", posts_router);
828
829 match app.match_route("/api/42/posts/100", &Method::GET) {
831 RouteMatch::Found { params, .. } => {
832 assert_eq!(params.get("user_id"), Some(&"42".to_string()));
833 assert_eq!(params.get("post_id"), Some(&"100".to_string()));
834 }
835 _ => panic!("Route should be found"),
836 }
837 }
838
839 #[test]
840 fn test_nested_route_matching_static_path() {
841 async fn handler() -> &'static str {
842 "handler"
843 }
844
845 let health_router = Router::new().route("/health", get(handler));
846
847 let app = Router::new().nest("/api/v1", health_router);
848
849 match app.match_route("/api/v1/health", &Method::GET) {
851 RouteMatch::Found { params, .. } => {
852 assert!(params.is_empty(), "Static path should have no params");
853 }
854 _ => panic!("Route should be found"),
855 }
856 }
857
858 #[test]
859 fn test_nested_route_not_found() {
860 async fn handler() -> &'static str {
861 "handler"
862 }
863
864 let users_router = Router::new().route("/users", get(handler));
865
866 let app = Router::new().nest("/api", users_router);
867
868 match app.match_route("/api/posts", &Method::GET) {
870 RouteMatch::NotFound => {
871 }
873 _ => panic!("Route should not be found"),
874 }
875
876 match app.match_route("/v2/users", &Method::GET) {
878 RouteMatch::NotFound => {
879 }
881 _ => panic!("Route with wrong prefix should not be found"),
882 }
883 }
884
885 #[test]
886 fn test_nested_route_method_not_allowed() {
887 async fn handler() -> &'static str {
888 "handler"
889 }
890
891 let users_router = Router::new().route("/users", get(handler));
892
893 let app = Router::new().nest("/api", users_router);
894
895 match app.match_route("/api/users", &Method::POST) {
897 RouteMatch::MethodNotAllowed { allowed } => {
898 assert!(allowed.contains(&Method::GET));
899 assert!(!allowed.contains(&Method::POST));
900 }
901 _ => panic!("Should return MethodNotAllowed"),
902 }
903 }
904
905 #[test]
906 fn test_nested_route_multiple_methods() {
907 async fn get_handler() -> &'static str {
908 "get"
909 }
910 async fn post_handler() -> &'static str {
911 "post"
912 }
913
914 let get_router = get(get_handler);
916 let post_router = post(post_handler);
917 let mut combined = MethodRouter::new();
918 for (method, handler) in get_router.handlers {
919 combined.handlers.insert(method, handler);
920 }
921 for (method, handler) in post_router.handlers {
922 combined.handlers.insert(method, handler);
923 }
924
925 let users_router = Router::new().route("/users", combined);
926 let app = Router::new().nest("/api", users_router);
927
928 match app.match_route("/api/users", &Method::GET) {
930 RouteMatch::Found { .. } => {}
931 _ => panic!("GET should be found"),
932 }
933
934 match app.match_route("/api/users", &Method::POST) {
935 RouteMatch::Found { .. } => {}
936 _ => panic!("POST should be found"),
937 }
938
939 match app.match_route("/api/users", &Method::DELETE) {
941 RouteMatch::MethodNotAllowed { allowed } => {
942 assert!(allowed.contains(&Method::GET));
943 assert!(allowed.contains(&Method::POST));
944 }
945 _ => panic!("DELETE should return MethodNotAllowed"),
946 }
947 }
948
949 #[test]
950 fn test_nested_router_prefix_normalization() {
951 async fn handler() -> &'static str {
952 "handler"
953 }
954
955 let router1 = Router::new().route("/test", get(handler));
957 let app1 = Router::new().nest("api", router1);
958 assert!(app1.registered_routes().contains_key("/api/test"));
959
960 let router2 = Router::new().route("/test", get(handler));
961 let app2 = Router::new().nest("/api/", router2);
962 assert!(app2.registered_routes().contains_key("/api/test"));
963
964 let router3 = Router::new().route("/test", get(handler));
965 let app3 = Router::new().nest("//api//", router3);
966 assert!(app3.registered_routes().contains_key("/api/test"));
967 }
968
969 #[test]
970 fn test_state_tracking() {
971 #[derive(Clone)]
972 struct MyState(String);
973
974 let router = Router::new().state(MyState("test".to_string()));
975
976 assert!(router.has_state::<MyState>());
977 assert!(!router.has_state::<String>());
978 }
979
980 #[test]
981 fn test_state_merge_nested_only() {
982 #[derive(Clone, PartialEq, Debug)]
983 struct NestedState(String);
984
985 async fn handler() -> &'static str {
986 "handler"
987 }
988
989 let state_source = Router::new().state(NestedState("nested".to_string()));
991
992 let nested = Router::new().route("/test", get(handler));
993
994 let parent = Router::new()
995 .nest("/api", nested)
996 .merge_state::<NestedState>(&state_source);
997
998 assert!(parent.has_state::<NestedState>());
1000
1001 let state = parent.state.get::<NestedState>().unwrap();
1003 assert_eq!(state.0, "nested");
1004 }
1005
1006 #[test]
1007 fn test_state_merge_parent_wins() {
1008 #[derive(Clone, PartialEq, Debug)]
1009 struct SharedState(String);
1010
1011 async fn handler() -> &'static str {
1012 "handler"
1013 }
1014
1015 let state_source = Router::new().state(SharedState("nested".to_string()));
1017
1018 let nested = Router::new().route("/test", get(handler));
1019
1020 let parent = Router::new()
1021 .state(SharedState("parent".to_string()))
1022 .nest("/api", nested)
1023 .merge_state::<SharedState>(&state_source);
1024
1025 assert!(parent.has_state::<SharedState>());
1027
1028 let state = parent.state.get::<SharedState>().unwrap();
1030 assert_eq!(state.0, "parent");
1031 }
1032
1033 #[test]
1034 fn test_state_type_ids_merged_on_nest() {
1035 #[derive(Clone)]
1036 struct NestedState(String);
1037
1038 async fn handler() -> &'static str {
1039 "handler"
1040 }
1041
1042 let nested = Router::new()
1043 .route("/test", get(handler))
1044 .state(NestedState("nested".to_string()));
1045
1046 let parent = Router::new().nest("/api", nested);
1047
1048 assert!(parent
1050 .state_type_ids()
1051 .contains(&std::any::TypeId::of::<NestedState>()));
1052 }
1053
1054 #[test]
1055 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
1056 fn test_nested_route_conflict_with_existing_route() {
1057 async fn handler1() -> &'static str {
1058 "handler1"
1059 }
1060 async fn handler2() -> &'static str {
1061 "handler2"
1062 }
1063
1064 let parent = Router::new().route("/api/users/{id}", get(handler1));
1066
1067 let nested = Router::new().route("/{user_id}", get(handler2));
1069
1070 let _app = parent.nest("/api/users", nested);
1072 }
1073
1074 #[test]
1075 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
1076 fn test_nested_route_conflict_same_path_different_param_names() {
1077 async fn handler1() -> &'static str {
1078 "handler1"
1079 }
1080 async fn handler2() -> &'static str {
1081 "handler2"
1082 }
1083
1084 let nested1 = Router::new().route("/{id}", get(handler1));
1086 let nested2 = Router::new().route("/{user_id}", get(handler2));
1087
1088 let _app = Router::new()
1090 .nest("/api/users", nested1)
1091 .nest("/api/users", nested2);
1092 }
1093
1094 #[test]
1095 fn test_nested_route_conflict_error_contains_both_paths() {
1096 use std::panic::{catch_unwind, AssertUnwindSafe};
1097
1098 async fn handler1() -> &'static str {
1099 "handler1"
1100 }
1101 async fn handler2() -> &'static str {
1102 "handler2"
1103 }
1104
1105 let result = catch_unwind(AssertUnwindSafe(|| {
1106 let parent = Router::new().route("/api/users/{id}", get(handler1));
1107 let nested = Router::new().route("/{user_id}", get(handler2));
1108 let _app = parent.nest("/api/users", nested);
1109 }));
1110
1111 assert!(result.is_err(), "Should have panicked due to conflict");
1112
1113 if let Err(panic_info) = result {
1114 if let Some(msg) = panic_info.downcast_ref::<String>() {
1115 assert!(
1116 msg.contains("ROUTE CONFLICT DETECTED"),
1117 "Error should contain 'ROUTE CONFLICT DETECTED'"
1118 );
1119 assert!(
1120 msg.contains("Existing:") && msg.contains("New:"),
1121 "Error should contain both 'Existing:' and 'New:' labels"
1122 );
1123 assert!(
1124 msg.contains("How to resolve:"),
1125 "Error should contain resolution guidance"
1126 );
1127 }
1128 }
1129 }
1130
1131 #[test]
1132 fn test_nested_routes_no_conflict_different_prefixes() {
1133 async fn handler1() -> &'static str {
1134 "handler1"
1135 }
1136 async fn handler2() -> &'static str {
1137 "handler2"
1138 }
1139
1140 let nested1 = Router::new().route("/{id}", get(handler1));
1142 let nested2 = Router::new().route("/{id}", get(handler2));
1143
1144 let app = Router::new()
1146 .nest("/api/users", nested1)
1147 .nest("/api/posts", nested2);
1148
1149 assert_eq!(app.registered_routes().len(), 2);
1150 assert!(app.registered_routes().contains_key("/api/users/:id"));
1151 assert!(app.registered_routes().contains_key("/api/posts/:id"));
1152 }
1153
1154 #[test]
1159 fn test_multiple_router_composition_all_routes_registered() {
1160 async fn users_list() -> &'static str {
1161 "users list"
1162 }
1163 async fn users_get() -> &'static str {
1164 "users get"
1165 }
1166 async fn posts_list() -> &'static str {
1167 "posts list"
1168 }
1169 async fn posts_get() -> &'static str {
1170 "posts get"
1171 }
1172 async fn comments_list() -> &'static str {
1173 "comments list"
1174 }
1175
1176 let users_router = Router::new()
1178 .route("/", get(users_list))
1179 .route("/{id}", get(users_get));
1180
1181 let posts_router = Router::new()
1182 .route("/", get(posts_list))
1183 .route("/{id}", get(posts_get));
1184
1185 let comments_router = Router::new().route("/", get(comments_list));
1186
1187 let app = Router::new()
1189 .nest("/api/users", users_router)
1190 .nest("/api/posts", posts_router)
1191 .nest("/api/comments", comments_router);
1192
1193 let routes = app.registered_routes();
1195 assert_eq!(routes.len(), 5, "Should have 5 routes registered");
1196
1197 assert!(
1199 routes.contains_key("/api/users"),
1200 "Should have /api/users route"
1201 );
1202 assert!(
1203 routes.contains_key("/api/users/:id"),
1204 "Should have /api/users/:id route"
1205 );
1206
1207 assert!(
1209 routes.contains_key("/api/posts"),
1210 "Should have /api/posts route"
1211 );
1212 assert!(
1213 routes.contains_key("/api/posts/:id"),
1214 "Should have /api/posts/:id route"
1215 );
1216
1217 assert!(
1219 routes.contains_key("/api/comments"),
1220 "Should have /api/comments route"
1221 );
1222 }
1223
1224 #[test]
1225 fn test_multiple_router_composition_no_interference() {
1226 async fn users_handler() -> &'static str {
1227 "users"
1228 }
1229 async fn posts_handler() -> &'static str {
1230 "posts"
1231 }
1232 async fn admin_handler() -> &'static str {
1233 "admin"
1234 }
1235
1236 let users_router = Router::new()
1238 .route("/list", get(users_handler))
1239 .route("/{id}", get(users_handler));
1240
1241 let posts_router = Router::new()
1242 .route("/list", get(posts_handler))
1243 .route("/{id}", get(posts_handler));
1244
1245 let admin_router = Router::new()
1246 .route("/list", get(admin_handler))
1247 .route("/{id}", get(admin_handler));
1248
1249 let app = Router::new()
1251 .nest("/api/v1/users", users_router)
1252 .nest("/api/v1/posts", posts_router)
1253 .nest("/admin", admin_router);
1254
1255 let routes = app.registered_routes();
1257 assert_eq!(routes.len(), 6, "Should have 6 routes registered");
1258
1259 assert!(routes.contains_key("/api/v1/users/list"));
1261 assert!(routes.contains_key("/api/v1/users/:id"));
1262 assert!(routes.contains_key("/api/v1/posts/list"));
1263 assert!(routes.contains_key("/api/v1/posts/:id"));
1264 assert!(routes.contains_key("/admin/list"));
1265 assert!(routes.contains_key("/admin/:id"));
1266
1267 match app.match_route("/api/v1/users/list", &Method::GET) {
1269 RouteMatch::Found { params, .. } => {
1270 assert!(params.is_empty(), "Static path should have no params");
1271 }
1272 _ => panic!("Should find /api/v1/users/list"),
1273 }
1274
1275 match app.match_route("/api/v1/posts/123", &Method::GET) {
1276 RouteMatch::Found { params, .. } => {
1277 assert_eq!(params.get("id"), Some(&"123".to_string()));
1278 }
1279 _ => panic!("Should find /api/v1/posts/123"),
1280 }
1281
1282 match app.match_route("/admin/456", &Method::GET) {
1283 RouteMatch::Found { params, .. } => {
1284 assert_eq!(params.get("id"), Some(&"456".to_string()));
1285 }
1286 _ => panic!("Should find /admin/456"),
1287 }
1288 }
1289
1290 #[test]
1291 fn test_multiple_router_composition_with_multiple_methods() {
1292 async fn get_handler() -> &'static str {
1293 "get"
1294 }
1295 async fn post_handler() -> &'static str {
1296 "post"
1297 }
1298 async fn put_handler() -> &'static str {
1299 "put"
1300 }
1301
1302 let get_router = get(get_handler);
1305 let post_router = post(post_handler);
1306 let mut users_root_combined = MethodRouter::new();
1307 for (method, handler) in get_router.handlers {
1308 users_root_combined.handlers.insert(method, handler);
1309 }
1310 for (method, handler) in post_router.handlers {
1311 users_root_combined.handlers.insert(method, handler);
1312 }
1313
1314 let get_router2 = get(get_handler);
1316 let put_router = put(put_handler);
1317 let mut users_id_combined = MethodRouter::new();
1318 for (method, handler) in get_router2.handlers {
1319 users_id_combined.handlers.insert(method, handler);
1320 }
1321 for (method, handler) in put_router.handlers {
1322 users_id_combined.handlers.insert(method, handler);
1323 }
1324
1325 let users_router = Router::new()
1326 .route("/", users_root_combined)
1327 .route("/{id}", users_id_combined);
1328
1329 let get_router3 = get(get_handler);
1331 let post_router2 = post(post_handler);
1332 let mut posts_root_combined = MethodRouter::new();
1333 for (method, handler) in get_router3.handlers {
1334 posts_root_combined.handlers.insert(method, handler);
1335 }
1336 for (method, handler) in post_router2.handlers {
1337 posts_root_combined.handlers.insert(method, handler);
1338 }
1339
1340 let posts_router = Router::new().route("/", posts_root_combined);
1341
1342 let app = Router::new()
1344 .nest("/users", users_router)
1345 .nest("/posts", posts_router);
1346
1347 let routes = app.registered_routes();
1349 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1350
1351 let users_root = routes.get("/users").unwrap();
1353 assert!(users_root.methods.contains(&Method::GET));
1354 assert!(users_root.methods.contains(&Method::POST));
1355
1356 let users_id = routes.get("/users/:id").unwrap();
1357 assert!(users_id.methods.contains(&Method::GET));
1358 assert!(users_id.methods.contains(&Method::PUT));
1359
1360 let posts_root = routes.get("/posts").unwrap();
1362 assert!(posts_root.methods.contains(&Method::GET));
1363 assert!(posts_root.methods.contains(&Method::POST));
1364
1365 match app.match_route("/users", &Method::GET) {
1367 RouteMatch::Found { .. } => {}
1368 _ => panic!("GET /users should be found"),
1369 }
1370 match app.match_route("/users", &Method::POST) {
1371 RouteMatch::Found { .. } => {}
1372 _ => panic!("POST /users should be found"),
1373 }
1374 match app.match_route("/users/123", &Method::PUT) {
1375 RouteMatch::Found { .. } => {}
1376 _ => panic!("PUT /users/123 should be found"),
1377 }
1378 }
1379
1380 #[test]
1381 fn test_multiple_router_composition_deep_nesting() {
1382 async fn handler() -> &'static str {
1383 "handler"
1384 }
1385
1386 let deep_router = Router::new().route("/action", get(handler));
1388
1389 let mid_router = Router::new().route("/info", get(handler));
1390
1391 let shallow_router = Router::new().route("/status", get(handler));
1392
1393 let app = Router::new()
1395 .nest("/api/v1/resources/items", deep_router)
1396 .nest("/api/v1/resources", mid_router)
1397 .nest("/api", shallow_router);
1398
1399 let routes = app.registered_routes();
1401 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1402
1403 assert!(routes.contains_key("/api/v1/resources/items/action"));
1404 assert!(routes.contains_key("/api/v1/resources/info"));
1405 assert!(routes.contains_key("/api/status"));
1406
1407 match app.match_route("/api/v1/resources/items/action", &Method::GET) {
1409 RouteMatch::Found { .. } => {}
1410 _ => panic!("Should find deep route"),
1411 }
1412 match app.match_route("/api/v1/resources/info", &Method::GET) {
1413 RouteMatch::Found { .. } => {}
1414 _ => panic!("Should find mid route"),
1415 }
1416 match app.match_route("/api/status", &Method::GET) {
1417 RouteMatch::Found { .. } => {}
1418 _ => panic!("Should find shallow route"),
1419 }
1420 }
1421}
1422
1423#[cfg(test)]
1424mod property_tests {
1425 use super::*;
1426 use proptest::prelude::*;
1427 use std::panic::{catch_unwind, AssertUnwindSafe};
1428
1429 proptest! {
1437 #![proptest_config(ProptestConfig::with_cases(100))]
1438
1439 #[test]
1444 fn prop_normalized_prefix_starts_with_single_slash(
1445 leading_slashes in prop::collection::vec(Just('/'), 0..5),
1447 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 0..4),
1448 trailing_slashes in prop::collection::vec(Just('/'), 0..5),
1449 ) {
1450 let mut prefix = String::new();
1452 for _ in &leading_slashes {
1453 prefix.push('/');
1454 }
1455 for (i, segment) in segments.iter().enumerate() {
1456 if i > 0 {
1457 prefix.push('/');
1458 }
1459 prefix.push_str(segment);
1460 }
1461 for _ in &trailing_slashes {
1462 prefix.push('/');
1463 }
1464
1465 let normalized = normalize_prefix(&prefix);
1466
1467 prop_assert!(
1469 normalized.starts_with('/'),
1470 "Normalized prefix '{}' should start with '/', input was '{}'",
1471 normalized, prefix
1472 );
1473
1474 prop_assert!(
1476 !normalized.starts_with("//"),
1477 "Normalized prefix '{}' should not start with '//', input was '{}'",
1478 normalized, prefix
1479 );
1480 }
1481
1482 #[test]
1487 fn prop_normalized_prefix_no_trailing_slash(
1488 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1489 trailing_slashes in prop::collection::vec(Just('/'), 0..5),
1490 ) {
1491 let mut prefix = String::from("/");
1493 for (i, segment) in segments.iter().enumerate() {
1494 if i > 0 {
1495 prefix.push('/');
1496 }
1497 prefix.push_str(segment);
1498 }
1499 for _ in &trailing_slashes {
1500 prefix.push('/');
1501 }
1502
1503 let normalized = normalize_prefix(&prefix);
1504
1505 prop_assert!(
1507 !normalized.ends_with('/'),
1508 "Normalized prefix '{}' should not end with '/', input was '{}'",
1509 normalized, prefix
1510 );
1511 }
1512
1513 #[test]
1518 fn prop_normalized_prefix_no_double_slashes(
1519 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1521 extra_slashes in prop::collection::vec(0..4usize, 1..4),
1522 ) {
1523 let mut prefix = String::from("/");
1525 for (i, segment) in segments.iter().enumerate() {
1526 if i > 0 {
1527 let num_slashes = extra_slashes.get(i).copied().unwrap_or(1);
1529 for _ in 0..=num_slashes {
1530 prefix.push('/');
1531 }
1532 }
1533 prefix.push_str(segment);
1534 }
1535
1536 let normalized = normalize_prefix(&prefix);
1537
1538 prop_assert!(
1540 !normalized.contains("//"),
1541 "Normalized prefix '{}' should not contain '//', input was '{}'",
1542 normalized, prefix
1543 );
1544 }
1545
1546 #[test]
1551 fn prop_normalized_prefix_preserves_segments(
1552 segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..4),
1553 ) {
1554 let prefix = format!("/{}", segments.join("/"));
1556
1557 let normalized = normalize_prefix(&prefix);
1558
1559 let normalized_segments: Vec<&str> = normalized
1561 .split('/')
1562 .filter(|s| !s.is_empty())
1563 .collect();
1564
1565 prop_assert_eq!(
1566 segments.len(),
1567 normalized_segments.len(),
1568 "Segment count should be preserved"
1569 );
1570
1571 for (original, normalized_seg) in segments.iter().zip(normalized_segments.iter()) {
1572 prop_assert_eq!(
1573 original, normalized_seg,
1574 "Segment content should be preserved"
1575 );
1576 }
1577 }
1578
1579 #[test]
1584 fn prop_empty_or_slashes_normalize_to_root(
1585 num_slashes in 0..10usize,
1586 ) {
1587 let prefix: String = std::iter::repeat('/').take(num_slashes).collect();
1588
1589 let normalized = normalize_prefix(&prefix);
1590
1591 prop_assert_eq!(
1592 normalized, "/",
1593 "Empty or slash-only prefix '{}' should normalize to '/'",
1594 prefix
1595 );
1596 }
1597 }
1598
1599 proptest! {
1606 #![proptest_config(ProptestConfig::with_cases(100))]
1607
1608 #[test]
1613 fn prop_method_router_clone_preserves_methods(
1614 use_get in any::<bool>(),
1616 use_post in any::<bool>(),
1617 use_put in any::<bool>(),
1618 use_patch in any::<bool>(),
1619 use_delete in any::<bool>(),
1620 ) {
1621 prop_assume!(use_get || use_post || use_put || use_patch || use_delete);
1623
1624 let mut method_router = MethodRouter::new();
1626 let mut expected_methods: Vec<Method> = Vec::new();
1627
1628 async fn handler() -> &'static str { "handler" }
1629
1630 if use_get {
1631 method_router = get(handler);
1632 expected_methods.push(Method::GET);
1633 }
1634
1635 if use_post {
1636 let post_router = post(handler);
1637 for (method, handler) in post_router.handlers {
1638 method_router.handlers.insert(method.clone(), handler);
1639 if !expected_methods.contains(&method) {
1640 expected_methods.push(method);
1641 }
1642 }
1643 }
1644
1645 if use_put {
1646 let put_router = put(handler);
1647 for (method, handler) in put_router.handlers {
1648 method_router.handlers.insert(method.clone(), handler);
1649 if !expected_methods.contains(&method) {
1650 expected_methods.push(method);
1651 }
1652 }
1653 }
1654
1655 if use_patch {
1656 let patch_router = patch(handler);
1657 for (method, handler) in patch_router.handlers {
1658 method_router.handlers.insert(method.clone(), handler);
1659 if !expected_methods.contains(&method) {
1660 expected_methods.push(method);
1661 }
1662 }
1663 }
1664
1665 if use_delete {
1666 let delete_router = delete(handler);
1667 for (method, handler) in delete_router.handlers {
1668 method_router.handlers.insert(method.clone(), handler);
1669 if !expected_methods.contains(&method) {
1670 expected_methods.push(method);
1671 }
1672 }
1673 }
1674
1675 let cloned_router = method_router.clone();
1677
1678 let original_methods = method_router.allowed_methods();
1680 let cloned_methods = cloned_router.allowed_methods();
1681
1682 prop_assert_eq!(
1683 original_methods.len(),
1684 cloned_methods.len(),
1685 "Cloned router should have same number of methods"
1686 );
1687
1688 for method in &expected_methods {
1689 prop_assert!(
1690 cloned_router.get_handler(method).is_some(),
1691 "Cloned router should have handler for method {:?}",
1692 method
1693 );
1694 }
1695
1696 for method in &cloned_methods {
1698 prop_assert!(
1699 cloned_router.get_handler(method).is_some(),
1700 "Handler for {:?} should be accessible after clone",
1701 method
1702 );
1703 }
1704 }
1705 }
1706
1707 proptest! {
1715 #![proptest_config(ProptestConfig::with_cases(100))]
1716
1717 #[test]
1722 fn prop_nested_routes_have_prefix(
1723 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1725 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1727 has_param in any::<bool>(),
1728 ) {
1729 async fn handler() -> &'static str { "handler" }
1730
1731 let prefix = format!("/{}", prefix_segments.join("/"));
1733
1734 let mut route_path = format!("/{}", route_segments.join("/"));
1736 if has_param {
1737 route_path.push_str("/{id}");
1738 }
1739
1740 let nested_router = Router::new().route(&route_path, get(handler));
1742 let app = Router::new().nest(&prefix, nested_router);
1743
1744 let expected_matchit_path = if has_param {
1746 format!("{}/{}/:id", prefix, route_segments.join("/"))
1747 } else {
1748 format!("{}/{}", prefix, route_segments.join("/"))
1749 };
1750
1751 let routes = app.registered_routes();
1752
1753 prop_assert!(
1755 routes.contains_key(&expected_matchit_path),
1756 "Expected route '{}' not found. Available routes: {:?}",
1757 expected_matchit_path,
1758 routes.keys().collect::<Vec<_>>()
1759 );
1760
1761 let route_info = routes.get(&expected_matchit_path).unwrap();
1763 let expected_display_path = format!("{}{}", prefix, route_path);
1764 prop_assert_eq!(
1765 &route_info.path, &expected_display_path,
1766 "Display path should be prefix + original path"
1767 );
1768 }
1769
1770 #[test]
1775 fn prop_route_count_preserved_after_nesting(
1776 num_routes in 1..4usize,
1778 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1779 ) {
1780 async fn handler() -> &'static str { "handler" }
1781
1782 let prefix = format!("/{}", prefix_segments.join("/"));
1783
1784 let mut nested_router = Router::new();
1786 for i in 0..num_routes {
1787 let path = format!("/route{}", i);
1788 nested_router = nested_router.route(&path, get(handler));
1789 }
1790
1791 let app = Router::new().nest(&prefix, nested_router);
1792
1793 prop_assert_eq!(
1794 app.registered_routes().len(),
1795 num_routes,
1796 "Number of routes should be preserved after nesting"
1797 );
1798 }
1799
1800 #[test]
1804 fn prop_nested_routes_are_matchable(
1805 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1806 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1807 ) {
1808 async fn handler() -> &'static str { "handler" }
1809
1810 let prefix = format!("/{}", prefix_segments.join("/"));
1811 let route_path = format!("/{}", route_segments.join("/"));
1812
1813 let nested_router = Router::new().route(&route_path, get(handler));
1814 let app = Router::new().nest(&prefix, nested_router);
1815
1816 let full_path = format!("{}{}", prefix, route_path);
1818
1819 match app.match_route(&full_path, &Method::GET) {
1821 RouteMatch::Found { .. } => {
1822 }
1824 RouteMatch::NotFound => {
1825 prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
1826 }
1827 RouteMatch::MethodNotAllowed { .. } => {
1828 prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
1829 }
1830 }
1831 }
1832 }
1833
1834 proptest! {
1841 #![proptest_config(ProptestConfig::with_cases(100))]
1842
1843 #[test]
1848 fn prop_state_type_ids_merged(
1849 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1850 has_nested_state in any::<bool>(),
1851 ) {
1852 #[derive(Clone)]
1853 struct TestState(i32);
1854
1855 async fn handler() -> &'static str { "handler" }
1856
1857 let prefix = format!("/{}", prefix_segments.join("/"));
1858
1859 let mut nested = Router::new().route("/test", get(handler));
1860 if has_nested_state {
1861 nested = nested.state(TestState(42));
1862 }
1863
1864 let parent = Router::new().nest(&prefix, nested);
1865
1866 if has_nested_state {
1868 prop_assert!(
1869 parent.state_type_ids().contains(&std::any::TypeId::of::<TestState>()),
1870 "Parent should track nested state type ID"
1871 );
1872 }
1873 }
1874
1875 #[test]
1880 fn prop_merge_state_adds_nested_state(
1881 state_value in any::<i32>(),
1882 ) {
1883 #[derive(Clone, PartialEq, Debug)]
1884 struct UniqueState(i32);
1885
1886 let source = Router::new().state(UniqueState(state_value));
1888
1889 let parent = Router::new().merge_state::<UniqueState>(&source);
1891
1892 prop_assert!(
1894 parent.has_state::<UniqueState>(),
1895 "Parent should have state after merge"
1896 );
1897
1898 let merged_state = parent.state.get::<UniqueState>().unwrap();
1900 prop_assert_eq!(
1901 merged_state.0, state_value,
1902 "Merged state value should match source"
1903 );
1904 }
1905 }
1906
1907 proptest! {
1914 #![proptest_config(ProptestConfig::with_cases(100))]
1915
1916 #[test]
1921 fn prop_parent_state_takes_precedence(
1922 parent_value in any::<i32>(),
1923 nested_value in any::<i32>(),
1924 ) {
1925 prop_assume!(parent_value != nested_value);
1927
1928 #[derive(Clone, PartialEq, Debug)]
1929 struct SharedState(i32);
1930
1931 let source = Router::new().state(SharedState(nested_value));
1933
1934 let parent = Router::new()
1936 .state(SharedState(parent_value))
1937 .merge_state::<SharedState>(&source);
1938
1939 prop_assert!(
1941 parent.has_state::<SharedState>(),
1942 "Parent should have state"
1943 );
1944
1945 let final_state = parent.state.get::<SharedState>().unwrap();
1947 prop_assert_eq!(
1948 final_state.0, parent_value,
1949 "Parent state value should be preserved, not overwritten by nested"
1950 );
1951 }
1952
1953 #[test]
1958 fn prop_state_precedence_consistent(
1959 parent_value in any::<i32>(),
1960 source1_value in any::<i32>(),
1961 source2_value in any::<i32>(),
1962 ) {
1963 #[derive(Clone, PartialEq, Debug)]
1964 struct ConsistentState(i32);
1965
1966 let source1 = Router::new().state(ConsistentState(source1_value));
1968 let source2 = Router::new().state(ConsistentState(source2_value));
1969
1970 let parent = Router::new()
1972 .state(ConsistentState(parent_value))
1973 .merge_state::<ConsistentState>(&source1)
1974 .merge_state::<ConsistentState>(&source2);
1975
1976 let final_state = parent.state.get::<ConsistentState>().unwrap();
1978 prop_assert_eq!(
1979 final_state.0, parent_value,
1980 "Parent state should be preserved after multiple merges"
1981 );
1982 }
1983 }
1984
1985 proptest! {
1993 #![proptest_config(ProptestConfig::with_cases(100))]
1994
1995 #[test]
2000 fn prop_same_structure_different_param_names_conflict(
2001 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
2003 param1 in "[a-z][a-z0-9]{0,5}",
2005 param2 in "[a-z][a-z0-9]{0,5}",
2006 ) {
2007 prop_assume!(param1 != param2);
2009
2010 let mut path1 = String::from("/");
2012 let mut path2 = String::from("/");
2013
2014 for segment in &segments {
2015 path1.push_str(segment);
2016 path1.push('/');
2017 path2.push_str(segment);
2018 path2.push('/');
2019 }
2020
2021 path1.push('{');
2022 path1.push_str(¶m1);
2023 path1.push('}');
2024
2025 path2.push('{');
2026 path2.push_str(¶m2);
2027 path2.push('}');
2028
2029 let result = catch_unwind(AssertUnwindSafe(|| {
2031 async fn handler1() -> &'static str { "handler1" }
2032 async fn handler2() -> &'static str { "handler2" }
2033
2034 let _router = Router::new()
2035 .route(&path1, get(handler1))
2036 .route(&path2, get(handler2));
2037 }));
2038
2039 prop_assert!(
2040 result.is_err(),
2041 "Routes '{}' and '{}' should conflict but didn't",
2042 path1, path2
2043 );
2044 }
2045
2046 #[test]
2051 fn prop_different_structures_no_conflict(
2052 segments1 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2054 segments2 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2055 has_param1 in any::<bool>(),
2057 has_param2 in any::<bool>(),
2058 ) {
2059 let mut path1 = String::from("/");
2061 let mut path2 = String::from("/");
2062
2063 for segment in &segments1 {
2064 path1.push_str(segment);
2065 path1.push('/');
2066 }
2067 path1.pop(); for segment in &segments2 {
2070 path2.push_str(segment);
2071 path2.push('/');
2072 }
2073 path2.pop(); if has_param1 {
2076 path1.push_str("/{id}");
2077 }
2078
2079 if has_param2 {
2080 path2.push_str("/{id}");
2081 }
2082
2083 let norm1 = normalize_path_for_comparison(&convert_path_params(&path1));
2085 let norm2 = normalize_path_for_comparison(&convert_path_params(&path2));
2086
2087 prop_assume!(norm1 != norm2);
2089
2090 let result = catch_unwind(AssertUnwindSafe(|| {
2092 async fn handler1() -> &'static str { "handler1" }
2093 async fn handler2() -> &'static str { "handler2" }
2094
2095 let router = Router::new()
2096 .route(&path1, get(handler1))
2097 .route(&path2, get(handler2));
2098
2099 router.registered_routes().len()
2100 }));
2101
2102 prop_assert!(
2103 result.is_ok(),
2104 "Routes '{}' and '{}' should not conflict but did",
2105 path1, path2
2106 );
2107
2108 if let Ok(count) = result {
2109 prop_assert_eq!(count, 2, "Should have registered 2 routes");
2110 }
2111 }
2112
2113 #[test]
2118 fn prop_conflict_error_contains_both_paths(
2119 segment in "[a-z][a-z0-9]{1,5}",
2121 param1 in "[a-z][a-z0-9]{1,5}",
2122 param2 in "[a-z][a-z0-9]{1,5}",
2123 ) {
2124 prop_assume!(param1 != param2);
2125
2126 let path1 = format!("/{}/{{{}}}", segment, param1);
2127 let path2 = format!("/{}/{{{}}}", segment, param2);
2128
2129 let result = catch_unwind(AssertUnwindSafe(|| {
2130 async fn handler1() -> &'static str { "handler1" }
2131 async fn handler2() -> &'static str { "handler2" }
2132
2133 let _router = Router::new()
2134 .route(&path1, get(handler1))
2135 .route(&path2, get(handler2));
2136 }));
2137
2138 prop_assert!(result.is_err(), "Should have panicked due to conflict");
2139
2140 if let Err(panic_info) = result {
2142 if let Some(msg) = panic_info.downcast_ref::<String>() {
2143 prop_assert!(
2144 msg.contains("ROUTE CONFLICT DETECTED"),
2145 "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
2146 msg
2147 );
2148 prop_assert!(
2149 msg.contains("Existing:") && msg.contains("New:"),
2150 "Error should contain both 'Existing:' and 'New:' labels, got: {}",
2151 msg
2152 );
2153 prop_assert!(
2154 msg.contains("How to resolve:"),
2155 "Error should contain resolution guidance, got: {}",
2156 msg
2157 );
2158 }
2159 }
2160 }
2161
2162 #[test]
2166 fn prop_exact_duplicate_paths_conflict(
2167 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
2169 has_param in any::<bool>(),
2170 ) {
2171 let mut path = String::from("/");
2173
2174 for segment in &segments {
2175 path.push_str(segment);
2176 path.push('/');
2177 }
2178 path.pop(); if has_param {
2181 path.push_str("/{id}");
2182 }
2183
2184 let result = catch_unwind(AssertUnwindSafe(|| {
2186 async fn handler1() -> &'static str { "handler1" }
2187 async fn handler2() -> &'static str { "handler2" }
2188
2189 let _router = Router::new()
2190 .route(&path, get(handler1))
2191 .route(&path, get(handler2));
2192 }));
2193
2194 prop_assert!(
2195 result.is_err(),
2196 "Registering path '{}' twice should conflict but didn't",
2197 path
2198 );
2199 }
2200 }
2201
2202 proptest! {
2209 #![proptest_config(ProptestConfig::with_cases(100))]
2210
2211 #[test]
2216 fn prop_nested_route_with_params_matches(
2217 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2218 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
2219 param_value in "[a-z0-9]{1,10}",
2220 ) {
2221 async fn handler() -> &'static str { "handler" }
2222
2223 let prefix = format!("/{}", prefix_segments.join("/"));
2224 let route_path = if route_segments.is_empty() {
2225 "/{id}".to_string()
2226 } else {
2227 format!("/{}/{{id}}", route_segments.join("/"))
2228 };
2229
2230 let nested_router = Router::new().route(&route_path, get(handler));
2231 let app = Router::new().nest(&prefix, nested_router);
2232
2233 let full_path = if route_segments.is_empty() {
2235 format!("{}/{}", prefix, param_value)
2236 } else {
2237 format!("{}/{}/{}", prefix, route_segments.join("/"), param_value)
2238 };
2239
2240 match app.match_route(&full_path, &Method::GET) {
2242 RouteMatch::Found { params, .. } => {
2243 prop_assert!(
2245 params.contains_key("id"),
2246 "Should have 'id' parameter, got: {:?}",
2247 params
2248 );
2249 prop_assert_eq!(
2250 params.get("id").unwrap(),
2251 ¶m_value,
2252 "Parameter value should match"
2253 );
2254 }
2255 RouteMatch::NotFound => {
2256 prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
2257 }
2258 RouteMatch::MethodNotAllowed { .. } => {
2259 prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
2260 }
2261 }
2262 }
2263
2264 #[test]
2269 fn prop_nested_route_matches_correct_method(
2270 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2271 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2272 use_get in any::<bool>(),
2273 ) {
2274 async fn handler() -> &'static str { "handler" }
2275
2276 let prefix = format!("/{}", prefix_segments.join("/"));
2277 let route_path = format!("/{}", route_segments.join("/"));
2278
2279 let method_router = if use_get { get(handler) } else { post(handler) };
2281 let nested_router = Router::new().route(&route_path, method_router);
2282 let app = Router::new().nest(&prefix, nested_router);
2283
2284 let full_path = format!("{}{}", prefix, route_path);
2285 let registered_method = if use_get { Method::GET } else { Method::POST };
2286 let other_method = if use_get { Method::POST } else { Method::GET };
2287
2288 match app.match_route(&full_path, ®istered_method) {
2290 RouteMatch::Found { .. } => {
2291 }
2293 other => {
2294 prop_assert!(false, "Route should be found for registered method, got: {:?}",
2295 match other {
2296 RouteMatch::NotFound => "NotFound",
2297 RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
2298 _ => "Found",
2299 }
2300 );
2301 }
2302 }
2303
2304 match app.match_route(&full_path, &other_method) {
2306 RouteMatch::MethodNotAllowed { allowed } => {
2307 prop_assert!(
2308 allowed.contains(®istered_method),
2309 "Allowed methods should contain {:?}",
2310 registered_method
2311 );
2312 }
2313 other => {
2314 prop_assert!(false, "Route should return MethodNotAllowed for other method, got: {:?}",
2315 match other {
2316 RouteMatch::NotFound => "NotFound",
2317 RouteMatch::Found { .. } => "Found",
2318 _ => "MethodNotAllowed",
2319 }
2320 );
2321 }
2322 }
2323 }
2324 }
2325
2326 proptest! {
2333 #![proptest_config(ProptestConfig::with_cases(100))]
2334
2335 #[test]
2340 fn prop_single_param_extraction(
2341 prefix in "[a-z][a-z0-9]{1,5}",
2342 param_name in "[a-z][a-z0-9]{1,5}",
2343 param_value in "[a-z0-9]{1,10}",
2344 ) {
2345 async fn handler() -> &'static str { "handler" }
2346
2347 let prefix = format!("/{}", prefix);
2348 let route_path = format!("/{{{}}}", param_name);
2349
2350 let nested_router = Router::new().route(&route_path, get(handler));
2351 let app = Router::new().nest(&prefix, nested_router);
2352
2353 let full_path = format!("{}/{}", prefix, param_value);
2354
2355 match app.match_route(&full_path, &Method::GET) {
2356 RouteMatch::Found { params, .. } => {
2357 prop_assert!(
2358 params.contains_key(¶m_name),
2359 "Should have '{}' parameter, got: {:?}",
2360 param_name, params
2361 );
2362 prop_assert_eq!(
2363 params.get(¶m_name).unwrap(),
2364 ¶m_value,
2365 "Parameter '{}' value should be '{}'",
2366 param_name, param_value
2367 );
2368 }
2369 _ => {
2370 prop_assert!(false, "Route should be found");
2371 }
2372 }
2373 }
2374
2375 #[test]
2380 fn prop_multiple_params_extraction(
2381 prefix in "[a-z][a-z0-9]{1,5}",
2382 param1_name in "[a-z]{1,5}",
2383 param1_value in "[a-z0-9]{1,10}",
2384 param2_name in "[a-z]{1,5}",
2385 param2_value in "[a-z0-9]{1,10}",
2386 ) {
2387 prop_assume!(param1_name != param2_name);
2389
2390 async fn handler() -> &'static str { "handler" }
2391
2392 let prefix = format!("/{}", prefix);
2393 let route_path = format!("/{{{}}}/items/{{{}}}", param1_name, param2_name);
2394
2395 let nested_router = Router::new().route(&route_path, get(handler));
2396 let app = Router::new().nest(&prefix, nested_router);
2397
2398 let full_path = format!("{}/{}/items/{}", prefix, param1_value, param2_value);
2399
2400 match app.match_route(&full_path, &Method::GET) {
2401 RouteMatch::Found { params, .. } => {
2402 prop_assert!(
2404 params.contains_key(¶m1_name),
2405 "Should have '{}' parameter, got: {:?}",
2406 param1_name, params
2407 );
2408 prop_assert_eq!(
2409 params.get(¶m1_name).unwrap(),
2410 ¶m1_value,
2411 "Parameter '{}' value should be '{}'",
2412 param1_name, param1_value
2413 );
2414
2415 prop_assert!(
2417 params.contains_key(¶m2_name),
2418 "Should have '{}' parameter, got: {:?}",
2419 param2_name, params
2420 );
2421 prop_assert_eq!(
2422 params.get(¶m2_name).unwrap(),
2423 ¶m2_value,
2424 "Parameter '{}' value should be '{}'",
2425 param2_name, param2_value
2426 );
2427 }
2428 _ => {
2429 prop_assert!(false, "Route should be found");
2430 }
2431 }
2432 }
2433
2434 #[test]
2439 fn prop_param_value_preservation(
2440 prefix in "[a-z]{1,5}",
2441 param_value in "[a-zA-Z0-9_-]{1,15}",
2443 ) {
2444 async fn handler() -> &'static str { "handler" }
2445
2446 let prefix = format!("/{}", prefix);
2447 let route_path = "/{id}".to_string();
2448
2449 let nested_router = Router::new().route(&route_path, get(handler));
2450 let app = Router::new().nest(&prefix, nested_router);
2451
2452 let full_path = format!("{}/{}", prefix, param_value);
2453
2454 match app.match_route(&full_path, &Method::GET) {
2455 RouteMatch::Found { params, .. } => {
2456 prop_assert_eq!(
2457 params.get("id").unwrap(),
2458 ¶m_value,
2459 "Parameter value should be preserved exactly"
2460 );
2461 }
2462 _ => {
2463 prop_assert!(false, "Route should be found");
2464 }
2465 }
2466 }
2467 }
2468
2469 proptest! {
2476 #![proptest_config(ProptestConfig::with_cases(100))]
2477
2478 #[test]
2483 fn prop_unregistered_path_returns_not_found(
2484 prefix in "[a-z][a-z0-9]{1,5}",
2485 route_segment in "[a-z][a-z0-9]{1,5}",
2486 unregistered_segment in "[a-z][a-z0-9]{6,10}",
2487 ) {
2488 prop_assume!(route_segment != unregistered_segment);
2490
2491 async fn handler() -> &'static str { "handler" }
2492
2493 let prefix = format!("/{}", prefix);
2494 let route_path = format!("/{}", route_segment);
2495
2496 let nested_router = Router::new().route(&route_path, get(handler));
2497 let app = Router::new().nest(&prefix, nested_router);
2498
2499 let unregistered_path = format!("{}/{}", prefix, unregistered_segment);
2501
2502 match app.match_route(&unregistered_path, &Method::GET) {
2503 RouteMatch::NotFound => {
2504 }
2506 RouteMatch::Found { .. } => {
2507 prop_assert!(false, "Path '{}' should not be found", unregistered_path);
2508 }
2509 RouteMatch::MethodNotAllowed { .. } => {
2510 prop_assert!(false, "Path '{}' should return NotFound, not MethodNotAllowed", unregistered_path);
2511 }
2512 }
2513 }
2514
2515 #[test]
2519 fn prop_wrong_prefix_returns_not_found(
2520 prefix1 in "[a-z][a-z0-9]{1,5}",
2521 prefix2 in "[a-z][a-z0-9]{6,10}",
2522 route_segment in "[a-z][a-z0-9]{1,5}",
2523 ) {
2524 prop_assume!(prefix1 != prefix2);
2526
2527 async fn handler() -> &'static str { "handler" }
2528
2529 let prefix = format!("/{}", prefix1);
2530 let route_path = format!("/{}", route_segment);
2531
2532 let nested_router = Router::new().route(&route_path, get(handler));
2533 let app = Router::new().nest(&prefix, nested_router);
2534
2535 let wrong_prefix_path = format!("/{}/{}", prefix2, route_segment);
2537
2538 match app.match_route(&wrong_prefix_path, &Method::GET) {
2539 RouteMatch::NotFound => {
2540 }
2542 _ => {
2543 prop_assert!(false, "Path '{}' with wrong prefix should return NotFound", wrong_prefix_path);
2544 }
2545 }
2546 }
2547
2548 #[test]
2553 fn prop_partial_path_returns_not_found(
2554 prefix in "[a-z][a-z0-9]{1,5}",
2555 segment1 in "[a-z][a-z0-9]{1,5}",
2556 segment2 in "[a-z][a-z0-9]{1,5}",
2557 ) {
2558 async fn handler() -> &'static str { "handler" }
2559
2560 let prefix = format!("/{}", prefix);
2561 let route_path = format!("/{}/{}", segment1, segment2);
2562
2563 let nested_router = Router::new().route(&route_path, get(handler));
2564 let app = Router::new().nest(&prefix, nested_router);
2565
2566 let partial_path = format!("{}/{}", prefix, segment1);
2568
2569 match app.match_route(&partial_path, &Method::GET) {
2570 RouteMatch::NotFound => {
2571 }
2573 _ => {
2574 prop_assert!(false, "Partial path '{}' should return NotFound", partial_path);
2575 }
2576 }
2577 }
2578 }
2579
2580 proptest! {
2587 #![proptest_config(ProptestConfig::with_cases(100))]
2588
2589 #[test]
2595 fn prop_unregistered_method_returns_method_not_allowed(
2596 prefix in "[a-z][a-z0-9]{1,5}",
2597 route_segment in "[a-z][a-z0-9]{1,5}",
2598 ) {
2599 async fn handler() -> &'static str { "handler" }
2600
2601 let prefix = format!("/{}", prefix);
2602 let route_path = format!("/{}", route_segment);
2603
2604 let nested_router = Router::new().route(&route_path, get(handler));
2606 let app = Router::new().nest(&prefix, nested_router);
2607
2608 let full_path = format!("{}{}", prefix, route_path);
2609
2610 match app.match_route(&full_path, &Method::POST) {
2612 RouteMatch::MethodNotAllowed { allowed } => {
2613 prop_assert!(
2614 allowed.contains(&Method::GET),
2615 "Allowed methods should contain GET, got: {:?}",
2616 allowed
2617 );
2618 prop_assert!(
2619 !allowed.contains(&Method::POST),
2620 "Allowed methods should not contain POST"
2621 );
2622 }
2623 RouteMatch::Found { .. } => {
2624 prop_assert!(false, "POST should not be found on GET-only route");
2625 }
2626 RouteMatch::NotFound => {
2627 prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
2628 }
2629 }
2630 }
2631
2632 #[test]
2637 fn prop_multiple_methods_in_allowed_list(
2638 prefix in "[a-z][a-z0-9]{1,5}",
2639 route_segment in "[a-z][a-z0-9]{1,5}",
2640 use_get in any::<bool>(),
2641 use_post in any::<bool>(),
2642 use_put in any::<bool>(),
2643 ) {
2644 prop_assume!(use_get || use_post || use_put);
2646
2647 async fn handler() -> &'static str { "handler" }
2648
2649 let prefix = format!("/{}", prefix);
2650 let route_path = format!("/{}", route_segment);
2651
2652 let mut method_router = MethodRouter::new();
2654 let mut expected_methods: Vec<Method> = Vec::new();
2655
2656 if use_get {
2657 let get_router = get(handler);
2658 for (method, h) in get_router.handlers {
2659 method_router.handlers.insert(method.clone(), h);
2660 expected_methods.push(method);
2661 }
2662 }
2663 if use_post {
2664 let post_router = post(handler);
2665 for (method, h) in post_router.handlers {
2666 method_router.handlers.insert(method.clone(), h);
2667 expected_methods.push(method);
2668 }
2669 }
2670 if use_put {
2671 let put_router = put(handler);
2672 for (method, h) in put_router.handlers {
2673 method_router.handlers.insert(method.clone(), h);
2674 expected_methods.push(method);
2675 }
2676 }
2677
2678 let nested_router = Router::new().route(&route_path, method_router);
2679 let app = Router::new().nest(&prefix, nested_router);
2680
2681 let full_path = format!("{}{}", prefix, route_path);
2682
2683 match app.match_route(&full_path, &Method::DELETE) {
2685 RouteMatch::MethodNotAllowed { allowed } => {
2686 for method in &expected_methods {
2688 prop_assert!(
2689 allowed.contains(method),
2690 "Allowed methods should contain {:?}, got: {:?}",
2691 method, allowed
2692 );
2693 }
2694 prop_assert!(
2696 !allowed.contains(&Method::DELETE),
2697 "Allowed methods should not contain DELETE"
2698 );
2699 }
2700 RouteMatch::Found { .. } => {
2701 prop_assert!(false, "DELETE should not be found");
2702 }
2703 RouteMatch::NotFound => {
2704 prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
2705 }
2706 }
2707 }
2708 }
2709
2710 proptest! {
2724 #![proptest_config(ProptestConfig::with_cases(100))]
2725
2726 #[test]
2732 fn prop_multiple_routers_all_routes_registered(
2733 prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2735 prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2736 num_routes1 in 1..4usize,
2738 num_routes2 in 1..4usize,
2739 ) {
2740 let prefix1 = format!("/{}", prefix1_segments.join("/"));
2742 let prefix2 = format!("/{}", prefix2_segments.join("/"));
2743
2744 prop_assume!(prefix1 != prefix2);
2746
2747 async fn handler() -> &'static str { "handler" }
2748
2749 let mut router1 = Router::new();
2751 for i in 0..num_routes1 {
2752 let path = format!("/route1_{}", i);
2753 router1 = router1.route(&path, get(handler));
2754 }
2755
2756 let mut router2 = Router::new();
2758 for i in 0..num_routes2 {
2759 let path = format!("/route2_{}", i);
2760 router2 = router2.route(&path, get(handler));
2761 }
2762
2763 let app = Router::new()
2765 .nest(&prefix1, router1)
2766 .nest(&prefix2, router2);
2767
2768 let routes = app.registered_routes();
2769
2770 let expected_count = num_routes1 + num_routes2;
2772 prop_assert_eq!(
2773 routes.len(),
2774 expected_count,
2775 "Should have {} routes ({}+{}), got {}",
2776 expected_count, num_routes1, num_routes2, routes.len()
2777 );
2778
2779 for i in 0..num_routes1 {
2781 let expected_path = format!("{}/route1_{}", prefix1, i);
2782 let matchit_path = convert_path_params(&expected_path);
2783 prop_assert!(
2784 routes.contains_key(&matchit_path),
2785 "Route '{}' should be registered",
2786 expected_path
2787 );
2788 }
2789
2790 for i in 0..num_routes2 {
2792 let expected_path = format!("{}/route2_{}", prefix2, i);
2793 let matchit_path = convert_path_params(&expected_path);
2794 prop_assert!(
2795 routes.contains_key(&matchit_path),
2796 "Route '{}' should be registered",
2797 expected_path
2798 );
2799 }
2800 }
2801
2802 #[test]
2807 fn prop_multiple_routers_no_interference(
2808 prefix1 in "[a-z][a-z0-9]{1,5}",
2809 prefix2 in "[a-z][a-z0-9]{1,5}",
2810 route_segment in "[a-z][a-z0-9]{1,5}",
2811 param_value1 in "[a-z0-9]{1,10}",
2812 param_value2 in "[a-z0-9]{1,10}",
2813 ) {
2814 prop_assume!(prefix1 != prefix2);
2816
2817 let prefix1 = format!("/{}", prefix1);
2818 let prefix2 = format!("/{}", prefix2);
2819
2820 async fn handler() -> &'static str { "handler" }
2821
2822 let router1 = Router::new()
2824 .route(&format!("/{}", route_segment), get(handler))
2825 .route("/{id}", get(handler));
2826
2827 let router2 = Router::new()
2828 .route(&format!("/{}", route_segment), get(handler))
2829 .route("/{id}", get(handler));
2830
2831 let app = Router::new()
2833 .nest(&prefix1, router1)
2834 .nest(&prefix2, router2);
2835
2836 let path1_static = format!("{}/{}", prefix1, route_segment);
2838 match app.match_route(&path1_static, &Method::GET) {
2839 RouteMatch::Found { params, .. } => {
2840 prop_assert!(params.is_empty(), "Static path should have no params");
2841 }
2842 _ => {
2843 prop_assert!(false, "Route '{}' should be found", path1_static);
2844 }
2845 }
2846
2847 let path1_param = format!("{}/{}", prefix1, param_value1);
2848 match app.match_route(&path1_param, &Method::GET) {
2849 RouteMatch::Found { params, .. } => {
2850 prop_assert_eq!(
2851 params.get("id"),
2852 Some(¶m_value1.to_string()),
2853 "Parameter should be extracted correctly"
2854 );
2855 }
2856 _ => {
2857 prop_assert!(false, "Route '{}' should be found", path1_param);
2858 }
2859 }
2860
2861 let path2_static = format!("{}/{}", prefix2, route_segment);
2863 match app.match_route(&path2_static, &Method::GET) {
2864 RouteMatch::Found { params, .. } => {
2865 prop_assert!(params.is_empty(), "Static path should have no params");
2866 }
2867 _ => {
2868 prop_assert!(false, "Route '{}' should be found", path2_static);
2869 }
2870 }
2871
2872 let path2_param = format!("{}/{}", prefix2, param_value2);
2873 match app.match_route(&path2_param, &Method::GET) {
2874 RouteMatch::Found { params, .. } => {
2875 prop_assert_eq!(
2876 params.get("id"),
2877 Some(¶m_value2.to_string()),
2878 "Parameter should be extracted correctly"
2879 );
2880 }
2881 _ => {
2882 prop_assert!(false, "Route '{}' should be found", path2_param);
2883 }
2884 }
2885 }
2886
2887 #[test]
2892 fn prop_multiple_routers_preserve_methods(
2893 prefix1 in "[a-z][a-z0-9]{1,5}",
2894 prefix2 in "[a-z][a-z0-9]{1,5}",
2895 route_segment in "[a-z][a-z0-9]{1,5}",
2896 router1_use_get in any::<bool>(),
2897 router1_use_post in any::<bool>(),
2898 router2_use_get in any::<bool>(),
2899 router2_use_put in any::<bool>(),
2900 ) {
2901 prop_assume!(router1_use_get || router1_use_post);
2903 prop_assume!(router2_use_get || router2_use_put);
2904 prop_assume!(prefix1 != prefix2);
2906
2907 let prefix1 = format!("/{}", prefix1);
2908 let prefix2 = format!("/{}", prefix2);
2909 let route_path = format!("/{}", route_segment);
2910
2911 async fn handler() -> &'static str { "handler" }
2912
2913 let mut method_router1 = MethodRouter::new();
2915 let mut expected_methods1: Vec<Method> = Vec::new();
2916 if router1_use_get {
2917 let get_router = get(handler);
2918 for (method, h) in get_router.handlers {
2919 method_router1.handlers.insert(method.clone(), h);
2920 expected_methods1.push(method);
2921 }
2922 }
2923 if router1_use_post {
2924 let post_router = post(handler);
2925 for (method, h) in post_router.handlers {
2926 method_router1.handlers.insert(method.clone(), h);
2927 expected_methods1.push(method);
2928 }
2929 }
2930
2931 let mut method_router2 = MethodRouter::new();
2933 let mut expected_methods2: Vec<Method> = Vec::new();
2934 if router2_use_get {
2935 let get_router = get(handler);
2936 for (method, h) in get_router.handlers {
2937 method_router2.handlers.insert(method.clone(), h);
2938 expected_methods2.push(method);
2939 }
2940 }
2941 if router2_use_put {
2942 let put_router = put(handler);
2943 for (method, h) in put_router.handlers {
2944 method_router2.handlers.insert(method.clone(), h);
2945 expected_methods2.push(method);
2946 }
2947 }
2948
2949 let router1 = Router::new().route(&route_path, method_router1);
2950 let router2 = Router::new().route(&route_path, method_router2);
2951
2952 let app = Router::new()
2953 .nest(&prefix1, router1)
2954 .nest(&prefix2, router2);
2955
2956 let full_path1 = format!("{}{}", prefix1, route_path);
2957 let full_path2 = format!("{}{}", prefix2, route_path);
2958
2959 for method in &expected_methods1 {
2961 match app.match_route(&full_path1, method) {
2962 RouteMatch::Found { .. } => {}
2963 _ => {
2964 prop_assert!(false, "Method {:?} should be found for {}", method, full_path1);
2965 }
2966 }
2967 }
2968
2969 for method in &expected_methods2 {
2971 match app.match_route(&full_path2, method) {
2972 RouteMatch::Found { .. } => {}
2973 _ => {
2974 prop_assert!(false, "Method {:?} should be found for {}", method, full_path2);
2975 }
2976 }
2977 }
2978
2979 if !expected_methods1.contains(&Method::DELETE) {
2981 match app.match_route(&full_path1, &Method::DELETE) {
2982 RouteMatch::MethodNotAllowed { allowed } => {
2983 for method in &expected_methods1 {
2984 prop_assert!(
2985 allowed.contains(method),
2986 "Allowed methods for {} should contain {:?}",
2987 full_path1, method
2988 );
2989 }
2990 }
2991 _ => {
2992 prop_assert!(false, "DELETE should return MethodNotAllowed for {}", full_path1);
2993 }
2994 }
2995 }
2996 }
2997
2998 #[test]
3003 fn prop_three_routers_composition(
3004 prefix1 in "[a-z]{1,3}",
3005 prefix2 in "[a-z]{4,6}",
3006 prefix3 in "[a-z]{7,9}",
3007 num_routes in 1..3usize,
3008 ) {
3009 let prefix1 = format!("/{}", prefix1);
3010 let prefix2 = format!("/{}", prefix2);
3011 let prefix3 = format!("/{}", prefix3);
3012
3013 async fn handler() -> &'static str { "handler" }
3014
3015 let mut router1 = Router::new();
3017 let mut router2 = Router::new();
3018 let mut router3 = Router::new();
3019
3020 for i in 0..num_routes {
3021 let path = format!("/item{}", i);
3022 router1 = router1.route(&path, get(handler));
3023 router2 = router2.route(&path, get(handler));
3024 router3 = router3.route(&path, get(handler));
3025 }
3026
3027 let app = Router::new()
3029 .nest(&prefix1, router1)
3030 .nest(&prefix2, router2)
3031 .nest(&prefix3, router3);
3032
3033 let routes = app.registered_routes();
3034
3035 let expected_count = 3 * num_routes;
3037 prop_assert_eq!(
3038 routes.len(),
3039 expected_count,
3040 "Should have {} routes, got {}",
3041 expected_count, routes.len()
3042 );
3043
3044 for i in 0..num_routes {
3046 let path1 = format!("{}/item{}", prefix1, i);
3047 let path2 = format!("{}/item{}", prefix2, i);
3048 let path3 = format!("{}/item{}", prefix3, i);
3049
3050 match app.match_route(&path1, &Method::GET) {
3051 RouteMatch::Found { .. } => {}
3052 _ => prop_assert!(false, "Route '{}' should be found", path1),
3053 }
3054 match app.match_route(&path2, &Method::GET) {
3055 RouteMatch::Found { .. } => {}
3056 _ => prop_assert!(false, "Route '{}' should be found", path2),
3057 }
3058 match app.match_route(&path3, &Method::GET) {
3059 RouteMatch::Found { .. } => {}
3060 _ => prop_assert!(false, "Route '{}' should be found", path3),
3061 }
3062 }
3063 }
3064 }
3065 proptest! {
3066 #![proptest_config(ProptestConfig::with_cases(100))]
3067
3068 #[test]
3074 fn prop_nested_route_conflict_different_param_names(
3075 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3076 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
3077 param1 in "[a-z][a-z0-9]{1,5}",
3078 param2 in "[a-z][a-z0-9]{1,5}",
3079 ) {
3080 prop_assume!(param1 != param2);
3082
3083 async fn handler1() -> &'static str { "handler1" }
3084 async fn handler2() -> &'static str { "handler2" }
3085
3086 let prefix = format!("/{}", prefix_segments.join("/"));
3087
3088 let existing_path = if route_segments.is_empty() {
3090 format!("{}/{{{}}}", prefix, param1)
3091 } else {
3092 format!("{}/{}/{{{}}}", prefix, route_segments.join("/"), param1)
3093 };
3094
3095 let nested_path = if route_segments.is_empty() {
3097 format!("/{{{}}}", param2)
3098 } else {
3099 format!("/{}/{{{}}}", route_segments.join("/"), param2)
3100 };
3101
3102 let result = catch_unwind(AssertUnwindSafe(|| {
3104 let parent = Router::new().route(&existing_path, get(handler1));
3105 let nested = Router::new().route(&nested_path, get(handler2));
3106 let _app = parent.nest(&prefix, nested);
3107 }));
3108
3109 prop_assert!(
3111 result.is_err(),
3112 "Nested route '{}{}' should conflict with existing route '{}' but didn't",
3113 prefix, nested_path, existing_path
3114 );
3115
3116 if let Err(panic_info) = result {
3118 if let Some(msg) = panic_info.downcast_ref::<String>() {
3119 prop_assert!(
3120 msg.contains("ROUTE CONFLICT DETECTED"),
3121 "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
3122 msg
3123 );
3124 prop_assert!(
3125 msg.contains("Existing:") && msg.contains("New:"),
3126 "Error should contain both 'Existing:' and 'New:' labels, got: {}",
3127 msg
3128 );
3129 }
3130 }
3131 }
3132
3133 #[test]
3138 fn prop_nested_route_conflict_exact_same_path(
3139 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3140 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3141 ) {
3142 async fn handler1() -> &'static str { "handler1" }
3143 async fn handler2() -> &'static str { "handler2" }
3144
3145 let prefix = format!("/{}", prefix_segments.join("/"));
3146 let route_path = format!("/{}", route_segments.join("/"));
3147
3148 let existing_path = format!("{}{}", prefix, route_path);
3150
3151 let result = catch_unwind(AssertUnwindSafe(|| {
3153 let parent = Router::new().route(&existing_path, get(handler1));
3154 let nested = Router::new().route(&route_path, get(handler2));
3155 let _app = parent.nest(&prefix, nested);
3156 }));
3157
3158 prop_assert!(
3160 result.is_err(),
3161 "Nested route '{}{}' should conflict with existing route '{}' but didn't",
3162 prefix, route_path, existing_path
3163 );
3164 }
3165
3166 #[test]
3171 fn prop_nested_routes_different_prefixes_no_conflict(
3172 prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3173 prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3174 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3175 has_param in any::<bool>(),
3176 ) {
3177 let prefix1 = format!("/{}", prefix1_segments.join("/"));
3179 let prefix2 = format!("/{}", prefix2_segments.join("/"));
3180
3181 prop_assume!(prefix1 != prefix2);
3183
3184 async fn handler1() -> &'static str { "handler1" }
3185 async fn handler2() -> &'static str { "handler2" }
3186
3187 let route_path = if has_param {
3189 format!("/{}/{{id}}", route_segments.join("/"))
3190 } else {
3191 format!("/{}", route_segments.join("/"))
3192 };
3193
3194 let result = catch_unwind(AssertUnwindSafe(|| {
3196 let nested1 = Router::new().route(&route_path, get(handler1));
3197 let nested2 = Router::new().route(&route_path, get(handler2));
3198
3199 let app = Router::new()
3200 .nest(&prefix1, nested1)
3201 .nest(&prefix2, nested2);
3202
3203 app.registered_routes().len()
3204 }));
3205
3206 prop_assert!(
3208 result.is_ok(),
3209 "Routes under different prefixes '{}' and '{}' should not conflict",
3210 prefix1, prefix2
3211 );
3212
3213 if let Ok(count) = result {
3214 prop_assert_eq!(count, 2, "Should have registered 2 routes");
3215 }
3216 }
3217
3218 #[test]
3223 fn prop_nested_conflict_error_contains_guidance(
3224 prefix in "[a-z][a-z0-9]{1,5}",
3225 segment in "[a-z][a-z0-9]{1,5}",
3226 param1 in "[a-z][a-z0-9]{1,5}",
3227 param2 in "[a-z][a-z0-9]{1,5}",
3228 ) {
3229 prop_assume!(param1 != param2);
3230
3231 async fn handler1() -> &'static str { "handler1" }
3232 async fn handler2() -> &'static str { "handler2" }
3233
3234 let prefix = format!("/{}", prefix);
3235 let existing_path = format!("{}/{}/{{{}}}", prefix, segment, param1);
3236 let nested_path = format!("/{}/{{{}}}", segment, param2);
3237
3238 let result = catch_unwind(AssertUnwindSafe(|| {
3239 let parent = Router::new().route(&existing_path, get(handler1));
3240 let nested = Router::new().route(&nested_path, get(handler2));
3241 let _app = parent.nest(&prefix, nested);
3242 }));
3243
3244 prop_assert!(result.is_err(), "Should have detected conflict");
3245
3246 if let Err(panic_info) = result {
3247 if let Some(msg) = panic_info.downcast_ref::<String>() {
3248 prop_assert!(
3249 msg.contains("How to resolve:"),
3250 "Error should contain 'How to resolve:' guidance, got: {}",
3251 msg
3252 );
3253 prop_assert!(
3254 msg.contains("Use different path patterns") ||
3255 msg.contains("different path patterns"),
3256 "Error should suggest using different path patterns, got: {}",
3257 msg
3258 );
3259 }
3260 }
3261 }
3262 }
3263}