1use crate::handler::{into_boxed_handler, BoxedHandler, Handler};
45use crate::path_params::PathParams;
46use crate::typed_path::TypedPath;
47use http::{Extensions, Method};
48use matchit::Router as MatchitRouter;
49use rustapi_openapi::Operation;
50use std::collections::HashMap;
51use std::sync::Arc;
52
53#[derive(Debug, Clone)]
55pub struct RouteInfo {
56 pub path: String,
58 pub methods: Vec<Method>,
60}
61
62#[derive(Debug, Clone)]
64pub struct RouteConflictError {
65 pub new_path: String,
67 pub method: Option<Method>,
69 pub existing_path: String,
71 pub details: String,
73}
74
75impl std::fmt::Display for RouteConflictError {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 writeln!(
78 f,
79 "\n╭─────────────────────────────────────────────────────────────╮"
80 )?;
81 writeln!(
82 f,
83 "│ ROUTE CONFLICT DETECTED │"
84 )?;
85 writeln!(
86 f,
87 "╰─────────────────────────────────────────────────────────────╯"
88 )?;
89 writeln!(f)?;
90 writeln!(f, " Conflicting routes:")?;
91 writeln!(f, " → Existing: {}", self.existing_path)?;
92 writeln!(f, " → New: {}", self.new_path)?;
93 writeln!(f)?;
94 if let Some(ref method) = self.method {
95 writeln!(f, " HTTP Method: {}", method)?;
96 writeln!(f)?;
97 }
98 writeln!(f, " Details: {}", self.details)?;
99 writeln!(f)?;
100 writeln!(f, " How to resolve:")?;
101 writeln!(f, " 1. Use different path patterns for each route")?;
102 writeln!(
103 f,
104 " 2. If paths must be similar, ensure parameter names differ"
105 )?;
106 writeln!(
107 f,
108 " 3. Consider using different HTTP methods if appropriate"
109 )?;
110 writeln!(f)?;
111 writeln!(f, " Example:")?;
112 writeln!(f, " Instead of:")?;
113 writeln!(f, " .route(\"/users/{{id}}\", get(handler1))")?;
114 writeln!(f, " .route(\"/users/{{user_id}}\", get(handler2))")?;
115 writeln!(f)?;
116 writeln!(f, " Use:")?;
117 writeln!(f, " .route(\"/users/{{id}}\", get(handler1))")?;
118 writeln!(f, " .route(\"/users/{{id}}/profile\", get(handler2))")?;
119 Ok(())
120 }
121}
122
123impl std::error::Error for RouteConflictError {}
124
125pub struct MethodRouter {
127 handlers: HashMap<Method, BoxedHandler>,
128 pub(crate) operations: HashMap<Method, Operation>,
129}
130
131impl Clone for MethodRouter {
132 fn clone(&self) -> Self {
133 Self {
134 handlers: self.handlers.clone(),
135 operations: self.operations.clone(),
136 }
137 }
138}
139
140impl MethodRouter {
141 pub fn new() -> Self {
143 Self {
144 handlers: HashMap::new(),
145 operations: HashMap::new(),
146 }
147 }
148
149 fn on(mut self, method: Method, handler: BoxedHandler, operation: Operation) -> Self {
151 self.handlers.insert(method.clone(), handler);
152 self.operations.insert(method, operation);
153 self
154 }
155
156 pub(crate) fn get_handler(&self, method: &Method) -> Option<&BoxedHandler> {
158 self.handlers.get(method)
159 }
160
161 pub(crate) fn allowed_methods(&self) -> Vec<Method> {
163 self.handlers.keys().cloned().collect()
164 }
165
166 pub(crate) fn from_boxed(handlers: HashMap<Method, BoxedHandler>) -> Self {
168 Self {
169 handlers,
170 operations: HashMap::new(), }
172 }
173
174 pub(crate) fn insert_boxed_with_operation(
178 &mut self,
179 method: Method,
180 handler: BoxedHandler,
181 operation: Operation,
182 ) {
183 if self.handlers.contains_key(&method) {
184 panic!(
185 "Duplicate handler for method {} on the same path",
186 method.as_str()
187 );
188 }
189
190 self.handlers.insert(method.clone(), handler);
191 self.operations.insert(method, operation);
192 }
193 pub fn get<H, T>(self, handler: H) -> Self
195 where
196 H: Handler<T>,
197 T: 'static,
198 {
199 let mut op = Operation::new();
200 H::update_operation(&mut op);
201 self.on(Method::GET, into_boxed_handler(handler), op)
202 }
203
204 pub fn post<H, T>(self, handler: H) -> Self
206 where
207 H: Handler<T>,
208 T: 'static,
209 {
210 let mut op = Operation::new();
211 H::update_operation(&mut op);
212 self.on(Method::POST, into_boxed_handler(handler), op)
213 }
214
215 pub fn put<H, T>(self, handler: H) -> Self
217 where
218 H: Handler<T>,
219 T: 'static,
220 {
221 let mut op = Operation::new();
222 H::update_operation(&mut op);
223 self.on(Method::PUT, into_boxed_handler(handler), op)
224 }
225
226 pub fn patch<H, T>(self, handler: H) -> Self
228 where
229 H: Handler<T>,
230 T: 'static,
231 {
232 let mut op = Operation::new();
233 H::update_operation(&mut op);
234 self.on(Method::PATCH, into_boxed_handler(handler), op)
235 }
236
237 pub fn delete<H, T>(self, handler: H) -> Self
239 where
240 H: Handler<T>,
241 T: 'static,
242 {
243 let mut op = Operation::new();
244 H::update_operation(&mut op);
245 self.on(Method::DELETE, into_boxed_handler(handler), op)
246 }
247}
248
249impl Default for MethodRouter {
250 fn default() -> Self {
251 Self::new()
252 }
253}
254
255pub fn get<H, T>(handler: H) -> MethodRouter
257where
258 H: Handler<T>,
259 T: 'static,
260{
261 let mut op = Operation::new();
262 H::update_operation(&mut op);
263 MethodRouter::new().on(Method::GET, into_boxed_handler(handler), op)
264}
265
266pub fn post<H, T>(handler: H) -> MethodRouter
268where
269 H: Handler<T>,
270 T: 'static,
271{
272 let mut op = Operation::new();
273 H::update_operation(&mut op);
274 MethodRouter::new().on(Method::POST, into_boxed_handler(handler), op)
275}
276
277pub fn put<H, T>(handler: H) -> MethodRouter
279where
280 H: Handler<T>,
281 T: 'static,
282{
283 let mut op = Operation::new();
284 H::update_operation(&mut op);
285 MethodRouter::new().on(Method::PUT, into_boxed_handler(handler), op)
286}
287
288pub fn patch<H, T>(handler: H) -> MethodRouter
290where
291 H: Handler<T>,
292 T: 'static,
293{
294 let mut op = Operation::new();
295 H::update_operation(&mut op);
296 MethodRouter::new().on(Method::PATCH, into_boxed_handler(handler), op)
297}
298
299pub fn delete<H, T>(handler: H) -> MethodRouter
301where
302 H: Handler<T>,
303 T: 'static,
304{
305 let mut op = Operation::new();
306 H::update_operation(&mut op);
307 MethodRouter::new().on(Method::DELETE, into_boxed_handler(handler), op)
308}
309
310pub struct Router {
312 inner: MatchitRouter<MethodRouter>,
313 state: Arc<Extensions>,
314 registered_routes: HashMap<String, RouteInfo>,
316 method_routers: HashMap<String, MethodRouter>,
318 state_type_ids: Vec<std::any::TypeId>,
321}
322
323impl Router {
324 pub fn new() -> Self {
326 Self {
327 inner: MatchitRouter::new(),
328 state: Arc::new(Extensions::new()),
329 registered_routes: HashMap::new(),
330 method_routers: HashMap::new(),
331 state_type_ids: Vec::new(),
332 }
333 }
334
335 pub fn typed<P: TypedPath>(self, method_router: MethodRouter) -> Self {
337 self.route(P::PATH, method_router)
338 }
339
340 pub fn route(mut self, path: &str, method_router: MethodRouter) -> Self {
342 let matchit_path = convert_path_params(path);
344
345 let methods: Vec<Method> = method_router.handlers.keys().cloned().collect();
347
348 self.method_routers
350 .insert(matchit_path.clone(), method_router.clone());
351
352 match self.inner.insert(matchit_path.clone(), method_router) {
353 Ok(_) => {
354 self.registered_routes.insert(
356 matchit_path.clone(),
357 RouteInfo {
358 path: path.to_string(),
359 methods,
360 },
361 );
362 }
363 Err(e) => {
364 self.method_routers.remove(&matchit_path);
366
367 let existing_path = self
369 .find_conflicting_route(&matchit_path)
370 .map(|info| info.path.clone())
371 .unwrap_or_else(|| "<unknown>".to_string());
372
373 let conflict_error = RouteConflictError {
374 new_path: path.to_string(),
375 method: methods.first().cloned(),
376 existing_path,
377 details: e.to_string(),
378 };
379
380 panic!("{}", conflict_error);
381 }
382 }
383 self
384 }
385
386 fn find_conflicting_route(&self, matchit_path: &str) -> Option<&RouteInfo> {
388 if let Some(info) = self.registered_routes.get(matchit_path) {
390 return Some(info);
391 }
392
393 let normalized_new = normalize_path_for_comparison(matchit_path);
395
396 for (registered_path, info) in &self.registered_routes {
397 let normalized_existing = normalize_path_for_comparison(registered_path);
398 if normalized_new == normalized_existing {
399 return Some(info);
400 }
401 }
402
403 None
404 }
405
406 pub fn state<S: Clone + Send + Sync + 'static>(mut self, state: S) -> Self {
408 let type_id = std::any::TypeId::of::<S>();
409 let extensions = Arc::make_mut(&mut self.state);
410 extensions.insert(state);
411 if !self.state_type_ids.contains(&type_id) {
412 self.state_type_ids.push(type_id);
413 }
414 self
415 }
416
417 pub fn has_state<S: 'static>(&self) -> bool {
419 self.state_type_ids.contains(&std::any::TypeId::of::<S>())
420 }
421
422 pub fn state_type_ids(&self) -> &[std::any::TypeId] {
424 &self.state_type_ids
425 }
426
427 pub fn nest(mut self, prefix: &str, router: Router) -> Self {
495 let normalized_prefix = normalize_prefix(prefix);
497
498 for type_id in &router.state_type_ids {
502 if !self.state_type_ids.contains(type_id) {
503 self.state_type_ids.push(*type_id);
504 }
505 }
506
507 let nested_routes: Vec<(String, RouteInfo, MethodRouter)> = router
510 .registered_routes
511 .into_iter()
512 .filter_map(|(matchit_path, route_info)| {
513 router
514 .method_routers
515 .get(&matchit_path)
516 .map(|mr| (matchit_path, route_info, mr.clone()))
517 })
518 .collect();
519
520 for (matchit_path, route_info, method_router) in nested_routes {
522 let prefixed_matchit_path = if matchit_path == "/" {
526 normalized_prefix.clone()
527 } else {
528 format!("{}{}", normalized_prefix, matchit_path)
529 };
530
531 let prefixed_display_path = if route_info.path == "/" {
532 normalized_prefix.clone()
533 } else {
534 format!("{}{}", normalized_prefix, route_info.path)
535 };
536
537 self.method_routers
539 .insert(prefixed_matchit_path.clone(), method_router.clone());
540
541 match self
543 .inner
544 .insert(prefixed_matchit_path.clone(), method_router)
545 {
546 Ok(_) => {
547 self.registered_routes.insert(
549 prefixed_matchit_path,
550 RouteInfo {
551 path: prefixed_display_path,
552 methods: route_info.methods,
553 },
554 );
555 }
556 Err(e) => {
557 self.method_routers.remove(&prefixed_matchit_path);
559
560 let existing_path = self
562 .find_conflicting_route(&prefixed_matchit_path)
563 .map(|info| info.path.clone())
564 .unwrap_or_else(|| "<unknown>".to_string());
565
566 let conflict_error = RouteConflictError {
567 new_path: prefixed_display_path,
568 method: route_info.methods.first().cloned(),
569 existing_path,
570 details: e.to_string(),
571 };
572
573 panic!("{}", conflict_error);
574 }
575 }
576 }
577
578 self
579 }
580
581 pub fn merge_state<S: Clone + Send + Sync + 'static>(mut self, other: &Router) -> Self {
598 let type_id = std::any::TypeId::of::<S>();
599
600 if !self.state_type_ids.contains(&type_id) {
602 if let Some(state) = other.state.get::<S>() {
604 let extensions = Arc::make_mut(&mut self.state);
605 extensions.insert(state.clone());
606 self.state_type_ids.push(type_id);
607 }
608 }
609
610 self
611 }
612
613 pub fn match_route(&self, path: &str, method: &Method) -> RouteMatch<'_> {
615 match self.inner.at(path) {
616 Ok(matched) => {
617 let method_router = matched.value;
618
619 if let Some(handler) = method_router.get_handler(method) {
620 let params: PathParams = matched
622 .params
623 .iter()
624 .map(|(k, v)| (k.to_string(), v.to_string()))
625 .collect();
626
627 RouteMatch::Found { handler, params }
628 } else {
629 RouteMatch::MethodNotAllowed {
630 allowed: method_router.allowed_methods(),
631 }
632 }
633 }
634 Err(_) => RouteMatch::NotFound,
635 }
636 }
637
638 pub fn state_ref(&self) -> Arc<Extensions> {
640 self.state.clone()
641 }
642
643 pub fn registered_routes(&self) -> &HashMap<String, RouteInfo> {
645 &self.registered_routes
646 }
647
648 pub fn method_routers(&self) -> &HashMap<String, MethodRouter> {
650 &self.method_routers
651 }
652}
653
654impl Default for Router {
655 fn default() -> Self {
656 Self::new()
657 }
658}
659
660pub enum RouteMatch<'a> {
662 Found {
663 handler: &'a BoxedHandler,
664 params: PathParams,
665 },
666 NotFound,
667 MethodNotAllowed {
668 allowed: Vec<Method>,
669 },
670}
671
672fn convert_path_params(path: &str) -> String {
674 let mut result = String::with_capacity(path.len());
675
676 for ch in path.chars() {
677 match ch {
678 '{' => {
679 result.push(':');
680 }
681 '}' => {
682 }
684 _ => {
685 result.push(ch);
686 }
687 }
688 }
689
690 result
691}
692
693fn normalize_path_for_comparison(path: &str) -> String {
695 let mut result = String::with_capacity(path.len());
696 let mut in_param = false;
697
698 for ch in path.chars() {
699 match ch {
700 ':' => {
701 in_param = true;
702 result.push_str(":_");
703 }
704 '/' => {
705 in_param = false;
706 result.push('/');
707 }
708 _ if in_param => {
709 }
711 _ => {
712 result.push(ch);
713 }
714 }
715 }
716
717 result
718}
719
720pub(crate) fn normalize_prefix(prefix: &str) -> String {
737 if prefix.is_empty() {
739 return "/".to_string();
740 }
741
742 let segments: Vec<&str> = prefix.split('/').filter(|s| !s.is_empty()).collect();
744
745 if segments.is_empty() {
747 return "/".to_string();
748 }
749
750 let mut result = String::with_capacity(prefix.len() + 1);
752 for segment in segments {
753 result.push('/');
754 result.push_str(segment);
755 }
756
757 result
758}
759
760#[cfg(test)]
761mod tests {
762 use super::*;
763
764 #[test]
765 fn test_convert_path_params() {
766 assert_eq!(convert_path_params("/users/{id}"), "/users/:id");
767 assert_eq!(
768 convert_path_params("/users/{user_id}/posts/{post_id}"),
769 "/users/:user_id/posts/:post_id"
770 );
771 assert_eq!(convert_path_params("/static/path"), "/static/path");
772 }
773
774 #[test]
775 fn test_normalize_path_for_comparison() {
776 assert_eq!(normalize_path_for_comparison("/users/:id"), "/users/:_");
777 assert_eq!(
778 normalize_path_for_comparison("/users/:user_id"),
779 "/users/:_"
780 );
781 assert_eq!(
782 normalize_path_for_comparison("/users/:id/posts/:post_id"),
783 "/users/:_/posts/:_"
784 );
785 assert_eq!(
786 normalize_path_for_comparison("/static/path"),
787 "/static/path"
788 );
789 }
790
791 #[test]
792 fn test_normalize_prefix() {
793 assert_eq!(normalize_prefix("api"), "/api");
795 assert_eq!(normalize_prefix("/api"), "/api");
796 assert_eq!(normalize_prefix("/api/"), "/api");
797 assert_eq!(normalize_prefix("api/"), "/api");
798
799 assert_eq!(normalize_prefix("api/v1"), "/api/v1");
801 assert_eq!(normalize_prefix("/api/v1"), "/api/v1");
802 assert_eq!(normalize_prefix("/api/v1/"), "/api/v1");
803
804 assert_eq!(normalize_prefix(""), "/");
806 assert_eq!(normalize_prefix("/"), "/");
807
808 assert_eq!(normalize_prefix("//api"), "/api");
810 assert_eq!(normalize_prefix("api//v1"), "/api/v1");
811 assert_eq!(normalize_prefix("//api//v1//"), "/api/v1");
812 assert_eq!(normalize_prefix("///"), "/");
813 }
814
815 #[test]
816 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
817 fn test_route_conflict_detection() {
818 async fn handler1() -> &'static str {
819 "handler1"
820 }
821 async fn handler2() -> &'static str {
822 "handler2"
823 }
824
825 let _router = Router::new()
826 .route("/users/{id}", get(handler1))
827 .route("/users/{user_id}", get(handler2)); }
829
830 #[test]
831 fn test_no_conflict_different_paths() {
832 async fn handler1() -> &'static str {
833 "handler1"
834 }
835 async fn handler2() -> &'static str {
836 "handler2"
837 }
838
839 let router = Router::new()
840 .route("/users/{id}", get(handler1))
841 .route("/users/{id}/profile", get(handler2));
842
843 assert_eq!(router.registered_routes().len(), 2);
844 }
845
846 #[test]
847 fn test_route_info_tracking() {
848 async fn handler() -> &'static str {
849 "handler"
850 }
851
852 let router = Router::new().route("/users/{id}", get(handler));
853
854 let routes = router.registered_routes();
855 assert_eq!(routes.len(), 1);
856
857 let info = routes.get("/users/:id").unwrap();
858 assert_eq!(info.path, "/users/{id}");
859 assert_eq!(info.methods.len(), 1);
860 assert_eq!(info.methods[0], Method::GET);
861 }
862
863 #[test]
864 fn test_basic_router_nesting() {
865 async fn list_users() -> &'static str {
866 "list users"
867 }
868 async fn get_user() -> &'static str {
869 "get user"
870 }
871
872 let users_router = Router::new()
873 .route("/", get(list_users))
874 .route("/{id}", get(get_user));
875
876 let app = Router::new().nest("/api/users", users_router);
877
878 let routes = app.registered_routes();
879 assert_eq!(routes.len(), 2);
880
881 assert!(routes.contains_key("/api/users"));
883 assert!(routes.contains_key("/api/users/:id"));
884
885 let list_info = routes.get("/api/users").unwrap();
887 assert_eq!(list_info.path, "/api/users");
888
889 let get_info = routes.get("/api/users/:id").unwrap();
890 assert_eq!(get_info.path, "/api/users/{id}");
891 }
892
893 #[test]
894 fn test_nested_route_matching() {
895 async fn handler() -> &'static str {
896 "handler"
897 }
898
899 let users_router = Router::new().route("/{id}", get(handler));
900
901 let app = Router::new().nest("/api/users", users_router);
902
903 match app.match_route("/api/users/123", &Method::GET) {
905 RouteMatch::Found { params, .. } => {
906 assert_eq!(params.get("id"), Some(&"123".to_string()));
907 }
908 _ => panic!("Route should be found"),
909 }
910 }
911
912 #[test]
913 fn test_nested_route_matching_multiple_params() {
914 async fn handler() -> &'static str {
915 "handler"
916 }
917
918 let posts_router = Router::new().route("/{user_id}/posts/{post_id}", get(handler));
919
920 let app = Router::new().nest("/api", posts_router);
921
922 match app.match_route("/api/42/posts/100", &Method::GET) {
924 RouteMatch::Found { params, .. } => {
925 assert_eq!(params.get("user_id"), Some(&"42".to_string()));
926 assert_eq!(params.get("post_id"), Some(&"100".to_string()));
927 }
928 _ => panic!("Route should be found"),
929 }
930 }
931
932 #[test]
933 fn test_nested_route_matching_static_path() {
934 async fn handler() -> &'static str {
935 "handler"
936 }
937
938 let health_router = Router::new().route("/health", get(handler));
939
940 let app = Router::new().nest("/api/v1", health_router);
941
942 match app.match_route("/api/v1/health", &Method::GET) {
944 RouteMatch::Found { params, .. } => {
945 assert!(params.is_empty(), "Static path should have no params");
946 }
947 _ => panic!("Route should be found"),
948 }
949 }
950
951 #[test]
952 fn test_nested_route_not_found() {
953 async fn handler() -> &'static str {
954 "handler"
955 }
956
957 let users_router = Router::new().route("/users", get(handler));
958
959 let app = Router::new().nest("/api", users_router);
960
961 match app.match_route("/api/posts", &Method::GET) {
963 RouteMatch::NotFound => {
964 }
966 _ => panic!("Route should not be found"),
967 }
968
969 match app.match_route("/v2/users", &Method::GET) {
971 RouteMatch::NotFound => {
972 }
974 _ => panic!("Route with wrong prefix should not be found"),
975 }
976 }
977
978 #[test]
979 fn test_nested_route_method_not_allowed() {
980 async fn handler() -> &'static str {
981 "handler"
982 }
983
984 let users_router = Router::new().route("/users", get(handler));
985
986 let app = Router::new().nest("/api", users_router);
987
988 match app.match_route("/api/users", &Method::POST) {
990 RouteMatch::MethodNotAllowed { allowed } => {
991 assert!(allowed.contains(&Method::GET));
992 assert!(!allowed.contains(&Method::POST));
993 }
994 _ => panic!("Should return MethodNotAllowed"),
995 }
996 }
997
998 #[test]
999 fn test_nested_route_multiple_methods() {
1000 async fn get_handler() -> &'static str {
1001 "get"
1002 }
1003 async fn post_handler() -> &'static str {
1004 "post"
1005 }
1006
1007 let get_router = get(get_handler);
1009 let post_router = post(post_handler);
1010 let mut combined = MethodRouter::new();
1011 for (method, handler) in get_router.handlers {
1012 combined.handlers.insert(method, handler);
1013 }
1014 for (method, handler) in post_router.handlers {
1015 combined.handlers.insert(method, handler);
1016 }
1017
1018 let users_router = Router::new().route("/users", combined);
1019 let app = Router::new().nest("/api", users_router);
1020
1021 match app.match_route("/api/users", &Method::GET) {
1023 RouteMatch::Found { .. } => {}
1024 _ => panic!("GET should be found"),
1025 }
1026
1027 match app.match_route("/api/users", &Method::POST) {
1028 RouteMatch::Found { .. } => {}
1029 _ => panic!("POST should be found"),
1030 }
1031
1032 match app.match_route("/api/users", &Method::DELETE) {
1034 RouteMatch::MethodNotAllowed { allowed } => {
1035 assert!(allowed.contains(&Method::GET));
1036 assert!(allowed.contains(&Method::POST));
1037 }
1038 _ => panic!("DELETE should return MethodNotAllowed"),
1039 }
1040 }
1041
1042 #[test]
1043 fn test_nested_router_prefix_normalization() {
1044 async fn handler() -> &'static str {
1045 "handler"
1046 }
1047
1048 let router1 = Router::new().route("/test", get(handler));
1050 let app1 = Router::new().nest("api", router1);
1051 assert!(app1.registered_routes().contains_key("/api/test"));
1052
1053 let router2 = Router::new().route("/test", get(handler));
1054 let app2 = Router::new().nest("/api/", router2);
1055 assert!(app2.registered_routes().contains_key("/api/test"));
1056
1057 let router3 = Router::new().route("/test", get(handler));
1058 let app3 = Router::new().nest("//api//", router3);
1059 assert!(app3.registered_routes().contains_key("/api/test"));
1060 }
1061
1062 #[test]
1063 fn test_state_tracking() {
1064 #[derive(Clone)]
1065 struct MyState(#[allow(dead_code)] String);
1066
1067 let router = Router::new().state(MyState("test".to_string()));
1068
1069 assert!(router.has_state::<MyState>());
1070 assert!(!router.has_state::<String>());
1071 }
1072
1073 #[test]
1074 fn test_state_merge_nested_only() {
1075 #[derive(Clone, PartialEq, Debug)]
1076 struct NestedState(String);
1077
1078 async fn handler() -> &'static str {
1079 "handler"
1080 }
1081
1082 let state_source = Router::new().state(NestedState("nested".to_string()));
1084
1085 let nested = Router::new().route("/test", get(handler));
1086
1087 let parent = Router::new()
1088 .nest("/api", nested)
1089 .merge_state::<NestedState>(&state_source);
1090
1091 assert!(parent.has_state::<NestedState>());
1093
1094 let state = parent.state.get::<NestedState>().unwrap();
1096 assert_eq!(state.0, "nested");
1097 }
1098
1099 #[test]
1100 fn test_state_merge_parent_wins() {
1101 #[derive(Clone, PartialEq, Debug)]
1102 struct SharedState(String);
1103
1104 async fn handler() -> &'static str {
1105 "handler"
1106 }
1107
1108 let state_source = Router::new().state(SharedState("nested".to_string()));
1110
1111 let nested = Router::new().route("/test", get(handler));
1112
1113 let parent = Router::new()
1114 .state(SharedState("parent".to_string()))
1115 .nest("/api", nested)
1116 .merge_state::<SharedState>(&state_source);
1117
1118 assert!(parent.has_state::<SharedState>());
1120
1121 let state = parent.state.get::<SharedState>().unwrap();
1123 assert_eq!(state.0, "parent");
1124 }
1125
1126 #[test]
1127 fn test_state_type_ids_merged_on_nest() {
1128 #[derive(Clone)]
1129 struct NestedState(#[allow(dead_code)] String);
1130
1131 async fn handler() -> &'static str {
1132 "handler"
1133 }
1134
1135 let nested = Router::new()
1136 .route("/test", get(handler))
1137 .state(NestedState("nested".to_string()));
1138
1139 let parent = Router::new().nest("/api", nested);
1140
1141 assert!(parent
1143 .state_type_ids()
1144 .contains(&std::any::TypeId::of::<NestedState>()));
1145 }
1146
1147 #[test]
1148 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
1149 fn test_nested_route_conflict_with_existing_route() {
1150 async fn handler1() -> &'static str {
1151 "handler1"
1152 }
1153 async fn handler2() -> &'static str {
1154 "handler2"
1155 }
1156
1157 let parent = Router::new().route("/api/users/{id}", get(handler1));
1159
1160 let nested = Router::new().route("/{user_id}", get(handler2));
1162
1163 let _app = parent.nest("/api/users", nested);
1165 }
1166
1167 #[test]
1168 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
1169 fn test_nested_route_conflict_same_path_different_param_names() {
1170 async fn handler1() -> &'static str {
1171 "handler1"
1172 }
1173 async fn handler2() -> &'static str {
1174 "handler2"
1175 }
1176
1177 let nested1 = Router::new().route("/{id}", get(handler1));
1179 let nested2 = Router::new().route("/{user_id}", get(handler2));
1180
1181 let _app = Router::new()
1183 .nest("/api/users", nested1)
1184 .nest("/api/users", nested2);
1185 }
1186
1187 #[test]
1188 fn test_nested_route_conflict_error_contains_both_paths() {
1189 use std::panic::{catch_unwind, AssertUnwindSafe};
1190
1191 async fn handler1() -> &'static str {
1192 "handler1"
1193 }
1194 async fn handler2() -> &'static str {
1195 "handler2"
1196 }
1197
1198 let result = catch_unwind(AssertUnwindSafe(|| {
1199 let parent = Router::new().route("/api/users/{id}", get(handler1));
1200 let nested = Router::new().route("/{user_id}", get(handler2));
1201 let _app = parent.nest("/api/users", nested);
1202 }));
1203
1204 assert!(result.is_err(), "Should have panicked due to conflict");
1205
1206 if let Err(panic_info) = result {
1207 if let Some(msg) = panic_info.downcast_ref::<String>() {
1208 assert!(
1209 msg.contains("ROUTE CONFLICT DETECTED"),
1210 "Error should contain 'ROUTE CONFLICT DETECTED'"
1211 );
1212 assert!(
1213 msg.contains("Existing:") && msg.contains("New:"),
1214 "Error should contain both 'Existing:' and 'New:' labels"
1215 );
1216 assert!(
1217 msg.contains("How to resolve:"),
1218 "Error should contain resolution guidance"
1219 );
1220 }
1221 }
1222 }
1223
1224 #[test]
1225 fn test_nested_routes_no_conflict_different_prefixes() {
1226 async fn handler1() -> &'static str {
1227 "handler1"
1228 }
1229 async fn handler2() -> &'static str {
1230 "handler2"
1231 }
1232
1233 let nested1 = Router::new().route("/{id}", get(handler1));
1235 let nested2 = Router::new().route("/{id}", get(handler2));
1236
1237 let app = Router::new()
1239 .nest("/api/users", nested1)
1240 .nest("/api/posts", nested2);
1241
1242 assert_eq!(app.registered_routes().len(), 2);
1243 assert!(app.registered_routes().contains_key("/api/users/:id"));
1244 assert!(app.registered_routes().contains_key("/api/posts/:id"));
1245 }
1246
1247 #[test]
1252 fn test_multiple_router_composition_all_routes_registered() {
1253 async fn users_list() -> &'static str {
1254 "users list"
1255 }
1256 async fn users_get() -> &'static str {
1257 "users get"
1258 }
1259 async fn posts_list() -> &'static str {
1260 "posts list"
1261 }
1262 async fn posts_get() -> &'static str {
1263 "posts get"
1264 }
1265 async fn comments_list() -> &'static str {
1266 "comments list"
1267 }
1268
1269 let users_router = Router::new()
1271 .route("/", get(users_list))
1272 .route("/{id}", get(users_get));
1273
1274 let posts_router = Router::new()
1275 .route("/", get(posts_list))
1276 .route("/{id}", get(posts_get));
1277
1278 let comments_router = Router::new().route("/", get(comments_list));
1279
1280 let app = Router::new()
1282 .nest("/api/users", users_router)
1283 .nest("/api/posts", posts_router)
1284 .nest("/api/comments", comments_router);
1285
1286 let routes = app.registered_routes();
1288 assert_eq!(routes.len(), 5, "Should have 5 routes registered");
1289
1290 assert!(
1292 routes.contains_key("/api/users"),
1293 "Should have /api/users route"
1294 );
1295 assert!(
1296 routes.contains_key("/api/users/:id"),
1297 "Should have /api/users/:id route"
1298 );
1299
1300 assert!(
1302 routes.contains_key("/api/posts"),
1303 "Should have /api/posts route"
1304 );
1305 assert!(
1306 routes.contains_key("/api/posts/:id"),
1307 "Should have /api/posts/:id route"
1308 );
1309
1310 assert!(
1312 routes.contains_key("/api/comments"),
1313 "Should have /api/comments route"
1314 );
1315 }
1316
1317 #[test]
1318 fn test_multiple_router_composition_no_interference() {
1319 async fn users_handler() -> &'static str {
1320 "users"
1321 }
1322 async fn posts_handler() -> &'static str {
1323 "posts"
1324 }
1325 async fn admin_handler() -> &'static str {
1326 "admin"
1327 }
1328
1329 let users_router = Router::new()
1331 .route("/list", get(users_handler))
1332 .route("/{id}", get(users_handler));
1333
1334 let posts_router = Router::new()
1335 .route("/list", get(posts_handler))
1336 .route("/{id}", get(posts_handler));
1337
1338 let admin_router = Router::new()
1339 .route("/list", get(admin_handler))
1340 .route("/{id}", get(admin_handler));
1341
1342 let app = Router::new()
1344 .nest("/api/v1/users", users_router)
1345 .nest("/api/v1/posts", posts_router)
1346 .nest("/admin", admin_router);
1347
1348 let routes = app.registered_routes();
1350 assert_eq!(routes.len(), 6, "Should have 6 routes registered");
1351
1352 assert!(routes.contains_key("/api/v1/users/list"));
1354 assert!(routes.contains_key("/api/v1/users/:id"));
1355 assert!(routes.contains_key("/api/v1/posts/list"));
1356 assert!(routes.contains_key("/api/v1/posts/:id"));
1357 assert!(routes.contains_key("/admin/list"));
1358 assert!(routes.contains_key("/admin/:id"));
1359
1360 match app.match_route("/api/v1/users/list", &Method::GET) {
1362 RouteMatch::Found { params, .. } => {
1363 assert!(params.is_empty(), "Static path should have no params");
1364 }
1365 _ => panic!("Should find /api/v1/users/list"),
1366 }
1367
1368 match app.match_route("/api/v1/posts/123", &Method::GET) {
1369 RouteMatch::Found { params, .. } => {
1370 assert_eq!(params.get("id"), Some(&"123".to_string()));
1371 }
1372 _ => panic!("Should find /api/v1/posts/123"),
1373 }
1374
1375 match app.match_route("/admin/456", &Method::GET) {
1376 RouteMatch::Found { params, .. } => {
1377 assert_eq!(params.get("id"), Some(&"456".to_string()));
1378 }
1379 _ => panic!("Should find /admin/456"),
1380 }
1381 }
1382
1383 #[test]
1384 fn test_multiple_router_composition_with_multiple_methods() {
1385 async fn get_handler() -> &'static str {
1386 "get"
1387 }
1388 async fn post_handler() -> &'static str {
1389 "post"
1390 }
1391 async fn put_handler() -> &'static str {
1392 "put"
1393 }
1394
1395 let get_router = get(get_handler);
1398 let post_router = post(post_handler);
1399 let mut users_root_combined = MethodRouter::new();
1400 for (method, handler) in get_router.handlers {
1401 users_root_combined.handlers.insert(method, handler);
1402 }
1403 for (method, handler) in post_router.handlers {
1404 users_root_combined.handlers.insert(method, handler);
1405 }
1406
1407 let get_router2 = get(get_handler);
1409 let put_router = put(put_handler);
1410 let mut users_id_combined = MethodRouter::new();
1411 for (method, handler) in get_router2.handlers {
1412 users_id_combined.handlers.insert(method, handler);
1413 }
1414 for (method, handler) in put_router.handlers {
1415 users_id_combined.handlers.insert(method, handler);
1416 }
1417
1418 let users_router = Router::new()
1419 .route("/", users_root_combined)
1420 .route("/{id}", users_id_combined);
1421
1422 let get_router3 = get(get_handler);
1424 let post_router2 = post(post_handler);
1425 let mut posts_root_combined = MethodRouter::new();
1426 for (method, handler) in get_router3.handlers {
1427 posts_root_combined.handlers.insert(method, handler);
1428 }
1429 for (method, handler) in post_router2.handlers {
1430 posts_root_combined.handlers.insert(method, handler);
1431 }
1432
1433 let posts_router = Router::new().route("/", posts_root_combined);
1434
1435 let app = Router::new()
1437 .nest("/users", users_router)
1438 .nest("/posts", posts_router);
1439
1440 let routes = app.registered_routes();
1442 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1443
1444 let users_root = routes.get("/users").unwrap();
1446 assert!(users_root.methods.contains(&Method::GET));
1447 assert!(users_root.methods.contains(&Method::POST));
1448
1449 let users_id = routes.get("/users/:id").unwrap();
1450 assert!(users_id.methods.contains(&Method::GET));
1451 assert!(users_id.methods.contains(&Method::PUT));
1452
1453 let posts_root = routes.get("/posts").unwrap();
1455 assert!(posts_root.methods.contains(&Method::GET));
1456 assert!(posts_root.methods.contains(&Method::POST));
1457
1458 match app.match_route("/users", &Method::GET) {
1460 RouteMatch::Found { .. } => {}
1461 _ => panic!("GET /users should be found"),
1462 }
1463 match app.match_route("/users", &Method::POST) {
1464 RouteMatch::Found { .. } => {}
1465 _ => panic!("POST /users should be found"),
1466 }
1467 match app.match_route("/users/123", &Method::PUT) {
1468 RouteMatch::Found { .. } => {}
1469 _ => panic!("PUT /users/123 should be found"),
1470 }
1471 }
1472
1473 #[test]
1474 fn test_multiple_router_composition_deep_nesting() {
1475 async fn handler() -> &'static str {
1476 "handler"
1477 }
1478
1479 let deep_router = Router::new().route("/action", get(handler));
1481
1482 let mid_router = Router::new().route("/info", get(handler));
1483
1484 let shallow_router = Router::new().route("/status", get(handler));
1485
1486 let app = Router::new()
1488 .nest("/api/v1/resources/items", deep_router)
1489 .nest("/api/v1/resources", mid_router)
1490 .nest("/api", shallow_router);
1491
1492 let routes = app.registered_routes();
1494 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1495
1496 assert!(routes.contains_key("/api/v1/resources/items/action"));
1497 assert!(routes.contains_key("/api/v1/resources/info"));
1498 assert!(routes.contains_key("/api/status"));
1499
1500 match app.match_route("/api/v1/resources/items/action", &Method::GET) {
1502 RouteMatch::Found { .. } => {}
1503 _ => panic!("Should find deep route"),
1504 }
1505 match app.match_route("/api/v1/resources/info", &Method::GET) {
1506 RouteMatch::Found { .. } => {}
1507 _ => panic!("Should find mid route"),
1508 }
1509 match app.match_route("/api/status", &Method::GET) {
1510 RouteMatch::Found { .. } => {}
1511 _ => panic!("Should find shallow route"),
1512 }
1513 }
1514}
1515
1516#[cfg(test)]
1517mod property_tests {
1518 use super::*;
1519 use proptest::prelude::*;
1520 use std::panic::{catch_unwind, AssertUnwindSafe};
1521
1522 proptest! {
1530 #![proptest_config(ProptestConfig::with_cases(100))]
1531
1532 #[test]
1537 fn prop_normalized_prefix_starts_with_single_slash(
1538 leading_slashes in prop::collection::vec(Just('/'), 0..5),
1540 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 0..4),
1541 trailing_slashes in prop::collection::vec(Just('/'), 0..5),
1542 ) {
1543 let mut prefix = String::new();
1545 for _ in &leading_slashes {
1546 prefix.push('/');
1547 }
1548 for (i, segment) in segments.iter().enumerate() {
1549 if i > 0 {
1550 prefix.push('/');
1551 }
1552 prefix.push_str(segment);
1553 }
1554 for _ in &trailing_slashes {
1555 prefix.push('/');
1556 }
1557
1558 let normalized = normalize_prefix(&prefix);
1559
1560 prop_assert!(
1562 normalized.starts_with('/'),
1563 "Normalized prefix '{}' should start with '/', input was '{}'",
1564 normalized, prefix
1565 );
1566
1567 prop_assert!(
1569 !normalized.starts_with("//"),
1570 "Normalized prefix '{}' should not start with '//', input was '{}'",
1571 normalized, prefix
1572 );
1573 }
1574
1575 #[test]
1580 fn prop_normalized_prefix_no_trailing_slash(
1581 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1582 trailing_slashes in prop::collection::vec(Just('/'), 0..5),
1583 ) {
1584 let mut prefix = String::from("/");
1586 for (i, segment) in segments.iter().enumerate() {
1587 if i > 0 {
1588 prefix.push('/');
1589 }
1590 prefix.push_str(segment);
1591 }
1592 for _ in &trailing_slashes {
1593 prefix.push('/');
1594 }
1595
1596 let normalized = normalize_prefix(&prefix);
1597
1598 prop_assert!(
1600 !normalized.ends_with('/'),
1601 "Normalized prefix '{}' should not end with '/', input was '{}'",
1602 normalized, prefix
1603 );
1604 }
1605
1606 #[test]
1611 fn prop_normalized_prefix_no_double_slashes(
1612 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1614 extra_slashes in prop::collection::vec(0..4usize, 1..4),
1615 ) {
1616 let mut prefix = String::from("/");
1618 for (i, segment) in segments.iter().enumerate() {
1619 if i > 0 {
1620 let num_slashes = extra_slashes.get(i).copied().unwrap_or(1);
1622 for _ in 0..=num_slashes {
1623 prefix.push('/');
1624 }
1625 }
1626 prefix.push_str(segment);
1627 }
1628
1629 let normalized = normalize_prefix(&prefix);
1630
1631 prop_assert!(
1633 !normalized.contains("//"),
1634 "Normalized prefix '{}' should not contain '//', input was '{}'",
1635 normalized, prefix
1636 );
1637 }
1638
1639 #[test]
1644 fn prop_normalized_prefix_preserves_segments(
1645 segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..4),
1646 ) {
1647 let prefix = format!("/{}", segments.join("/"));
1649
1650 let normalized = normalize_prefix(&prefix);
1651
1652 let normalized_segments: Vec<&str> = normalized
1654 .split('/')
1655 .filter(|s| !s.is_empty())
1656 .collect();
1657
1658 prop_assert_eq!(
1659 segments.len(),
1660 normalized_segments.len(),
1661 "Segment count should be preserved"
1662 );
1663
1664 for (original, normalized_seg) in segments.iter().zip(normalized_segments.iter()) {
1665 prop_assert_eq!(
1666 original, normalized_seg,
1667 "Segment content should be preserved"
1668 );
1669 }
1670 }
1671
1672 #[test]
1677 fn prop_empty_or_slashes_normalize_to_root(
1678 num_slashes in 0..10usize,
1679 ) {
1680 let prefix: String = std::iter::repeat('/').take(num_slashes).collect();
1681
1682 let normalized = normalize_prefix(&prefix);
1683
1684 prop_assert_eq!(
1685 normalized, "/",
1686 "Empty or slash-only prefix '{}' should normalize to '/'",
1687 prefix
1688 );
1689 }
1690 }
1691
1692 proptest! {
1699 #![proptest_config(ProptestConfig::with_cases(100))]
1700
1701 #[test]
1706 fn prop_method_router_clone_preserves_methods(
1707 use_get in any::<bool>(),
1709 use_post in any::<bool>(),
1710 use_put in any::<bool>(),
1711 use_patch in any::<bool>(),
1712 use_delete in any::<bool>(),
1713 ) {
1714 prop_assume!(use_get || use_post || use_put || use_patch || use_delete);
1716
1717 let mut method_router = MethodRouter::new();
1719 let mut expected_methods: Vec<Method> = Vec::new();
1720
1721 async fn handler() -> &'static str { "handler" }
1722
1723 if use_get {
1724 method_router = get(handler);
1725 expected_methods.push(Method::GET);
1726 }
1727
1728 if use_post {
1729 let post_router = post(handler);
1730 for (method, handler) in post_router.handlers {
1731 method_router.handlers.insert(method.clone(), handler);
1732 if !expected_methods.contains(&method) {
1733 expected_methods.push(method);
1734 }
1735 }
1736 }
1737
1738 if use_put {
1739 let put_router = put(handler);
1740 for (method, handler) in put_router.handlers {
1741 method_router.handlers.insert(method.clone(), handler);
1742 if !expected_methods.contains(&method) {
1743 expected_methods.push(method);
1744 }
1745 }
1746 }
1747
1748 if use_patch {
1749 let patch_router = patch(handler);
1750 for (method, handler) in patch_router.handlers {
1751 method_router.handlers.insert(method.clone(), handler);
1752 if !expected_methods.contains(&method) {
1753 expected_methods.push(method);
1754 }
1755 }
1756 }
1757
1758 if use_delete {
1759 let delete_router = delete(handler);
1760 for (method, handler) in delete_router.handlers {
1761 method_router.handlers.insert(method.clone(), handler);
1762 if !expected_methods.contains(&method) {
1763 expected_methods.push(method);
1764 }
1765 }
1766 }
1767
1768 let cloned_router = method_router.clone();
1770
1771 let original_methods = method_router.allowed_methods();
1773 let cloned_methods = cloned_router.allowed_methods();
1774
1775 prop_assert_eq!(
1776 original_methods.len(),
1777 cloned_methods.len(),
1778 "Cloned router should have same number of methods"
1779 );
1780
1781 for method in &expected_methods {
1782 prop_assert!(
1783 cloned_router.get_handler(method).is_some(),
1784 "Cloned router should have handler for method {:?}",
1785 method
1786 );
1787 }
1788
1789 for method in &cloned_methods {
1791 prop_assert!(
1792 cloned_router.get_handler(method).is_some(),
1793 "Handler for {:?} should be accessible after clone",
1794 method
1795 );
1796 }
1797 }
1798 }
1799
1800 proptest! {
1808 #![proptest_config(ProptestConfig::with_cases(100))]
1809
1810 #[test]
1815 fn prop_nested_routes_have_prefix(
1816 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1818 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1820 has_param in any::<bool>(),
1821 ) {
1822 async fn handler() -> &'static str { "handler" }
1823
1824 let prefix = format!("/{}", prefix_segments.join("/"));
1826
1827 let mut route_path = format!("/{}", route_segments.join("/"));
1829 if has_param {
1830 route_path.push_str("/{id}");
1831 }
1832
1833 let nested_router = Router::new().route(&route_path, get(handler));
1835 let app = Router::new().nest(&prefix, nested_router);
1836
1837 let expected_matchit_path = if has_param {
1839 format!("{}/{}/:id", prefix, route_segments.join("/"))
1840 } else {
1841 format!("{}/{}", prefix, route_segments.join("/"))
1842 };
1843
1844 let routes = app.registered_routes();
1845
1846 prop_assert!(
1848 routes.contains_key(&expected_matchit_path),
1849 "Expected route '{}' not found. Available routes: {:?}",
1850 expected_matchit_path,
1851 routes.keys().collect::<Vec<_>>()
1852 );
1853
1854 let route_info = routes.get(&expected_matchit_path).unwrap();
1856 let expected_display_path = format!("{}{}", prefix, route_path);
1857 prop_assert_eq!(
1858 &route_info.path, &expected_display_path,
1859 "Display path should be prefix + original path"
1860 );
1861 }
1862
1863 #[test]
1868 fn prop_route_count_preserved_after_nesting(
1869 num_routes in 1..4usize,
1871 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1872 ) {
1873 async fn handler() -> &'static str { "handler" }
1874
1875 let prefix = format!("/{}", prefix_segments.join("/"));
1876
1877 let mut nested_router = Router::new();
1879 for i in 0..num_routes {
1880 let path = format!("/route{}", i);
1881 nested_router = nested_router.route(&path, get(handler));
1882 }
1883
1884 let app = Router::new().nest(&prefix, nested_router);
1885
1886 prop_assert_eq!(
1887 app.registered_routes().len(),
1888 num_routes,
1889 "Number of routes should be preserved after nesting"
1890 );
1891 }
1892
1893 #[test]
1897 fn prop_nested_routes_are_matchable(
1898 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1899 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1900 ) {
1901 async fn handler() -> &'static str { "handler" }
1902
1903 let prefix = format!("/{}", prefix_segments.join("/"));
1904 let route_path = format!("/{}", route_segments.join("/"));
1905
1906 let nested_router = Router::new().route(&route_path, get(handler));
1907 let app = Router::new().nest(&prefix, nested_router);
1908
1909 let full_path = format!("{}{}", prefix, route_path);
1911
1912 match app.match_route(&full_path, &Method::GET) {
1914 RouteMatch::Found { .. } => {
1915 }
1917 RouteMatch::NotFound => {
1918 prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
1919 }
1920 RouteMatch::MethodNotAllowed { .. } => {
1921 prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
1922 }
1923 }
1924 }
1925 }
1926
1927 proptest! {
1934 #![proptest_config(ProptestConfig::with_cases(100))]
1935
1936 #[test]
1941 fn prop_state_type_ids_merged(
1942 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1943 has_nested_state in any::<bool>(),
1944 ) {
1945 #[derive(Clone)]
1946 struct TestState(#[allow(dead_code)] i32);
1947
1948 async fn handler() -> &'static str { "handler" }
1949
1950 let prefix = format!("/{}", prefix_segments.join("/"));
1951
1952 let mut nested = Router::new().route("/test", get(handler));
1953 if has_nested_state {
1954 nested = nested.state(TestState(42));
1955 }
1956
1957 let parent = Router::new().nest(&prefix, nested);
1958
1959 if has_nested_state {
1961 prop_assert!(
1962 parent.state_type_ids().contains(&std::any::TypeId::of::<TestState>()),
1963 "Parent should track nested state type ID"
1964 );
1965 }
1966 }
1967
1968 #[test]
1973 fn prop_merge_state_adds_nested_state(
1974 state_value in any::<i32>(),
1975 ) {
1976 #[derive(Clone, PartialEq, Debug)]
1977 struct UniqueState(i32);
1978
1979 let source = Router::new().state(UniqueState(state_value));
1981
1982 let parent = Router::new().merge_state::<UniqueState>(&source);
1984
1985 prop_assert!(
1987 parent.has_state::<UniqueState>(),
1988 "Parent should have state after merge"
1989 );
1990
1991 let merged_state = parent.state.get::<UniqueState>().unwrap();
1993 prop_assert_eq!(
1994 merged_state.0, state_value,
1995 "Merged state value should match source"
1996 );
1997 }
1998 }
1999
2000 proptest! {
2007 #![proptest_config(ProptestConfig::with_cases(100))]
2008
2009 #[test]
2014 fn prop_parent_state_takes_precedence(
2015 parent_value in any::<i32>(),
2016 nested_value in any::<i32>(),
2017 ) {
2018 prop_assume!(parent_value != nested_value);
2020
2021 #[derive(Clone, PartialEq, Debug)]
2022 struct SharedState(i32);
2023
2024 let source = Router::new().state(SharedState(nested_value));
2026
2027 let parent = Router::new()
2029 .state(SharedState(parent_value))
2030 .merge_state::<SharedState>(&source);
2031
2032 prop_assert!(
2034 parent.has_state::<SharedState>(),
2035 "Parent should have state"
2036 );
2037
2038 let final_state = parent.state.get::<SharedState>().unwrap();
2040 prop_assert_eq!(
2041 final_state.0, parent_value,
2042 "Parent state value should be preserved, not overwritten by nested"
2043 );
2044 }
2045
2046 #[test]
2051 fn prop_state_precedence_consistent(
2052 parent_value in any::<i32>(),
2053 source1_value in any::<i32>(),
2054 source2_value in any::<i32>(),
2055 ) {
2056 #[derive(Clone, PartialEq, Debug)]
2057 struct ConsistentState(i32);
2058
2059 let source1 = Router::new().state(ConsistentState(source1_value));
2061 let source2 = Router::new().state(ConsistentState(source2_value));
2062
2063 let parent = Router::new()
2065 .state(ConsistentState(parent_value))
2066 .merge_state::<ConsistentState>(&source1)
2067 .merge_state::<ConsistentState>(&source2);
2068
2069 let final_state = parent.state.get::<ConsistentState>().unwrap();
2071 prop_assert_eq!(
2072 final_state.0, parent_value,
2073 "Parent state should be preserved after multiple merges"
2074 );
2075 }
2076 }
2077
2078 proptest! {
2086 #![proptest_config(ProptestConfig::with_cases(100))]
2087
2088 #[test]
2093 fn prop_same_structure_different_param_names_conflict(
2094 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
2096 param1 in "[a-z][a-z0-9]{0,5}",
2098 param2 in "[a-z][a-z0-9]{0,5}",
2099 ) {
2100 prop_assume!(param1 != param2);
2102
2103 let mut path1 = String::from("/");
2105 let mut path2 = String::from("/");
2106
2107 for segment in &segments {
2108 path1.push_str(segment);
2109 path1.push('/');
2110 path2.push_str(segment);
2111 path2.push('/');
2112 }
2113
2114 path1.push('{');
2115 path1.push_str(¶m1);
2116 path1.push('}');
2117
2118 path2.push('{');
2119 path2.push_str(¶m2);
2120 path2.push('}');
2121
2122 let result = catch_unwind(AssertUnwindSafe(|| {
2124 async fn handler1() -> &'static str { "handler1" }
2125 async fn handler2() -> &'static str { "handler2" }
2126
2127 let _router = Router::new()
2128 .route(&path1, get(handler1))
2129 .route(&path2, get(handler2));
2130 }));
2131
2132 prop_assert!(
2133 result.is_err(),
2134 "Routes '{}' and '{}' should conflict but didn't",
2135 path1, path2
2136 );
2137 }
2138
2139 #[test]
2144 fn prop_different_structures_no_conflict(
2145 segments1 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2147 segments2 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2148 has_param1 in any::<bool>(),
2150 has_param2 in any::<bool>(),
2151 ) {
2152 let mut path1 = String::from("/");
2154 let mut path2 = String::from("/");
2155
2156 for segment in &segments1 {
2157 path1.push_str(segment);
2158 path1.push('/');
2159 }
2160 path1.pop(); for segment in &segments2 {
2163 path2.push_str(segment);
2164 path2.push('/');
2165 }
2166 path2.pop(); if has_param1 {
2169 path1.push_str("/{id}");
2170 }
2171
2172 if has_param2 {
2173 path2.push_str("/{id}");
2174 }
2175
2176 let norm1 = normalize_path_for_comparison(&convert_path_params(&path1));
2178 let norm2 = normalize_path_for_comparison(&convert_path_params(&path2));
2179
2180 prop_assume!(norm1 != norm2);
2182
2183 let result = catch_unwind(AssertUnwindSafe(|| {
2185 async fn handler1() -> &'static str { "handler1" }
2186 async fn handler2() -> &'static str { "handler2" }
2187
2188 let router = Router::new()
2189 .route(&path1, get(handler1))
2190 .route(&path2, get(handler2));
2191
2192 router.registered_routes().len()
2193 }));
2194
2195 prop_assert!(
2196 result.is_ok(),
2197 "Routes '{}' and '{}' should not conflict but did",
2198 path1, path2
2199 );
2200
2201 if let Ok(count) = result {
2202 prop_assert_eq!(count, 2, "Should have registered 2 routes");
2203 }
2204 }
2205
2206 #[test]
2211 fn prop_conflict_error_contains_both_paths(
2212 segment in "[a-z][a-z0-9]{1,5}",
2214 param1 in "[a-z][a-z0-9]{1,5}",
2215 param2 in "[a-z][a-z0-9]{1,5}",
2216 ) {
2217 prop_assume!(param1 != param2);
2218
2219 let path1 = format!("/{}/{{{}}}", segment, param1);
2220 let path2 = format!("/{}/{{{}}}", segment, param2);
2221
2222 let result = catch_unwind(AssertUnwindSafe(|| {
2223 async fn handler1() -> &'static str { "handler1" }
2224 async fn handler2() -> &'static str { "handler2" }
2225
2226 let _router = Router::new()
2227 .route(&path1, get(handler1))
2228 .route(&path2, get(handler2));
2229 }));
2230
2231 prop_assert!(result.is_err(), "Should have panicked due to conflict");
2232
2233 if let Err(panic_info) = result {
2235 if let Some(msg) = panic_info.downcast_ref::<String>() {
2236 prop_assert!(
2237 msg.contains("ROUTE CONFLICT DETECTED"),
2238 "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
2239 msg
2240 );
2241 prop_assert!(
2242 msg.contains("Existing:") && msg.contains("New:"),
2243 "Error should contain both 'Existing:' and 'New:' labels, got: {}",
2244 msg
2245 );
2246 prop_assert!(
2247 msg.contains("How to resolve:"),
2248 "Error should contain resolution guidance, got: {}",
2249 msg
2250 );
2251 }
2252 }
2253 }
2254
2255 #[test]
2259 fn prop_exact_duplicate_paths_conflict(
2260 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
2262 has_param in any::<bool>(),
2263 ) {
2264 let mut path = String::from("/");
2266
2267 for segment in &segments {
2268 path.push_str(segment);
2269 path.push('/');
2270 }
2271 path.pop(); if has_param {
2274 path.push_str("/{id}");
2275 }
2276
2277 let result = catch_unwind(AssertUnwindSafe(|| {
2279 async fn handler1() -> &'static str { "handler1" }
2280 async fn handler2() -> &'static str { "handler2" }
2281
2282 let _router = Router::new()
2283 .route(&path, get(handler1))
2284 .route(&path, get(handler2));
2285 }));
2286
2287 prop_assert!(
2288 result.is_err(),
2289 "Registering path '{}' twice should conflict but didn't",
2290 path
2291 );
2292 }
2293 }
2294
2295 proptest! {
2302 #![proptest_config(ProptestConfig::with_cases(100))]
2303
2304 #[test]
2309 fn prop_nested_route_with_params_matches(
2310 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2311 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
2312 param_value in "[a-z0-9]{1,10}",
2313 ) {
2314 async fn handler() -> &'static str { "handler" }
2315
2316 let prefix = format!("/{}", prefix_segments.join("/"));
2317 let route_path = if route_segments.is_empty() {
2318 "/{id}".to_string()
2319 } else {
2320 format!("/{}/{{id}}", route_segments.join("/"))
2321 };
2322
2323 let nested_router = Router::new().route(&route_path, get(handler));
2324 let app = Router::new().nest(&prefix, nested_router);
2325
2326 let full_path = if route_segments.is_empty() {
2328 format!("{}/{}", prefix, param_value)
2329 } else {
2330 format!("{}/{}/{}", prefix, route_segments.join("/"), param_value)
2331 };
2332
2333 match app.match_route(&full_path, &Method::GET) {
2335 RouteMatch::Found { params, .. } => {
2336 prop_assert!(
2338 params.contains_key("id"),
2339 "Should have 'id' parameter, got: {:?}",
2340 params
2341 );
2342 prop_assert_eq!(
2343 params.get("id").unwrap(),
2344 ¶m_value,
2345 "Parameter value should match"
2346 );
2347 }
2348 RouteMatch::NotFound => {
2349 prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
2350 }
2351 RouteMatch::MethodNotAllowed { .. } => {
2352 prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
2353 }
2354 }
2355 }
2356
2357 #[test]
2362 fn prop_nested_route_matches_correct_method(
2363 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2364 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2365 use_get in any::<bool>(),
2366 ) {
2367 async fn handler() -> &'static str { "handler" }
2368
2369 let prefix = format!("/{}", prefix_segments.join("/"));
2370 let route_path = format!("/{}", route_segments.join("/"));
2371
2372 let method_router = if use_get { get(handler) } else { post(handler) };
2374 let nested_router = Router::new().route(&route_path, method_router);
2375 let app = Router::new().nest(&prefix, nested_router);
2376
2377 let full_path = format!("{}{}", prefix, route_path);
2378 let registered_method = if use_get { Method::GET } else { Method::POST };
2379 let other_method = if use_get { Method::POST } else { Method::GET };
2380
2381 match app.match_route(&full_path, ®istered_method) {
2383 RouteMatch::Found { .. } => {
2384 }
2386 other => {
2387 prop_assert!(false, "Route should be found for registered method, got: {:?}",
2388 match other {
2389 RouteMatch::NotFound => "NotFound",
2390 RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
2391 _ => "Found",
2392 }
2393 );
2394 }
2395 }
2396
2397 match app.match_route(&full_path, &other_method) {
2399 RouteMatch::MethodNotAllowed { allowed } => {
2400 prop_assert!(
2401 allowed.contains(®istered_method),
2402 "Allowed methods should contain {:?}",
2403 registered_method
2404 );
2405 }
2406 other => {
2407 prop_assert!(false, "Route should return MethodNotAllowed for other method, got: {:?}",
2408 match other {
2409 RouteMatch::NotFound => "NotFound",
2410 RouteMatch::Found { .. } => "Found",
2411 _ => "MethodNotAllowed",
2412 }
2413 );
2414 }
2415 }
2416 }
2417 }
2418
2419 proptest! {
2426 #![proptest_config(ProptestConfig::with_cases(100))]
2427
2428 #[test]
2433 fn prop_single_param_extraction(
2434 prefix in "[a-z][a-z0-9]{1,5}",
2435 param_name in "[a-z][a-z0-9]{1,5}",
2436 param_value in "[a-z0-9]{1,10}",
2437 ) {
2438 async fn handler() -> &'static str { "handler" }
2439
2440 let prefix = format!("/{}", prefix);
2441 let route_path = format!("/{{{}}}", param_name);
2442
2443 let nested_router = Router::new().route(&route_path, get(handler));
2444 let app = Router::new().nest(&prefix, nested_router);
2445
2446 let full_path = format!("{}/{}", prefix, param_value);
2447
2448 match app.match_route(&full_path, &Method::GET) {
2449 RouteMatch::Found { params, .. } => {
2450 prop_assert!(
2451 params.contains_key(¶m_name),
2452 "Should have '{}' parameter, got: {:?}",
2453 param_name, params
2454 );
2455 prop_assert_eq!(
2456 params.get(¶m_name).unwrap(),
2457 ¶m_value,
2458 "Parameter '{}' value should be '{}'",
2459 param_name, param_value
2460 );
2461 }
2462 _ => {
2463 prop_assert!(false, "Route should be found");
2464 }
2465 }
2466 }
2467
2468 #[test]
2473 fn prop_multiple_params_extraction(
2474 prefix in "[a-z][a-z0-9]{1,5}",
2475 param1_name in "[a-z]{1,5}",
2476 param1_value in "[a-z0-9]{1,10}",
2477 param2_name in "[a-z]{1,5}",
2478 param2_value in "[a-z0-9]{1,10}",
2479 ) {
2480 prop_assume!(param1_name != param2_name);
2482
2483 async fn handler() -> &'static str { "handler" }
2484
2485 let prefix = format!("/{}", prefix);
2486 let route_path = format!("/{{{}}}/items/{{{}}}", param1_name, param2_name);
2487
2488 let nested_router = Router::new().route(&route_path, get(handler));
2489 let app = Router::new().nest(&prefix, nested_router);
2490
2491 let full_path = format!("{}/{}/items/{}", prefix, param1_value, param2_value);
2492
2493 match app.match_route(&full_path, &Method::GET) {
2494 RouteMatch::Found { params, .. } => {
2495 prop_assert!(
2497 params.contains_key(¶m1_name),
2498 "Should have '{}' parameter, got: {:?}",
2499 param1_name, params
2500 );
2501 prop_assert_eq!(
2502 params.get(¶m1_name).unwrap(),
2503 ¶m1_value,
2504 "Parameter '{}' value should be '{}'",
2505 param1_name, param1_value
2506 );
2507
2508 prop_assert!(
2510 params.contains_key(¶m2_name),
2511 "Should have '{}' parameter, got: {:?}",
2512 param2_name, params
2513 );
2514 prop_assert_eq!(
2515 params.get(¶m2_name).unwrap(),
2516 ¶m2_value,
2517 "Parameter '{}' value should be '{}'",
2518 param2_name, param2_value
2519 );
2520 }
2521 _ => {
2522 prop_assert!(false, "Route should be found");
2523 }
2524 }
2525 }
2526
2527 #[test]
2532 fn prop_param_value_preservation(
2533 prefix in "[a-z]{1,5}",
2534 param_value in "[a-zA-Z0-9_-]{1,15}",
2536 ) {
2537 async fn handler() -> &'static str { "handler" }
2538
2539 let prefix = format!("/{}", prefix);
2540 let route_path = "/{id}".to_string();
2541
2542 let nested_router = Router::new().route(&route_path, get(handler));
2543 let app = Router::new().nest(&prefix, nested_router);
2544
2545 let full_path = format!("{}/{}", prefix, param_value);
2546
2547 match app.match_route(&full_path, &Method::GET) {
2548 RouteMatch::Found { params, .. } => {
2549 prop_assert_eq!(
2550 params.get("id").unwrap(),
2551 ¶m_value,
2552 "Parameter value should be preserved exactly"
2553 );
2554 }
2555 _ => {
2556 prop_assert!(false, "Route should be found");
2557 }
2558 }
2559 }
2560 }
2561
2562 proptest! {
2569 #![proptest_config(ProptestConfig::with_cases(100))]
2570
2571 #[test]
2576 fn prop_unregistered_path_returns_not_found(
2577 prefix in "[a-z][a-z0-9]{1,5}",
2578 route_segment in "[a-z][a-z0-9]{1,5}",
2579 unregistered_segment in "[a-z][a-z0-9]{6,10}",
2580 ) {
2581 prop_assume!(route_segment != unregistered_segment);
2583
2584 async fn handler() -> &'static str { "handler" }
2585
2586 let prefix = format!("/{}", prefix);
2587 let route_path = format!("/{}", route_segment);
2588
2589 let nested_router = Router::new().route(&route_path, get(handler));
2590 let app = Router::new().nest(&prefix, nested_router);
2591
2592 let unregistered_path = format!("{}/{}", prefix, unregistered_segment);
2594
2595 match app.match_route(&unregistered_path, &Method::GET) {
2596 RouteMatch::NotFound => {
2597 }
2599 RouteMatch::Found { .. } => {
2600 prop_assert!(false, "Path '{}' should not be found", unregistered_path);
2601 }
2602 RouteMatch::MethodNotAllowed { .. } => {
2603 prop_assert!(false, "Path '{}' should return NotFound, not MethodNotAllowed", unregistered_path);
2604 }
2605 }
2606 }
2607
2608 #[test]
2612 fn prop_wrong_prefix_returns_not_found(
2613 prefix1 in "[a-z][a-z0-9]{1,5}",
2614 prefix2 in "[a-z][a-z0-9]{6,10}",
2615 route_segment in "[a-z][a-z0-9]{1,5}",
2616 ) {
2617 prop_assume!(prefix1 != prefix2);
2619
2620 async fn handler() -> &'static str { "handler" }
2621
2622 let prefix = format!("/{}", prefix1);
2623 let route_path = format!("/{}", route_segment);
2624
2625 let nested_router = Router::new().route(&route_path, get(handler));
2626 let app = Router::new().nest(&prefix, nested_router);
2627
2628 let wrong_prefix_path = format!("/{}/{}", prefix2, route_segment);
2630
2631 match app.match_route(&wrong_prefix_path, &Method::GET) {
2632 RouteMatch::NotFound => {
2633 }
2635 _ => {
2636 prop_assert!(false, "Path '{}' with wrong prefix should return NotFound", wrong_prefix_path);
2637 }
2638 }
2639 }
2640
2641 #[test]
2646 fn prop_partial_path_returns_not_found(
2647 prefix in "[a-z][a-z0-9]{1,5}",
2648 segment1 in "[a-z][a-z0-9]{1,5}",
2649 segment2 in "[a-z][a-z0-9]{1,5}",
2650 ) {
2651 async fn handler() -> &'static str { "handler" }
2652
2653 let prefix = format!("/{}", prefix);
2654 let route_path = format!("/{}/{}", segment1, segment2);
2655
2656 let nested_router = Router::new().route(&route_path, get(handler));
2657 let app = Router::new().nest(&prefix, nested_router);
2658
2659 let partial_path = format!("{}/{}", prefix, segment1);
2661
2662 match app.match_route(&partial_path, &Method::GET) {
2663 RouteMatch::NotFound => {
2664 }
2666 _ => {
2667 prop_assert!(false, "Partial path '{}' should return NotFound", partial_path);
2668 }
2669 }
2670 }
2671 }
2672
2673 proptest! {
2680 #![proptest_config(ProptestConfig::with_cases(100))]
2681
2682 #[test]
2688 fn prop_unregistered_method_returns_method_not_allowed(
2689 prefix in "[a-z][a-z0-9]{1,5}",
2690 route_segment in "[a-z][a-z0-9]{1,5}",
2691 ) {
2692 async fn handler() -> &'static str { "handler" }
2693
2694 let prefix = format!("/{}", prefix);
2695 let route_path = format!("/{}", route_segment);
2696
2697 let nested_router = Router::new().route(&route_path, get(handler));
2699 let app = Router::new().nest(&prefix, nested_router);
2700
2701 let full_path = format!("{}{}", prefix, route_path);
2702
2703 match app.match_route(&full_path, &Method::POST) {
2705 RouteMatch::MethodNotAllowed { allowed } => {
2706 prop_assert!(
2707 allowed.contains(&Method::GET),
2708 "Allowed methods should contain GET, got: {:?}",
2709 allowed
2710 );
2711 prop_assert!(
2712 !allowed.contains(&Method::POST),
2713 "Allowed methods should not contain POST"
2714 );
2715 }
2716 RouteMatch::Found { .. } => {
2717 prop_assert!(false, "POST should not be found on GET-only route");
2718 }
2719 RouteMatch::NotFound => {
2720 prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
2721 }
2722 }
2723 }
2724
2725 #[test]
2730 fn prop_multiple_methods_in_allowed_list(
2731 prefix in "[a-z][a-z0-9]{1,5}",
2732 route_segment in "[a-z][a-z0-9]{1,5}",
2733 use_get in any::<bool>(),
2734 use_post in any::<bool>(),
2735 use_put in any::<bool>(),
2736 ) {
2737 prop_assume!(use_get || use_post || use_put);
2739
2740 async fn handler() -> &'static str { "handler" }
2741
2742 let prefix = format!("/{}", prefix);
2743 let route_path = format!("/{}", route_segment);
2744
2745 let mut method_router = MethodRouter::new();
2747 let mut expected_methods: Vec<Method> = Vec::new();
2748
2749 if use_get {
2750 let get_router = get(handler);
2751 for (method, h) in get_router.handlers {
2752 method_router.handlers.insert(method.clone(), h);
2753 expected_methods.push(method);
2754 }
2755 }
2756 if use_post {
2757 let post_router = post(handler);
2758 for (method, h) in post_router.handlers {
2759 method_router.handlers.insert(method.clone(), h);
2760 expected_methods.push(method);
2761 }
2762 }
2763 if use_put {
2764 let put_router = put(handler);
2765 for (method, h) in put_router.handlers {
2766 method_router.handlers.insert(method.clone(), h);
2767 expected_methods.push(method);
2768 }
2769 }
2770
2771 let nested_router = Router::new().route(&route_path, method_router);
2772 let app = Router::new().nest(&prefix, nested_router);
2773
2774 let full_path = format!("{}{}", prefix, route_path);
2775
2776 match app.match_route(&full_path, &Method::DELETE) {
2778 RouteMatch::MethodNotAllowed { allowed } => {
2779 for method in &expected_methods {
2781 prop_assert!(
2782 allowed.contains(method),
2783 "Allowed methods should contain {:?}, got: {:?}",
2784 method, allowed
2785 );
2786 }
2787 prop_assert!(
2789 !allowed.contains(&Method::DELETE),
2790 "Allowed methods should not contain DELETE"
2791 );
2792 }
2793 RouteMatch::Found { .. } => {
2794 prop_assert!(false, "DELETE should not be found");
2795 }
2796 RouteMatch::NotFound => {
2797 prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
2798 }
2799 }
2800 }
2801 }
2802
2803 proptest! {
2817 #![proptest_config(ProptestConfig::with_cases(100))]
2818
2819 #[test]
2825 fn prop_multiple_routers_all_routes_registered(
2826 prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2828 prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2829 num_routes1 in 1..4usize,
2831 num_routes2 in 1..4usize,
2832 ) {
2833 let prefix1 = format!("/{}", prefix1_segments.join("/"));
2835 let prefix2 = format!("/{}", prefix2_segments.join("/"));
2836
2837 prop_assume!(prefix1 != prefix2);
2839
2840 async fn handler() -> &'static str { "handler" }
2841
2842 let mut router1 = Router::new();
2844 for i in 0..num_routes1 {
2845 let path = format!("/route1_{}", i);
2846 router1 = router1.route(&path, get(handler));
2847 }
2848
2849 let mut router2 = Router::new();
2851 for i in 0..num_routes2 {
2852 let path = format!("/route2_{}", i);
2853 router2 = router2.route(&path, get(handler));
2854 }
2855
2856 let app = Router::new()
2858 .nest(&prefix1, router1)
2859 .nest(&prefix2, router2);
2860
2861 let routes = app.registered_routes();
2862
2863 let expected_count = num_routes1 + num_routes2;
2865 prop_assert_eq!(
2866 routes.len(),
2867 expected_count,
2868 "Should have {} routes ({}+{}), got {}",
2869 expected_count, num_routes1, num_routes2, routes.len()
2870 );
2871
2872 for i in 0..num_routes1 {
2874 let expected_path = format!("{}/route1_{}", prefix1, i);
2875 let matchit_path = convert_path_params(&expected_path);
2876 prop_assert!(
2877 routes.contains_key(&matchit_path),
2878 "Route '{}' should be registered",
2879 expected_path
2880 );
2881 }
2882
2883 for i in 0..num_routes2 {
2885 let expected_path = format!("{}/route2_{}", prefix2, i);
2886 let matchit_path = convert_path_params(&expected_path);
2887 prop_assert!(
2888 routes.contains_key(&matchit_path),
2889 "Route '{}' should be registered",
2890 expected_path
2891 );
2892 }
2893 }
2894
2895 #[test]
2900 fn prop_multiple_routers_no_interference(
2901 prefix1 in "[a-z][a-z0-9]{1,5}",
2902 prefix2 in "[a-z][a-z0-9]{1,5}",
2903 route_segment in "[a-z][a-z0-9]{1,5}",
2904 param_value1 in "[a-z0-9]{1,10}",
2905 param_value2 in "[a-z0-9]{1,10}",
2906 ) {
2907 prop_assume!(prefix1 != prefix2);
2909
2910 let prefix1 = format!("/{}", prefix1);
2911 let prefix2 = format!("/{}", prefix2);
2912
2913 async fn handler() -> &'static str { "handler" }
2914
2915 let router1 = Router::new()
2917 .route(&format!("/{}", route_segment), get(handler))
2918 .route("/{id}", get(handler));
2919
2920 let router2 = Router::new()
2921 .route(&format!("/{}", route_segment), get(handler))
2922 .route("/{id}", get(handler));
2923
2924 let app = Router::new()
2926 .nest(&prefix1, router1)
2927 .nest(&prefix2, router2);
2928
2929 let path1_static = format!("{}/{}", prefix1, route_segment);
2931 match app.match_route(&path1_static, &Method::GET) {
2932 RouteMatch::Found { params, .. } => {
2933 prop_assert!(params.is_empty(), "Static path should have no params");
2934 }
2935 _ => {
2936 prop_assert!(false, "Route '{}' should be found", path1_static);
2937 }
2938 }
2939
2940 let path1_param = format!("{}/{}", prefix1, param_value1);
2941 match app.match_route(&path1_param, &Method::GET) {
2942 RouteMatch::Found { params, .. } => {
2943 prop_assert_eq!(
2944 params.get("id"),
2945 Some(¶m_value1.to_string()),
2946 "Parameter should be extracted correctly"
2947 );
2948 }
2949 _ => {
2950 prop_assert!(false, "Route '{}' should be found", path1_param);
2951 }
2952 }
2953
2954 let path2_static = format!("{}/{}", prefix2, route_segment);
2956 match app.match_route(&path2_static, &Method::GET) {
2957 RouteMatch::Found { params, .. } => {
2958 prop_assert!(params.is_empty(), "Static path should have no params");
2959 }
2960 _ => {
2961 prop_assert!(false, "Route '{}' should be found", path2_static);
2962 }
2963 }
2964
2965 let path2_param = format!("{}/{}", prefix2, param_value2);
2966 match app.match_route(&path2_param, &Method::GET) {
2967 RouteMatch::Found { params, .. } => {
2968 prop_assert_eq!(
2969 params.get("id"),
2970 Some(¶m_value2.to_string()),
2971 "Parameter should be extracted correctly"
2972 );
2973 }
2974 _ => {
2975 prop_assert!(false, "Route '{}' should be found", path2_param);
2976 }
2977 }
2978 }
2979
2980 #[test]
2985 fn prop_multiple_routers_preserve_methods(
2986 prefix1 in "[a-z][a-z0-9]{1,5}",
2987 prefix2 in "[a-z][a-z0-9]{1,5}",
2988 route_segment in "[a-z][a-z0-9]{1,5}",
2989 router1_use_get in any::<bool>(),
2990 router1_use_post in any::<bool>(),
2991 router2_use_get in any::<bool>(),
2992 router2_use_put in any::<bool>(),
2993 ) {
2994 prop_assume!(router1_use_get || router1_use_post);
2996 prop_assume!(router2_use_get || router2_use_put);
2997 prop_assume!(prefix1 != prefix2);
2999
3000 let prefix1 = format!("/{}", prefix1);
3001 let prefix2 = format!("/{}", prefix2);
3002 let route_path = format!("/{}", route_segment);
3003
3004 async fn handler() -> &'static str { "handler" }
3005
3006 let mut method_router1 = MethodRouter::new();
3008 let mut expected_methods1: Vec<Method> = Vec::new();
3009 if router1_use_get {
3010 let get_router = get(handler);
3011 for (method, h) in get_router.handlers {
3012 method_router1.handlers.insert(method.clone(), h);
3013 expected_methods1.push(method);
3014 }
3015 }
3016 if router1_use_post {
3017 let post_router = post(handler);
3018 for (method, h) in post_router.handlers {
3019 method_router1.handlers.insert(method.clone(), h);
3020 expected_methods1.push(method);
3021 }
3022 }
3023
3024 let mut method_router2 = MethodRouter::new();
3026 let mut expected_methods2: Vec<Method> = Vec::new();
3027 if router2_use_get {
3028 let get_router = get(handler);
3029 for (method, h) in get_router.handlers {
3030 method_router2.handlers.insert(method.clone(), h);
3031 expected_methods2.push(method);
3032 }
3033 }
3034 if router2_use_put {
3035 let put_router = put(handler);
3036 for (method, h) in put_router.handlers {
3037 method_router2.handlers.insert(method.clone(), h);
3038 expected_methods2.push(method);
3039 }
3040 }
3041
3042 let router1 = Router::new().route(&route_path, method_router1);
3043 let router2 = Router::new().route(&route_path, method_router2);
3044
3045 let app = Router::new()
3046 .nest(&prefix1, router1)
3047 .nest(&prefix2, router2);
3048
3049 let full_path1 = format!("{}{}", prefix1, route_path);
3050 let full_path2 = format!("{}{}", prefix2, route_path);
3051
3052 for method in &expected_methods1 {
3054 match app.match_route(&full_path1, method) {
3055 RouteMatch::Found { .. } => {}
3056 _ => {
3057 prop_assert!(false, "Method {:?} should be found for {}", method, full_path1);
3058 }
3059 }
3060 }
3061
3062 for method in &expected_methods2 {
3064 match app.match_route(&full_path2, method) {
3065 RouteMatch::Found { .. } => {}
3066 _ => {
3067 prop_assert!(false, "Method {:?} should be found for {}", method, full_path2);
3068 }
3069 }
3070 }
3071
3072 if !expected_methods1.contains(&Method::DELETE) {
3074 match app.match_route(&full_path1, &Method::DELETE) {
3075 RouteMatch::MethodNotAllowed { allowed } => {
3076 for method in &expected_methods1 {
3077 prop_assert!(
3078 allowed.contains(method),
3079 "Allowed methods for {} should contain {:?}",
3080 full_path1, method
3081 );
3082 }
3083 }
3084 _ => {
3085 prop_assert!(false, "DELETE should return MethodNotAllowed for {}", full_path1);
3086 }
3087 }
3088 }
3089 }
3090
3091 #[test]
3096 fn prop_three_routers_composition(
3097 prefix1 in "[a-z]{1,3}",
3098 prefix2 in "[a-z]{4,6}",
3099 prefix3 in "[a-z]{7,9}",
3100 num_routes in 1..3usize,
3101 ) {
3102 let prefix1 = format!("/{}", prefix1);
3103 let prefix2 = format!("/{}", prefix2);
3104 let prefix3 = format!("/{}", prefix3);
3105
3106 async fn handler() -> &'static str { "handler" }
3107
3108 let mut router1 = Router::new();
3110 let mut router2 = Router::new();
3111 let mut router3 = Router::new();
3112
3113 for i in 0..num_routes {
3114 let path = format!("/item{}", i);
3115 router1 = router1.route(&path, get(handler));
3116 router2 = router2.route(&path, get(handler));
3117 router3 = router3.route(&path, get(handler));
3118 }
3119
3120 let app = Router::new()
3122 .nest(&prefix1, router1)
3123 .nest(&prefix2, router2)
3124 .nest(&prefix3, router3);
3125
3126 let routes = app.registered_routes();
3127
3128 let expected_count = 3 * num_routes;
3130 prop_assert_eq!(
3131 routes.len(),
3132 expected_count,
3133 "Should have {} routes, got {}",
3134 expected_count, routes.len()
3135 );
3136
3137 for i in 0..num_routes {
3139 let path1 = format!("{}/item{}", prefix1, i);
3140 let path2 = format!("{}/item{}", prefix2, i);
3141 let path3 = format!("{}/item{}", prefix3, i);
3142
3143 match app.match_route(&path1, &Method::GET) {
3144 RouteMatch::Found { .. } => {}
3145 _ => prop_assert!(false, "Route '{}' should be found", path1),
3146 }
3147 match app.match_route(&path2, &Method::GET) {
3148 RouteMatch::Found { .. } => {}
3149 _ => prop_assert!(false, "Route '{}' should be found", path2),
3150 }
3151 match app.match_route(&path3, &Method::GET) {
3152 RouteMatch::Found { .. } => {}
3153 _ => prop_assert!(false, "Route '{}' should be found", path3),
3154 }
3155 }
3156 }
3157 }
3158 proptest! {
3159 #![proptest_config(ProptestConfig::with_cases(100))]
3160
3161 #[test]
3167 fn prop_nested_route_conflict_different_param_names(
3168 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3169 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
3170 param1 in "[a-z][a-z0-9]{1,5}",
3171 param2 in "[a-z][a-z0-9]{1,5}",
3172 ) {
3173 prop_assume!(param1 != param2);
3175
3176 async fn handler1() -> &'static str { "handler1" }
3177 async fn handler2() -> &'static str { "handler2" }
3178
3179 let prefix = format!("/{}", prefix_segments.join("/"));
3180
3181 let existing_path = if route_segments.is_empty() {
3183 format!("{}/{{{}}}", prefix, param1)
3184 } else {
3185 format!("{}/{}/{{{}}}", prefix, route_segments.join("/"), param1)
3186 };
3187
3188 let nested_path = if route_segments.is_empty() {
3190 format!("/{{{}}}", param2)
3191 } else {
3192 format!("/{}/{{{}}}", route_segments.join("/"), param2)
3193 };
3194
3195 let result = catch_unwind(AssertUnwindSafe(|| {
3197 let parent = Router::new().route(&existing_path, get(handler1));
3198 let nested = Router::new().route(&nested_path, get(handler2));
3199 let _app = parent.nest(&prefix, nested);
3200 }));
3201
3202 prop_assert!(
3204 result.is_err(),
3205 "Nested route '{}{}' should conflict with existing route '{}' but didn't",
3206 prefix, nested_path, existing_path
3207 );
3208
3209 if let Err(panic_info) = result {
3211 if let Some(msg) = panic_info.downcast_ref::<String>() {
3212 prop_assert!(
3213 msg.contains("ROUTE CONFLICT DETECTED"),
3214 "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
3215 msg
3216 );
3217 prop_assert!(
3218 msg.contains("Existing:") && msg.contains("New:"),
3219 "Error should contain both 'Existing:' and 'New:' labels, got: {}",
3220 msg
3221 );
3222 }
3223 }
3224 }
3225
3226 #[test]
3231 fn prop_nested_route_conflict_exact_same_path(
3232 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3233 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3234 ) {
3235 async fn handler1() -> &'static str { "handler1" }
3236 async fn handler2() -> &'static str { "handler2" }
3237
3238 let prefix = format!("/{}", prefix_segments.join("/"));
3239 let route_path = format!("/{}", route_segments.join("/"));
3240
3241 let existing_path = format!("{}{}", prefix, route_path);
3243
3244 let result = catch_unwind(AssertUnwindSafe(|| {
3246 let parent = Router::new().route(&existing_path, get(handler1));
3247 let nested = Router::new().route(&route_path, get(handler2));
3248 let _app = parent.nest(&prefix, nested);
3249 }));
3250
3251 prop_assert!(
3253 result.is_err(),
3254 "Nested route '{}{}' should conflict with existing route '{}' but didn't",
3255 prefix, route_path, existing_path
3256 );
3257 }
3258
3259 #[test]
3264 fn prop_nested_routes_different_prefixes_no_conflict(
3265 prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3266 prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3267 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3268 has_param in any::<bool>(),
3269 ) {
3270 let prefix1 = format!("/{}", prefix1_segments.join("/"));
3272 let prefix2 = format!("/{}", prefix2_segments.join("/"));
3273
3274 prop_assume!(prefix1 != prefix2);
3276
3277 async fn handler1() -> &'static str { "handler1" }
3278 async fn handler2() -> &'static str { "handler2" }
3279
3280 let route_path = if has_param {
3282 format!("/{}/{{id}}", route_segments.join("/"))
3283 } else {
3284 format!("/{}", route_segments.join("/"))
3285 };
3286
3287 let result = catch_unwind(AssertUnwindSafe(|| {
3289 let nested1 = Router::new().route(&route_path, get(handler1));
3290 let nested2 = Router::new().route(&route_path, get(handler2));
3291
3292 let app = Router::new()
3293 .nest(&prefix1, nested1)
3294 .nest(&prefix2, nested2);
3295
3296 app.registered_routes().len()
3297 }));
3298
3299 prop_assert!(
3301 result.is_ok(),
3302 "Routes under different prefixes '{}' and '{}' should not conflict",
3303 prefix1, prefix2
3304 );
3305
3306 if let Ok(count) = result {
3307 prop_assert_eq!(count, 2, "Should have registered 2 routes");
3308 }
3309 }
3310
3311 #[test]
3316 fn prop_nested_conflict_error_contains_guidance(
3317 prefix in "[a-z][a-z0-9]{1,5}",
3318 segment in "[a-z][a-z0-9]{1,5}",
3319 param1 in "[a-z][a-z0-9]{1,5}",
3320 param2 in "[a-z][a-z0-9]{1,5}",
3321 ) {
3322 prop_assume!(param1 != param2);
3323
3324 async fn handler1() -> &'static str { "handler1" }
3325 async fn handler2() -> &'static str { "handler2" }
3326
3327 let prefix = format!("/{}", prefix);
3328 let existing_path = format!("{}/{}/{{{}}}", prefix, segment, param1);
3329 let nested_path = format!("/{}/{{{}}}", segment, param2);
3330
3331 let result = catch_unwind(AssertUnwindSafe(|| {
3332 let parent = Router::new().route(&existing_path, get(handler1));
3333 let nested = Router::new().route(&nested_path, get(handler2));
3334 let _app = parent.nest(&prefix, nested);
3335 }));
3336
3337 prop_assert!(result.is_err(), "Should have detected conflict");
3338
3339 if let Err(panic_info) = result {
3340 if let Some(msg) = panic_info.downcast_ref::<String>() {
3341 prop_assert!(
3342 msg.contains("How to resolve:"),
3343 "Error should contain 'How to resolve:' guidance, got: {}",
3344 msg
3345 );
3346 prop_assert!(
3347 msg.contains("Use different path patterns") ||
3348 msg.contains("different path patterns"),
3349 "Error should suggest using different path patterns, got: {}",
3350 msg
3351 );
3352 }
3353 }
3354 }
3355 }
3356}