1use crate::handler::{into_boxed_handler, BoxedHandler, Handler};
45use crate::path_params::PathParams;
46use http::{Extensions, Method};
47use matchit::Router as MatchitRouter;
48use rustapi_openapi::Operation;
49use std::collections::HashMap;
50use std::sync::Arc;
51
52#[derive(Debug, Clone)]
54pub struct RouteInfo {
55 pub path: String,
57 pub methods: Vec<Method>,
59}
60
61#[derive(Debug, Clone)]
63pub struct RouteConflictError {
64 pub new_path: String,
66 pub method: Option<Method>,
68 pub existing_path: String,
70 pub details: String,
72}
73
74impl std::fmt::Display for RouteConflictError {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 writeln!(
77 f,
78 "\n╭─────────────────────────────────────────────────────────────╮"
79 )?;
80 writeln!(
81 f,
82 "│ ROUTE CONFLICT DETECTED │"
83 )?;
84 writeln!(
85 f,
86 "╰─────────────────────────────────────────────────────────────╯"
87 )?;
88 writeln!(f)?;
89 writeln!(f, " Conflicting routes:")?;
90 writeln!(f, " → Existing: {}", self.existing_path)?;
91 writeln!(f, " → New: {}", self.new_path)?;
92 writeln!(f)?;
93 if let Some(ref method) = self.method {
94 writeln!(f, " HTTP Method: {}", method)?;
95 writeln!(f)?;
96 }
97 writeln!(f, " Details: {}", self.details)?;
98 writeln!(f)?;
99 writeln!(f, " How to resolve:")?;
100 writeln!(f, " 1. Use different path patterns for each route")?;
101 writeln!(
102 f,
103 " 2. If paths must be similar, ensure parameter names differ"
104 )?;
105 writeln!(
106 f,
107 " 3. Consider using different HTTP methods if appropriate"
108 )?;
109 writeln!(f)?;
110 writeln!(f, " Example:")?;
111 writeln!(f, " Instead of:")?;
112 writeln!(f, " .route(\"/users/{{id}}\", get(handler1))")?;
113 writeln!(f, " .route(\"/users/{{user_id}}\", get(handler2))")?;
114 writeln!(f)?;
115 writeln!(f, " Use:")?;
116 writeln!(f, " .route(\"/users/{{id}}\", get(handler1))")?;
117 writeln!(f, " .route(\"/users/{{id}}/profile\", get(handler2))")?;
118 Ok(())
119 }
120}
121
122impl std::error::Error for RouteConflictError {}
123
124pub struct MethodRouter {
126 handlers: HashMap<Method, BoxedHandler>,
127 pub(crate) operations: HashMap<Method, Operation>,
128}
129
130impl Clone for MethodRouter {
131 fn clone(&self) -> Self {
132 Self {
133 handlers: self.handlers.clone(),
134 operations: self.operations.clone(),
135 }
136 }
137}
138
139impl MethodRouter {
140 pub fn new() -> Self {
142 Self {
143 handlers: HashMap::new(),
144 operations: HashMap::new(),
145 }
146 }
147
148 fn on(mut self, method: Method, handler: BoxedHandler, operation: Operation) -> Self {
150 self.handlers.insert(method.clone(), handler);
151 self.operations.insert(method, operation);
152 self
153 }
154
155 pub(crate) fn get_handler(&self, method: &Method) -> Option<&BoxedHandler> {
157 self.handlers.get(method)
158 }
159
160 pub(crate) fn allowed_methods(&self) -> Vec<Method> {
162 self.handlers.keys().cloned().collect()
163 }
164
165 pub(crate) fn from_boxed(handlers: HashMap<Method, BoxedHandler>) -> Self {
167 Self {
168 handlers,
169 operations: HashMap::new(), }
171 }
172
173 pub(crate) fn insert_boxed_with_operation(
177 &mut self,
178 method: Method,
179 handler: BoxedHandler,
180 operation: Operation,
181 ) {
182 if self.handlers.contains_key(&method) {
183 panic!(
184 "Duplicate handler for method {} on the same path",
185 method.as_str()
186 );
187 }
188
189 self.handlers.insert(method.clone(), handler);
190 self.operations.insert(method, operation);
191 }
192 pub fn get<H, T>(self, handler: H) -> Self
194 where
195 H: Handler<T>,
196 T: 'static,
197 {
198 let mut op = Operation::new();
199 H::update_operation(&mut op);
200 self.on(Method::GET, into_boxed_handler(handler), op)
201 }
202
203 pub fn post<H, T>(self, handler: H) -> Self
205 where
206 H: Handler<T>,
207 T: 'static,
208 {
209 let mut op = Operation::new();
210 H::update_operation(&mut op);
211 self.on(Method::POST, into_boxed_handler(handler), op)
212 }
213
214 pub fn put<H, T>(self, handler: H) -> Self
216 where
217 H: Handler<T>,
218 T: 'static,
219 {
220 let mut op = Operation::new();
221 H::update_operation(&mut op);
222 self.on(Method::PUT, into_boxed_handler(handler), op)
223 }
224
225 pub fn patch<H, T>(self, handler: H) -> Self
227 where
228 H: Handler<T>,
229 T: 'static,
230 {
231 let mut op = Operation::new();
232 H::update_operation(&mut op);
233 self.on(Method::PATCH, into_boxed_handler(handler), op)
234 }
235
236 pub fn delete<H, T>(self, handler: H) -> Self
238 where
239 H: Handler<T>,
240 T: 'static,
241 {
242 let mut op = Operation::new();
243 H::update_operation(&mut op);
244 self.on(Method::DELETE, into_boxed_handler(handler), op)
245 }
246}
247
248impl Default for MethodRouter {
249 fn default() -> Self {
250 Self::new()
251 }
252}
253
254pub fn get<H, T>(handler: H) -> MethodRouter
256where
257 H: Handler<T>,
258 T: 'static,
259{
260 let mut op = Operation::new();
261 H::update_operation(&mut op);
262 MethodRouter::new().on(Method::GET, into_boxed_handler(handler), op)
263}
264
265pub fn post<H, T>(handler: H) -> MethodRouter
267where
268 H: Handler<T>,
269 T: 'static,
270{
271 let mut op = Operation::new();
272 H::update_operation(&mut op);
273 MethodRouter::new().on(Method::POST, into_boxed_handler(handler), op)
274}
275
276pub fn put<H, T>(handler: H) -> MethodRouter
278where
279 H: Handler<T>,
280 T: 'static,
281{
282 let mut op = Operation::new();
283 H::update_operation(&mut op);
284 MethodRouter::new().on(Method::PUT, into_boxed_handler(handler), op)
285}
286
287pub fn patch<H, T>(handler: H) -> MethodRouter
289where
290 H: Handler<T>,
291 T: 'static,
292{
293 let mut op = Operation::new();
294 H::update_operation(&mut op);
295 MethodRouter::new().on(Method::PATCH, into_boxed_handler(handler), op)
296}
297
298pub fn delete<H, T>(handler: H) -> MethodRouter
300where
301 H: Handler<T>,
302 T: 'static,
303{
304 let mut op = Operation::new();
305 H::update_operation(&mut op);
306 MethodRouter::new().on(Method::DELETE, into_boxed_handler(handler), op)
307}
308
309pub struct Router {
311 inner: MatchitRouter<MethodRouter>,
312 state: Arc<Extensions>,
313 registered_routes: HashMap<String, RouteInfo>,
315 method_routers: HashMap<String, MethodRouter>,
317 state_type_ids: Vec<std::any::TypeId>,
320}
321
322impl Router {
323 pub fn new() -> Self {
325 Self {
326 inner: MatchitRouter::new(),
327 state: Arc::new(Extensions::new()),
328 registered_routes: HashMap::new(),
329 method_routers: HashMap::new(),
330 state_type_ids: Vec::new(),
331 }
332 }
333
334 pub fn route(mut self, path: &str, method_router: MethodRouter) -> Self {
336 let matchit_path = convert_path_params(path);
338
339 let methods: Vec<Method> = method_router.handlers.keys().cloned().collect();
341
342 self.method_routers
344 .insert(matchit_path.clone(), method_router.clone());
345
346 match self.inner.insert(matchit_path.clone(), method_router) {
347 Ok(_) => {
348 self.registered_routes.insert(
350 matchit_path.clone(),
351 RouteInfo {
352 path: path.to_string(),
353 methods,
354 },
355 );
356 }
357 Err(e) => {
358 self.method_routers.remove(&matchit_path);
360
361 let existing_path = self
363 .find_conflicting_route(&matchit_path)
364 .map(|info| info.path.clone())
365 .unwrap_or_else(|| "<unknown>".to_string());
366
367 let conflict_error = RouteConflictError {
368 new_path: path.to_string(),
369 method: methods.first().cloned(),
370 existing_path,
371 details: e.to_string(),
372 };
373
374 panic!("{}", conflict_error);
375 }
376 }
377 self
378 }
379
380 fn find_conflicting_route(&self, matchit_path: &str) -> Option<&RouteInfo> {
382 if let Some(info) = self.registered_routes.get(matchit_path) {
384 return Some(info);
385 }
386
387 let normalized_new = normalize_path_for_comparison(matchit_path);
389
390 for (registered_path, info) in &self.registered_routes {
391 let normalized_existing = normalize_path_for_comparison(registered_path);
392 if normalized_new == normalized_existing {
393 return Some(info);
394 }
395 }
396
397 None
398 }
399
400 pub fn state<S: Clone + Send + Sync + 'static>(mut self, state: S) -> Self {
402 let type_id = std::any::TypeId::of::<S>();
403 let extensions = Arc::make_mut(&mut self.state);
404 extensions.insert(state);
405 if !self.state_type_ids.contains(&type_id) {
406 self.state_type_ids.push(type_id);
407 }
408 self
409 }
410
411 pub fn has_state<S: 'static>(&self) -> bool {
413 self.state_type_ids.contains(&std::any::TypeId::of::<S>())
414 }
415
416 pub fn state_type_ids(&self) -> &[std::any::TypeId] {
418 &self.state_type_ids
419 }
420
421 pub fn nest(mut self, prefix: &str, router: Router) -> Self {
457 let normalized_prefix = normalize_prefix(prefix);
459
460 for type_id in &router.state_type_ids {
464 if !self.state_type_ids.contains(type_id) {
465 self.state_type_ids.push(*type_id);
466 }
467 }
468
469 let nested_routes: Vec<(String, RouteInfo, MethodRouter)> = router
472 .registered_routes
473 .into_iter()
474 .filter_map(|(matchit_path, route_info)| {
475 router
476 .method_routers
477 .get(&matchit_path)
478 .map(|mr| (matchit_path, route_info, mr.clone()))
479 })
480 .collect();
481
482 for (matchit_path, route_info, method_router) in nested_routes {
484 let prefixed_matchit_path = if matchit_path == "/" {
488 normalized_prefix.clone()
489 } else {
490 format!("{}{}", normalized_prefix, matchit_path)
491 };
492
493 let prefixed_display_path = if route_info.path == "/" {
494 normalized_prefix.clone()
495 } else {
496 format!("{}{}", normalized_prefix, route_info.path)
497 };
498
499 self.method_routers
501 .insert(prefixed_matchit_path.clone(), method_router.clone());
502
503 match self
505 .inner
506 .insert(prefixed_matchit_path.clone(), method_router)
507 {
508 Ok(_) => {
509 self.registered_routes.insert(
511 prefixed_matchit_path,
512 RouteInfo {
513 path: prefixed_display_path,
514 methods: route_info.methods,
515 },
516 );
517 }
518 Err(e) => {
519 self.method_routers.remove(&prefixed_matchit_path);
521
522 let existing_path = self
524 .find_conflicting_route(&prefixed_matchit_path)
525 .map(|info| info.path.clone())
526 .unwrap_or_else(|| "<unknown>".to_string());
527
528 let conflict_error = RouteConflictError {
529 new_path: prefixed_display_path,
530 method: route_info.methods.first().cloned(),
531 existing_path,
532 details: e.to_string(),
533 };
534
535 panic!("{}", conflict_error);
536 }
537 }
538 }
539
540 self
541 }
542
543 pub fn merge_state<S: Clone + Send + Sync + 'static>(mut self, other: &Router) -> Self {
560 let type_id = std::any::TypeId::of::<S>();
561
562 if !self.state_type_ids.contains(&type_id) {
564 if let Some(state) = other.state.get::<S>() {
566 let extensions = Arc::make_mut(&mut self.state);
567 extensions.insert(state.clone());
568 self.state_type_ids.push(type_id);
569 }
570 }
571
572 self
573 }
574
575 pub(crate) fn match_route(&self, path: &str, method: &Method) -> RouteMatch<'_> {
577 match self.inner.at(path) {
578 Ok(matched) => {
579 let method_router = matched.value;
580
581 if let Some(handler) = method_router.get_handler(method) {
582 let params: PathParams = matched
584 .params
585 .iter()
586 .map(|(k, v)| (k.to_string(), v.to_string()))
587 .collect();
588
589 RouteMatch::Found { handler, params }
590 } else {
591 RouteMatch::MethodNotAllowed {
592 allowed: method_router.allowed_methods(),
593 }
594 }
595 }
596 Err(_) => RouteMatch::NotFound,
597 }
598 }
599
600 pub(crate) fn state_ref(&self) -> Arc<Extensions> {
602 self.state.clone()
603 }
604
605 pub fn registered_routes(&self) -> &HashMap<String, RouteInfo> {
607 &self.registered_routes
608 }
609
610 pub fn method_routers(&self) -> &HashMap<String, MethodRouter> {
612 &self.method_routers
613 }
614}
615
616impl Default for Router {
617 fn default() -> Self {
618 Self::new()
619 }
620}
621
622pub(crate) enum RouteMatch<'a> {
624 Found {
625 handler: &'a BoxedHandler,
626 params: PathParams,
627 },
628 NotFound,
629 MethodNotAllowed {
630 allowed: Vec<Method>,
631 },
632}
633
634fn convert_path_params(path: &str) -> String {
636 let mut result = String::with_capacity(path.len());
637
638 for ch in path.chars() {
639 match ch {
640 '{' => {
641 result.push(':');
642 }
643 '}' => {
644 }
646 _ => {
647 result.push(ch);
648 }
649 }
650 }
651
652 result
653}
654
655fn normalize_path_for_comparison(path: &str) -> String {
657 let mut result = String::with_capacity(path.len());
658 let mut in_param = false;
659
660 for ch in path.chars() {
661 match ch {
662 ':' => {
663 in_param = true;
664 result.push_str(":_");
665 }
666 '/' => {
667 in_param = false;
668 result.push('/');
669 }
670 _ if in_param => {
671 }
673 _ => {
674 result.push(ch);
675 }
676 }
677 }
678
679 result
680}
681
682pub(crate) fn normalize_prefix(prefix: &str) -> String {
699 if prefix.is_empty() {
701 return "/".to_string();
702 }
703
704 let segments: Vec<&str> = prefix.split('/').filter(|s| !s.is_empty()).collect();
706
707 if segments.is_empty() {
709 return "/".to_string();
710 }
711
712 let mut result = String::with_capacity(prefix.len() + 1);
714 for segment in segments {
715 result.push('/');
716 result.push_str(segment);
717 }
718
719 result
720}
721
722#[cfg(test)]
723mod tests {
724 use super::*;
725
726 #[test]
727 fn test_convert_path_params() {
728 assert_eq!(convert_path_params("/users/{id}"), "/users/:id");
729 assert_eq!(
730 convert_path_params("/users/{user_id}/posts/{post_id}"),
731 "/users/:user_id/posts/:post_id"
732 );
733 assert_eq!(convert_path_params("/static/path"), "/static/path");
734 }
735
736 #[test]
737 fn test_normalize_path_for_comparison() {
738 assert_eq!(normalize_path_for_comparison("/users/:id"), "/users/:_");
739 assert_eq!(
740 normalize_path_for_comparison("/users/:user_id"),
741 "/users/:_"
742 );
743 assert_eq!(
744 normalize_path_for_comparison("/users/:id/posts/:post_id"),
745 "/users/:_/posts/:_"
746 );
747 assert_eq!(
748 normalize_path_for_comparison("/static/path"),
749 "/static/path"
750 );
751 }
752
753 #[test]
754 fn test_normalize_prefix() {
755 assert_eq!(normalize_prefix("api"), "/api");
757 assert_eq!(normalize_prefix("/api"), "/api");
758 assert_eq!(normalize_prefix("/api/"), "/api");
759 assert_eq!(normalize_prefix("api/"), "/api");
760
761 assert_eq!(normalize_prefix("api/v1"), "/api/v1");
763 assert_eq!(normalize_prefix("/api/v1"), "/api/v1");
764 assert_eq!(normalize_prefix("/api/v1/"), "/api/v1");
765
766 assert_eq!(normalize_prefix(""), "/");
768 assert_eq!(normalize_prefix("/"), "/");
769
770 assert_eq!(normalize_prefix("//api"), "/api");
772 assert_eq!(normalize_prefix("api//v1"), "/api/v1");
773 assert_eq!(normalize_prefix("//api//v1//"), "/api/v1");
774 assert_eq!(normalize_prefix("///"), "/");
775 }
776
777 #[test]
778 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
779 fn test_route_conflict_detection() {
780 async fn handler1() -> &'static str {
781 "handler1"
782 }
783 async fn handler2() -> &'static str {
784 "handler2"
785 }
786
787 let _router = Router::new()
788 .route("/users/{id}", get(handler1))
789 .route("/users/{user_id}", get(handler2)); }
791
792 #[test]
793 fn test_no_conflict_different_paths() {
794 async fn handler1() -> &'static str {
795 "handler1"
796 }
797 async fn handler2() -> &'static str {
798 "handler2"
799 }
800
801 let router = Router::new()
802 .route("/users/{id}", get(handler1))
803 .route("/users/{id}/profile", get(handler2));
804
805 assert_eq!(router.registered_routes().len(), 2);
806 }
807
808 #[test]
809 fn test_route_info_tracking() {
810 async fn handler() -> &'static str {
811 "handler"
812 }
813
814 let router = Router::new().route("/users/{id}", get(handler));
815
816 let routes = router.registered_routes();
817 assert_eq!(routes.len(), 1);
818
819 let info = routes.get("/users/:id").unwrap();
820 assert_eq!(info.path, "/users/{id}");
821 assert_eq!(info.methods.len(), 1);
822 assert_eq!(info.methods[0], Method::GET);
823 }
824
825 #[test]
826 fn test_basic_router_nesting() {
827 async fn list_users() -> &'static str {
828 "list users"
829 }
830 async fn get_user() -> &'static str {
831 "get user"
832 }
833
834 let users_router = Router::new()
835 .route("/", get(list_users))
836 .route("/{id}", get(get_user));
837
838 let app = Router::new().nest("/api/users", users_router);
839
840 let routes = app.registered_routes();
841 assert_eq!(routes.len(), 2);
842
843 assert!(routes.contains_key("/api/users"));
845 assert!(routes.contains_key("/api/users/:id"));
846
847 let list_info = routes.get("/api/users").unwrap();
849 assert_eq!(list_info.path, "/api/users");
850
851 let get_info = routes.get("/api/users/:id").unwrap();
852 assert_eq!(get_info.path, "/api/users/{id}");
853 }
854
855 #[test]
856 fn test_nested_route_matching() {
857 async fn handler() -> &'static str {
858 "handler"
859 }
860
861 let users_router = Router::new().route("/{id}", get(handler));
862
863 let app = Router::new().nest("/api/users", users_router);
864
865 match app.match_route("/api/users/123", &Method::GET) {
867 RouteMatch::Found { params, .. } => {
868 assert_eq!(params.get("id"), Some(&"123".to_string()));
869 }
870 _ => panic!("Route should be found"),
871 }
872 }
873
874 #[test]
875 fn test_nested_route_matching_multiple_params() {
876 async fn handler() -> &'static str {
877 "handler"
878 }
879
880 let posts_router = Router::new().route("/{user_id}/posts/{post_id}", get(handler));
881
882 let app = Router::new().nest("/api", posts_router);
883
884 match app.match_route("/api/42/posts/100", &Method::GET) {
886 RouteMatch::Found { params, .. } => {
887 assert_eq!(params.get("user_id"), Some(&"42".to_string()));
888 assert_eq!(params.get("post_id"), Some(&"100".to_string()));
889 }
890 _ => panic!("Route should be found"),
891 }
892 }
893
894 #[test]
895 fn test_nested_route_matching_static_path() {
896 async fn handler() -> &'static str {
897 "handler"
898 }
899
900 let health_router = Router::new().route("/health", get(handler));
901
902 let app = Router::new().nest("/api/v1", health_router);
903
904 match app.match_route("/api/v1/health", &Method::GET) {
906 RouteMatch::Found { params, .. } => {
907 assert!(params.is_empty(), "Static path should have no params");
908 }
909 _ => panic!("Route should be found"),
910 }
911 }
912
913 #[test]
914 fn test_nested_route_not_found() {
915 async fn handler() -> &'static str {
916 "handler"
917 }
918
919 let users_router = Router::new().route("/users", get(handler));
920
921 let app = Router::new().nest("/api", users_router);
922
923 match app.match_route("/api/posts", &Method::GET) {
925 RouteMatch::NotFound => {
926 }
928 _ => panic!("Route should not be found"),
929 }
930
931 match app.match_route("/v2/users", &Method::GET) {
933 RouteMatch::NotFound => {
934 }
936 _ => panic!("Route with wrong prefix should not be found"),
937 }
938 }
939
940 #[test]
941 fn test_nested_route_method_not_allowed() {
942 async fn handler() -> &'static str {
943 "handler"
944 }
945
946 let users_router = Router::new().route("/users", get(handler));
947
948 let app = Router::new().nest("/api", users_router);
949
950 match app.match_route("/api/users", &Method::POST) {
952 RouteMatch::MethodNotAllowed { allowed } => {
953 assert!(allowed.contains(&Method::GET));
954 assert!(!allowed.contains(&Method::POST));
955 }
956 _ => panic!("Should return MethodNotAllowed"),
957 }
958 }
959
960 #[test]
961 fn test_nested_route_multiple_methods() {
962 async fn get_handler() -> &'static str {
963 "get"
964 }
965 async fn post_handler() -> &'static str {
966 "post"
967 }
968
969 let get_router = get(get_handler);
971 let post_router = post(post_handler);
972 let mut combined = MethodRouter::new();
973 for (method, handler) in get_router.handlers {
974 combined.handlers.insert(method, handler);
975 }
976 for (method, handler) in post_router.handlers {
977 combined.handlers.insert(method, handler);
978 }
979
980 let users_router = Router::new().route("/users", combined);
981 let app = Router::new().nest("/api", users_router);
982
983 match app.match_route("/api/users", &Method::GET) {
985 RouteMatch::Found { .. } => {}
986 _ => panic!("GET should be found"),
987 }
988
989 match app.match_route("/api/users", &Method::POST) {
990 RouteMatch::Found { .. } => {}
991 _ => panic!("POST should be found"),
992 }
993
994 match app.match_route("/api/users", &Method::DELETE) {
996 RouteMatch::MethodNotAllowed { allowed } => {
997 assert!(allowed.contains(&Method::GET));
998 assert!(allowed.contains(&Method::POST));
999 }
1000 _ => panic!("DELETE should return MethodNotAllowed"),
1001 }
1002 }
1003
1004 #[test]
1005 fn test_nested_router_prefix_normalization() {
1006 async fn handler() -> &'static str {
1007 "handler"
1008 }
1009
1010 let router1 = Router::new().route("/test", get(handler));
1012 let app1 = Router::new().nest("api", router1);
1013 assert!(app1.registered_routes().contains_key("/api/test"));
1014
1015 let router2 = Router::new().route("/test", get(handler));
1016 let app2 = Router::new().nest("/api/", router2);
1017 assert!(app2.registered_routes().contains_key("/api/test"));
1018
1019 let router3 = Router::new().route("/test", get(handler));
1020 let app3 = Router::new().nest("//api//", router3);
1021 assert!(app3.registered_routes().contains_key("/api/test"));
1022 }
1023
1024 #[test]
1025 fn test_state_tracking() {
1026 #[derive(Clone)]
1027 struct MyState(String);
1028
1029 let router = Router::new().state(MyState("test".to_string()));
1030
1031 assert!(router.has_state::<MyState>());
1032 assert!(!router.has_state::<String>());
1033 }
1034
1035 #[test]
1036 fn test_state_merge_nested_only() {
1037 #[derive(Clone, PartialEq, Debug)]
1038 struct NestedState(String);
1039
1040 async fn handler() -> &'static str {
1041 "handler"
1042 }
1043
1044 let state_source = Router::new().state(NestedState("nested".to_string()));
1046
1047 let nested = Router::new().route("/test", get(handler));
1048
1049 let parent = Router::new()
1050 .nest("/api", nested)
1051 .merge_state::<NestedState>(&state_source);
1052
1053 assert!(parent.has_state::<NestedState>());
1055
1056 let state = parent.state.get::<NestedState>().unwrap();
1058 assert_eq!(state.0, "nested");
1059 }
1060
1061 #[test]
1062 fn test_state_merge_parent_wins() {
1063 #[derive(Clone, PartialEq, Debug)]
1064 struct SharedState(String);
1065
1066 async fn handler() -> &'static str {
1067 "handler"
1068 }
1069
1070 let state_source = Router::new().state(SharedState("nested".to_string()));
1072
1073 let nested = Router::new().route("/test", get(handler));
1074
1075 let parent = Router::new()
1076 .state(SharedState("parent".to_string()))
1077 .nest("/api", nested)
1078 .merge_state::<SharedState>(&state_source);
1079
1080 assert!(parent.has_state::<SharedState>());
1082
1083 let state = parent.state.get::<SharedState>().unwrap();
1085 assert_eq!(state.0, "parent");
1086 }
1087
1088 #[test]
1089 fn test_state_type_ids_merged_on_nest() {
1090 #[derive(Clone)]
1091 struct NestedState(String);
1092
1093 async fn handler() -> &'static str {
1094 "handler"
1095 }
1096
1097 let nested = Router::new()
1098 .route("/test", get(handler))
1099 .state(NestedState("nested".to_string()));
1100
1101 let parent = Router::new().nest("/api", nested);
1102
1103 assert!(parent
1105 .state_type_ids()
1106 .contains(&std::any::TypeId::of::<NestedState>()));
1107 }
1108
1109 #[test]
1110 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
1111 fn test_nested_route_conflict_with_existing_route() {
1112 async fn handler1() -> &'static str {
1113 "handler1"
1114 }
1115 async fn handler2() -> &'static str {
1116 "handler2"
1117 }
1118
1119 let parent = Router::new().route("/api/users/{id}", get(handler1));
1121
1122 let nested = Router::new().route("/{user_id}", get(handler2));
1124
1125 let _app = parent.nest("/api/users", nested);
1127 }
1128
1129 #[test]
1130 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
1131 fn test_nested_route_conflict_same_path_different_param_names() {
1132 async fn handler1() -> &'static str {
1133 "handler1"
1134 }
1135 async fn handler2() -> &'static str {
1136 "handler2"
1137 }
1138
1139 let nested1 = Router::new().route("/{id}", get(handler1));
1141 let nested2 = Router::new().route("/{user_id}", get(handler2));
1142
1143 let _app = Router::new()
1145 .nest("/api/users", nested1)
1146 .nest("/api/users", nested2);
1147 }
1148
1149 #[test]
1150 fn test_nested_route_conflict_error_contains_both_paths() {
1151 use std::panic::{catch_unwind, AssertUnwindSafe};
1152
1153 async fn handler1() -> &'static str {
1154 "handler1"
1155 }
1156 async fn handler2() -> &'static str {
1157 "handler2"
1158 }
1159
1160 let result = catch_unwind(AssertUnwindSafe(|| {
1161 let parent = Router::new().route("/api/users/{id}", get(handler1));
1162 let nested = Router::new().route("/{user_id}", get(handler2));
1163 let _app = parent.nest("/api/users", nested);
1164 }));
1165
1166 assert!(result.is_err(), "Should have panicked due to conflict");
1167
1168 if let Err(panic_info) = result {
1169 if let Some(msg) = panic_info.downcast_ref::<String>() {
1170 assert!(
1171 msg.contains("ROUTE CONFLICT DETECTED"),
1172 "Error should contain 'ROUTE CONFLICT DETECTED'"
1173 );
1174 assert!(
1175 msg.contains("Existing:") && msg.contains("New:"),
1176 "Error should contain both 'Existing:' and 'New:' labels"
1177 );
1178 assert!(
1179 msg.contains("How to resolve:"),
1180 "Error should contain resolution guidance"
1181 );
1182 }
1183 }
1184 }
1185
1186 #[test]
1187 fn test_nested_routes_no_conflict_different_prefixes() {
1188 async fn handler1() -> &'static str {
1189 "handler1"
1190 }
1191 async fn handler2() -> &'static str {
1192 "handler2"
1193 }
1194
1195 let nested1 = Router::new().route("/{id}", get(handler1));
1197 let nested2 = Router::new().route("/{id}", get(handler2));
1198
1199 let app = Router::new()
1201 .nest("/api/users", nested1)
1202 .nest("/api/posts", nested2);
1203
1204 assert_eq!(app.registered_routes().len(), 2);
1205 assert!(app.registered_routes().contains_key("/api/users/:id"));
1206 assert!(app.registered_routes().contains_key("/api/posts/:id"));
1207 }
1208
1209 #[test]
1214 fn test_multiple_router_composition_all_routes_registered() {
1215 async fn users_list() -> &'static str {
1216 "users list"
1217 }
1218 async fn users_get() -> &'static str {
1219 "users get"
1220 }
1221 async fn posts_list() -> &'static str {
1222 "posts list"
1223 }
1224 async fn posts_get() -> &'static str {
1225 "posts get"
1226 }
1227 async fn comments_list() -> &'static str {
1228 "comments list"
1229 }
1230
1231 let users_router = Router::new()
1233 .route("/", get(users_list))
1234 .route("/{id}", get(users_get));
1235
1236 let posts_router = Router::new()
1237 .route("/", get(posts_list))
1238 .route("/{id}", get(posts_get));
1239
1240 let comments_router = Router::new().route("/", get(comments_list));
1241
1242 let app = Router::new()
1244 .nest("/api/users", users_router)
1245 .nest("/api/posts", posts_router)
1246 .nest("/api/comments", comments_router);
1247
1248 let routes = app.registered_routes();
1250 assert_eq!(routes.len(), 5, "Should have 5 routes registered");
1251
1252 assert!(
1254 routes.contains_key("/api/users"),
1255 "Should have /api/users route"
1256 );
1257 assert!(
1258 routes.contains_key("/api/users/:id"),
1259 "Should have /api/users/:id route"
1260 );
1261
1262 assert!(
1264 routes.contains_key("/api/posts"),
1265 "Should have /api/posts route"
1266 );
1267 assert!(
1268 routes.contains_key("/api/posts/:id"),
1269 "Should have /api/posts/:id route"
1270 );
1271
1272 assert!(
1274 routes.contains_key("/api/comments"),
1275 "Should have /api/comments route"
1276 );
1277 }
1278
1279 #[test]
1280 fn test_multiple_router_composition_no_interference() {
1281 async fn users_handler() -> &'static str {
1282 "users"
1283 }
1284 async fn posts_handler() -> &'static str {
1285 "posts"
1286 }
1287 async fn admin_handler() -> &'static str {
1288 "admin"
1289 }
1290
1291 let users_router = Router::new()
1293 .route("/list", get(users_handler))
1294 .route("/{id}", get(users_handler));
1295
1296 let posts_router = Router::new()
1297 .route("/list", get(posts_handler))
1298 .route("/{id}", get(posts_handler));
1299
1300 let admin_router = Router::new()
1301 .route("/list", get(admin_handler))
1302 .route("/{id}", get(admin_handler));
1303
1304 let app = Router::new()
1306 .nest("/api/v1/users", users_router)
1307 .nest("/api/v1/posts", posts_router)
1308 .nest("/admin", admin_router);
1309
1310 let routes = app.registered_routes();
1312 assert_eq!(routes.len(), 6, "Should have 6 routes registered");
1313
1314 assert!(routes.contains_key("/api/v1/users/list"));
1316 assert!(routes.contains_key("/api/v1/users/:id"));
1317 assert!(routes.contains_key("/api/v1/posts/list"));
1318 assert!(routes.contains_key("/api/v1/posts/:id"));
1319 assert!(routes.contains_key("/admin/list"));
1320 assert!(routes.contains_key("/admin/:id"));
1321
1322 match app.match_route("/api/v1/users/list", &Method::GET) {
1324 RouteMatch::Found { params, .. } => {
1325 assert!(params.is_empty(), "Static path should have no params");
1326 }
1327 _ => panic!("Should find /api/v1/users/list"),
1328 }
1329
1330 match app.match_route("/api/v1/posts/123", &Method::GET) {
1331 RouteMatch::Found { params, .. } => {
1332 assert_eq!(params.get("id"), Some(&"123".to_string()));
1333 }
1334 _ => panic!("Should find /api/v1/posts/123"),
1335 }
1336
1337 match app.match_route("/admin/456", &Method::GET) {
1338 RouteMatch::Found { params, .. } => {
1339 assert_eq!(params.get("id"), Some(&"456".to_string()));
1340 }
1341 _ => panic!("Should find /admin/456"),
1342 }
1343 }
1344
1345 #[test]
1346 fn test_multiple_router_composition_with_multiple_methods() {
1347 async fn get_handler() -> &'static str {
1348 "get"
1349 }
1350 async fn post_handler() -> &'static str {
1351 "post"
1352 }
1353 async fn put_handler() -> &'static str {
1354 "put"
1355 }
1356
1357 let get_router = get(get_handler);
1360 let post_router = post(post_handler);
1361 let mut users_root_combined = MethodRouter::new();
1362 for (method, handler) in get_router.handlers {
1363 users_root_combined.handlers.insert(method, handler);
1364 }
1365 for (method, handler) in post_router.handlers {
1366 users_root_combined.handlers.insert(method, handler);
1367 }
1368
1369 let get_router2 = get(get_handler);
1371 let put_router = put(put_handler);
1372 let mut users_id_combined = MethodRouter::new();
1373 for (method, handler) in get_router2.handlers {
1374 users_id_combined.handlers.insert(method, handler);
1375 }
1376 for (method, handler) in put_router.handlers {
1377 users_id_combined.handlers.insert(method, handler);
1378 }
1379
1380 let users_router = Router::new()
1381 .route("/", users_root_combined)
1382 .route("/{id}", users_id_combined);
1383
1384 let get_router3 = get(get_handler);
1386 let post_router2 = post(post_handler);
1387 let mut posts_root_combined = MethodRouter::new();
1388 for (method, handler) in get_router3.handlers {
1389 posts_root_combined.handlers.insert(method, handler);
1390 }
1391 for (method, handler) in post_router2.handlers {
1392 posts_root_combined.handlers.insert(method, handler);
1393 }
1394
1395 let posts_router = Router::new().route("/", posts_root_combined);
1396
1397 let app = Router::new()
1399 .nest("/users", users_router)
1400 .nest("/posts", posts_router);
1401
1402 let routes = app.registered_routes();
1404 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1405
1406 let users_root = routes.get("/users").unwrap();
1408 assert!(users_root.methods.contains(&Method::GET));
1409 assert!(users_root.methods.contains(&Method::POST));
1410
1411 let users_id = routes.get("/users/:id").unwrap();
1412 assert!(users_id.methods.contains(&Method::GET));
1413 assert!(users_id.methods.contains(&Method::PUT));
1414
1415 let posts_root = routes.get("/posts").unwrap();
1417 assert!(posts_root.methods.contains(&Method::GET));
1418 assert!(posts_root.methods.contains(&Method::POST));
1419
1420 match app.match_route("/users", &Method::GET) {
1422 RouteMatch::Found { .. } => {}
1423 _ => panic!("GET /users should be found"),
1424 }
1425 match app.match_route("/users", &Method::POST) {
1426 RouteMatch::Found { .. } => {}
1427 _ => panic!("POST /users should be found"),
1428 }
1429 match app.match_route("/users/123", &Method::PUT) {
1430 RouteMatch::Found { .. } => {}
1431 _ => panic!("PUT /users/123 should be found"),
1432 }
1433 }
1434
1435 #[test]
1436 fn test_multiple_router_composition_deep_nesting() {
1437 async fn handler() -> &'static str {
1438 "handler"
1439 }
1440
1441 let deep_router = Router::new().route("/action", get(handler));
1443
1444 let mid_router = Router::new().route("/info", get(handler));
1445
1446 let shallow_router = Router::new().route("/status", get(handler));
1447
1448 let app = Router::new()
1450 .nest("/api/v1/resources/items", deep_router)
1451 .nest("/api/v1/resources", mid_router)
1452 .nest("/api", shallow_router);
1453
1454 let routes = app.registered_routes();
1456 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1457
1458 assert!(routes.contains_key("/api/v1/resources/items/action"));
1459 assert!(routes.contains_key("/api/v1/resources/info"));
1460 assert!(routes.contains_key("/api/status"));
1461
1462 match app.match_route("/api/v1/resources/items/action", &Method::GET) {
1464 RouteMatch::Found { .. } => {}
1465 _ => panic!("Should find deep route"),
1466 }
1467 match app.match_route("/api/v1/resources/info", &Method::GET) {
1468 RouteMatch::Found { .. } => {}
1469 _ => panic!("Should find mid route"),
1470 }
1471 match app.match_route("/api/status", &Method::GET) {
1472 RouteMatch::Found { .. } => {}
1473 _ => panic!("Should find shallow route"),
1474 }
1475 }
1476}
1477
1478#[cfg(test)]
1479mod property_tests {
1480 use super::*;
1481 use proptest::prelude::*;
1482 use std::panic::{catch_unwind, AssertUnwindSafe};
1483
1484 proptest! {
1492 #![proptest_config(ProptestConfig::with_cases(100))]
1493
1494 #[test]
1499 fn prop_normalized_prefix_starts_with_single_slash(
1500 leading_slashes in prop::collection::vec(Just('/'), 0..5),
1502 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 0..4),
1503 trailing_slashes in prop::collection::vec(Just('/'), 0..5),
1504 ) {
1505 let mut prefix = String::new();
1507 for _ in &leading_slashes {
1508 prefix.push('/');
1509 }
1510 for (i, segment) in segments.iter().enumerate() {
1511 if i > 0 {
1512 prefix.push('/');
1513 }
1514 prefix.push_str(segment);
1515 }
1516 for _ in &trailing_slashes {
1517 prefix.push('/');
1518 }
1519
1520 let normalized = normalize_prefix(&prefix);
1521
1522 prop_assert!(
1524 normalized.starts_with('/'),
1525 "Normalized prefix '{}' should start with '/', input was '{}'",
1526 normalized, prefix
1527 );
1528
1529 prop_assert!(
1531 !normalized.starts_with("//"),
1532 "Normalized prefix '{}' should not start with '//', input was '{}'",
1533 normalized, prefix
1534 );
1535 }
1536
1537 #[test]
1542 fn prop_normalized_prefix_no_trailing_slash(
1543 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1544 trailing_slashes in prop::collection::vec(Just('/'), 0..5),
1545 ) {
1546 let mut prefix = String::from("/");
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.ends_with('/'),
1563 "Normalized prefix '{}' should not end with '/', input was '{}'",
1564 normalized, prefix
1565 );
1566 }
1567
1568 #[test]
1573 fn prop_normalized_prefix_no_double_slashes(
1574 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1576 extra_slashes in prop::collection::vec(0..4usize, 1..4),
1577 ) {
1578 let mut prefix = String::from("/");
1580 for (i, segment) in segments.iter().enumerate() {
1581 if i > 0 {
1582 let num_slashes = extra_slashes.get(i).copied().unwrap_or(1);
1584 for _ in 0..=num_slashes {
1585 prefix.push('/');
1586 }
1587 }
1588 prefix.push_str(segment);
1589 }
1590
1591 let normalized = normalize_prefix(&prefix);
1592
1593 prop_assert!(
1595 !normalized.contains("//"),
1596 "Normalized prefix '{}' should not contain '//', input was '{}'",
1597 normalized, prefix
1598 );
1599 }
1600
1601 #[test]
1606 fn prop_normalized_prefix_preserves_segments(
1607 segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..4),
1608 ) {
1609 let prefix = format!("/{}", segments.join("/"));
1611
1612 let normalized = normalize_prefix(&prefix);
1613
1614 let normalized_segments: Vec<&str> = normalized
1616 .split('/')
1617 .filter(|s| !s.is_empty())
1618 .collect();
1619
1620 prop_assert_eq!(
1621 segments.len(),
1622 normalized_segments.len(),
1623 "Segment count should be preserved"
1624 );
1625
1626 for (original, normalized_seg) in segments.iter().zip(normalized_segments.iter()) {
1627 prop_assert_eq!(
1628 original, normalized_seg,
1629 "Segment content should be preserved"
1630 );
1631 }
1632 }
1633
1634 #[test]
1639 fn prop_empty_or_slashes_normalize_to_root(
1640 num_slashes in 0..10usize,
1641 ) {
1642 let prefix: String = std::iter::repeat('/').take(num_slashes).collect();
1643
1644 let normalized = normalize_prefix(&prefix);
1645
1646 prop_assert_eq!(
1647 normalized, "/",
1648 "Empty or slash-only prefix '{}' should normalize to '/'",
1649 prefix
1650 );
1651 }
1652 }
1653
1654 proptest! {
1661 #![proptest_config(ProptestConfig::with_cases(100))]
1662
1663 #[test]
1668 fn prop_method_router_clone_preserves_methods(
1669 use_get in any::<bool>(),
1671 use_post in any::<bool>(),
1672 use_put in any::<bool>(),
1673 use_patch in any::<bool>(),
1674 use_delete in any::<bool>(),
1675 ) {
1676 prop_assume!(use_get || use_post || use_put || use_patch || use_delete);
1678
1679 let mut method_router = MethodRouter::new();
1681 let mut expected_methods: Vec<Method> = Vec::new();
1682
1683 async fn handler() -> &'static str { "handler" }
1684
1685 if use_get {
1686 method_router = get(handler);
1687 expected_methods.push(Method::GET);
1688 }
1689
1690 if use_post {
1691 let post_router = post(handler);
1692 for (method, handler) in post_router.handlers {
1693 method_router.handlers.insert(method.clone(), handler);
1694 if !expected_methods.contains(&method) {
1695 expected_methods.push(method);
1696 }
1697 }
1698 }
1699
1700 if use_put {
1701 let put_router = put(handler);
1702 for (method, handler) in put_router.handlers {
1703 method_router.handlers.insert(method.clone(), handler);
1704 if !expected_methods.contains(&method) {
1705 expected_methods.push(method);
1706 }
1707 }
1708 }
1709
1710 if use_patch {
1711 let patch_router = patch(handler);
1712 for (method, handler) in patch_router.handlers {
1713 method_router.handlers.insert(method.clone(), handler);
1714 if !expected_methods.contains(&method) {
1715 expected_methods.push(method);
1716 }
1717 }
1718 }
1719
1720 if use_delete {
1721 let delete_router = delete(handler);
1722 for (method, handler) in delete_router.handlers {
1723 method_router.handlers.insert(method.clone(), handler);
1724 if !expected_methods.contains(&method) {
1725 expected_methods.push(method);
1726 }
1727 }
1728 }
1729
1730 let cloned_router = method_router.clone();
1732
1733 let original_methods = method_router.allowed_methods();
1735 let cloned_methods = cloned_router.allowed_methods();
1736
1737 prop_assert_eq!(
1738 original_methods.len(),
1739 cloned_methods.len(),
1740 "Cloned router should have same number of methods"
1741 );
1742
1743 for method in &expected_methods {
1744 prop_assert!(
1745 cloned_router.get_handler(method).is_some(),
1746 "Cloned router should have handler for method {:?}",
1747 method
1748 );
1749 }
1750
1751 for method in &cloned_methods {
1753 prop_assert!(
1754 cloned_router.get_handler(method).is_some(),
1755 "Handler for {:?} should be accessible after clone",
1756 method
1757 );
1758 }
1759 }
1760 }
1761
1762 proptest! {
1770 #![proptest_config(ProptestConfig::with_cases(100))]
1771
1772 #[test]
1777 fn prop_nested_routes_have_prefix(
1778 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1780 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1782 has_param in any::<bool>(),
1783 ) {
1784 async fn handler() -> &'static str { "handler" }
1785
1786 let prefix = format!("/{}", prefix_segments.join("/"));
1788
1789 let mut route_path = format!("/{}", route_segments.join("/"));
1791 if has_param {
1792 route_path.push_str("/{id}");
1793 }
1794
1795 let nested_router = Router::new().route(&route_path, get(handler));
1797 let app = Router::new().nest(&prefix, nested_router);
1798
1799 let expected_matchit_path = if has_param {
1801 format!("{}/{}/:id", prefix, route_segments.join("/"))
1802 } else {
1803 format!("{}/{}", prefix, route_segments.join("/"))
1804 };
1805
1806 let routes = app.registered_routes();
1807
1808 prop_assert!(
1810 routes.contains_key(&expected_matchit_path),
1811 "Expected route '{}' not found. Available routes: {:?}",
1812 expected_matchit_path,
1813 routes.keys().collect::<Vec<_>>()
1814 );
1815
1816 let route_info = routes.get(&expected_matchit_path).unwrap();
1818 let expected_display_path = format!("{}{}", prefix, route_path);
1819 prop_assert_eq!(
1820 &route_info.path, &expected_display_path,
1821 "Display path should be prefix + original path"
1822 );
1823 }
1824
1825 #[test]
1830 fn prop_route_count_preserved_after_nesting(
1831 num_routes in 1..4usize,
1833 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1834 ) {
1835 async fn handler() -> &'static str { "handler" }
1836
1837 let prefix = format!("/{}", prefix_segments.join("/"));
1838
1839 let mut nested_router = Router::new();
1841 for i in 0..num_routes {
1842 let path = format!("/route{}", i);
1843 nested_router = nested_router.route(&path, get(handler));
1844 }
1845
1846 let app = Router::new().nest(&prefix, nested_router);
1847
1848 prop_assert_eq!(
1849 app.registered_routes().len(),
1850 num_routes,
1851 "Number of routes should be preserved after nesting"
1852 );
1853 }
1854
1855 #[test]
1859 fn prop_nested_routes_are_matchable(
1860 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1861 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1862 ) {
1863 async fn handler() -> &'static str { "handler" }
1864
1865 let prefix = format!("/{}", prefix_segments.join("/"));
1866 let route_path = format!("/{}", route_segments.join("/"));
1867
1868 let nested_router = Router::new().route(&route_path, get(handler));
1869 let app = Router::new().nest(&prefix, nested_router);
1870
1871 let full_path = format!("{}{}", prefix, route_path);
1873
1874 match app.match_route(&full_path, &Method::GET) {
1876 RouteMatch::Found { .. } => {
1877 }
1879 RouteMatch::NotFound => {
1880 prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
1881 }
1882 RouteMatch::MethodNotAllowed { .. } => {
1883 prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
1884 }
1885 }
1886 }
1887 }
1888
1889 proptest! {
1896 #![proptest_config(ProptestConfig::with_cases(100))]
1897
1898 #[test]
1903 fn prop_state_type_ids_merged(
1904 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1905 has_nested_state in any::<bool>(),
1906 ) {
1907 #[derive(Clone)]
1908 struct TestState(i32);
1909
1910 async fn handler() -> &'static str { "handler" }
1911
1912 let prefix = format!("/{}", prefix_segments.join("/"));
1913
1914 let mut nested = Router::new().route("/test", get(handler));
1915 if has_nested_state {
1916 nested = nested.state(TestState(42));
1917 }
1918
1919 let parent = Router::new().nest(&prefix, nested);
1920
1921 if has_nested_state {
1923 prop_assert!(
1924 parent.state_type_ids().contains(&std::any::TypeId::of::<TestState>()),
1925 "Parent should track nested state type ID"
1926 );
1927 }
1928 }
1929
1930 #[test]
1935 fn prop_merge_state_adds_nested_state(
1936 state_value in any::<i32>(),
1937 ) {
1938 #[derive(Clone, PartialEq, Debug)]
1939 struct UniqueState(i32);
1940
1941 let source = Router::new().state(UniqueState(state_value));
1943
1944 let parent = Router::new().merge_state::<UniqueState>(&source);
1946
1947 prop_assert!(
1949 parent.has_state::<UniqueState>(),
1950 "Parent should have state after merge"
1951 );
1952
1953 let merged_state = parent.state.get::<UniqueState>().unwrap();
1955 prop_assert_eq!(
1956 merged_state.0, state_value,
1957 "Merged state value should match source"
1958 );
1959 }
1960 }
1961
1962 proptest! {
1969 #![proptest_config(ProptestConfig::with_cases(100))]
1970
1971 #[test]
1976 fn prop_parent_state_takes_precedence(
1977 parent_value in any::<i32>(),
1978 nested_value in any::<i32>(),
1979 ) {
1980 prop_assume!(parent_value != nested_value);
1982
1983 #[derive(Clone, PartialEq, Debug)]
1984 struct SharedState(i32);
1985
1986 let source = Router::new().state(SharedState(nested_value));
1988
1989 let parent = Router::new()
1991 .state(SharedState(parent_value))
1992 .merge_state::<SharedState>(&source);
1993
1994 prop_assert!(
1996 parent.has_state::<SharedState>(),
1997 "Parent should have state"
1998 );
1999
2000 let final_state = parent.state.get::<SharedState>().unwrap();
2002 prop_assert_eq!(
2003 final_state.0, parent_value,
2004 "Parent state value should be preserved, not overwritten by nested"
2005 );
2006 }
2007
2008 #[test]
2013 fn prop_state_precedence_consistent(
2014 parent_value in any::<i32>(),
2015 source1_value in any::<i32>(),
2016 source2_value in any::<i32>(),
2017 ) {
2018 #[derive(Clone, PartialEq, Debug)]
2019 struct ConsistentState(i32);
2020
2021 let source1 = Router::new().state(ConsistentState(source1_value));
2023 let source2 = Router::new().state(ConsistentState(source2_value));
2024
2025 let parent = Router::new()
2027 .state(ConsistentState(parent_value))
2028 .merge_state::<ConsistentState>(&source1)
2029 .merge_state::<ConsistentState>(&source2);
2030
2031 let final_state = parent.state.get::<ConsistentState>().unwrap();
2033 prop_assert_eq!(
2034 final_state.0, parent_value,
2035 "Parent state should be preserved after multiple merges"
2036 );
2037 }
2038 }
2039
2040 proptest! {
2048 #![proptest_config(ProptestConfig::with_cases(100))]
2049
2050 #[test]
2055 fn prop_same_structure_different_param_names_conflict(
2056 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
2058 param1 in "[a-z][a-z0-9]{0,5}",
2060 param2 in "[a-z][a-z0-9]{0,5}",
2061 ) {
2062 prop_assume!(param1 != param2);
2064
2065 let mut path1 = String::from("/");
2067 let mut path2 = String::from("/");
2068
2069 for segment in &segments {
2070 path1.push_str(segment);
2071 path1.push('/');
2072 path2.push_str(segment);
2073 path2.push('/');
2074 }
2075
2076 path1.push('{');
2077 path1.push_str(¶m1);
2078 path1.push('}');
2079
2080 path2.push('{');
2081 path2.push_str(¶m2);
2082 path2.push('}');
2083
2084 let result = catch_unwind(AssertUnwindSafe(|| {
2086 async fn handler1() -> &'static str { "handler1" }
2087 async fn handler2() -> &'static str { "handler2" }
2088
2089 let _router = Router::new()
2090 .route(&path1, get(handler1))
2091 .route(&path2, get(handler2));
2092 }));
2093
2094 prop_assert!(
2095 result.is_err(),
2096 "Routes '{}' and '{}' should conflict but didn't",
2097 path1, path2
2098 );
2099 }
2100
2101 #[test]
2106 fn prop_different_structures_no_conflict(
2107 segments1 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2109 segments2 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2110 has_param1 in any::<bool>(),
2112 has_param2 in any::<bool>(),
2113 ) {
2114 let mut path1 = String::from("/");
2116 let mut path2 = String::from("/");
2117
2118 for segment in &segments1 {
2119 path1.push_str(segment);
2120 path1.push('/');
2121 }
2122 path1.pop(); for segment in &segments2 {
2125 path2.push_str(segment);
2126 path2.push('/');
2127 }
2128 path2.pop(); if has_param1 {
2131 path1.push_str("/{id}");
2132 }
2133
2134 if has_param2 {
2135 path2.push_str("/{id}");
2136 }
2137
2138 let norm1 = normalize_path_for_comparison(&convert_path_params(&path1));
2140 let norm2 = normalize_path_for_comparison(&convert_path_params(&path2));
2141
2142 prop_assume!(norm1 != norm2);
2144
2145 let result = catch_unwind(AssertUnwindSafe(|| {
2147 async fn handler1() -> &'static str { "handler1" }
2148 async fn handler2() -> &'static str { "handler2" }
2149
2150 let router = Router::new()
2151 .route(&path1, get(handler1))
2152 .route(&path2, get(handler2));
2153
2154 router.registered_routes().len()
2155 }));
2156
2157 prop_assert!(
2158 result.is_ok(),
2159 "Routes '{}' and '{}' should not conflict but did",
2160 path1, path2
2161 );
2162
2163 if let Ok(count) = result {
2164 prop_assert_eq!(count, 2, "Should have registered 2 routes");
2165 }
2166 }
2167
2168 #[test]
2173 fn prop_conflict_error_contains_both_paths(
2174 segment in "[a-z][a-z0-9]{1,5}",
2176 param1 in "[a-z][a-z0-9]{1,5}",
2177 param2 in "[a-z][a-z0-9]{1,5}",
2178 ) {
2179 prop_assume!(param1 != param2);
2180
2181 let path1 = format!("/{}/{{{}}}", segment, param1);
2182 let path2 = format!("/{}/{{{}}}", segment, param2);
2183
2184 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
2193 prop_assert!(result.is_err(), "Should have panicked due to conflict");
2194
2195 if let Err(panic_info) = result {
2197 if let Some(msg) = panic_info.downcast_ref::<String>() {
2198 prop_assert!(
2199 msg.contains("ROUTE CONFLICT DETECTED"),
2200 "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
2201 msg
2202 );
2203 prop_assert!(
2204 msg.contains("Existing:") && msg.contains("New:"),
2205 "Error should contain both 'Existing:' and 'New:' labels, got: {}",
2206 msg
2207 );
2208 prop_assert!(
2209 msg.contains("How to resolve:"),
2210 "Error should contain resolution guidance, got: {}",
2211 msg
2212 );
2213 }
2214 }
2215 }
2216
2217 #[test]
2221 fn prop_exact_duplicate_paths_conflict(
2222 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
2224 has_param in any::<bool>(),
2225 ) {
2226 let mut path = String::from("/");
2228
2229 for segment in &segments {
2230 path.push_str(segment);
2231 path.push('/');
2232 }
2233 path.pop(); if has_param {
2236 path.push_str("/{id}");
2237 }
2238
2239 let result = catch_unwind(AssertUnwindSafe(|| {
2241 async fn handler1() -> &'static str { "handler1" }
2242 async fn handler2() -> &'static str { "handler2" }
2243
2244 let _router = Router::new()
2245 .route(&path, get(handler1))
2246 .route(&path, get(handler2));
2247 }));
2248
2249 prop_assert!(
2250 result.is_err(),
2251 "Registering path '{}' twice should conflict but didn't",
2252 path
2253 );
2254 }
2255 }
2256
2257 proptest! {
2264 #![proptest_config(ProptestConfig::with_cases(100))]
2265
2266 #[test]
2271 fn prop_nested_route_with_params_matches(
2272 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2273 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
2274 param_value in "[a-z0-9]{1,10}",
2275 ) {
2276 async fn handler() -> &'static str { "handler" }
2277
2278 let prefix = format!("/{}", prefix_segments.join("/"));
2279 let route_path = if route_segments.is_empty() {
2280 "/{id}".to_string()
2281 } else {
2282 format!("/{}/{{id}}", route_segments.join("/"))
2283 };
2284
2285 let nested_router = Router::new().route(&route_path, get(handler));
2286 let app = Router::new().nest(&prefix, nested_router);
2287
2288 let full_path = if route_segments.is_empty() {
2290 format!("{}/{}", prefix, param_value)
2291 } else {
2292 format!("{}/{}/{}", prefix, route_segments.join("/"), param_value)
2293 };
2294
2295 match app.match_route(&full_path, &Method::GET) {
2297 RouteMatch::Found { params, .. } => {
2298 prop_assert!(
2300 params.contains_key("id"),
2301 "Should have 'id' parameter, got: {:?}",
2302 params
2303 );
2304 prop_assert_eq!(
2305 params.get("id").unwrap(),
2306 ¶m_value,
2307 "Parameter value should match"
2308 );
2309 }
2310 RouteMatch::NotFound => {
2311 prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
2312 }
2313 RouteMatch::MethodNotAllowed { .. } => {
2314 prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
2315 }
2316 }
2317 }
2318
2319 #[test]
2324 fn prop_nested_route_matches_correct_method(
2325 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2326 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2327 use_get in any::<bool>(),
2328 ) {
2329 async fn handler() -> &'static str { "handler" }
2330
2331 let prefix = format!("/{}", prefix_segments.join("/"));
2332 let route_path = format!("/{}", route_segments.join("/"));
2333
2334 let method_router = if use_get { get(handler) } else { post(handler) };
2336 let nested_router = Router::new().route(&route_path, method_router);
2337 let app = Router::new().nest(&prefix, nested_router);
2338
2339 let full_path = format!("{}{}", prefix, route_path);
2340 let registered_method = if use_get { Method::GET } else { Method::POST };
2341 let other_method = if use_get { Method::POST } else { Method::GET };
2342
2343 match app.match_route(&full_path, ®istered_method) {
2345 RouteMatch::Found { .. } => {
2346 }
2348 other => {
2349 prop_assert!(false, "Route should be found for registered method, got: {:?}",
2350 match other {
2351 RouteMatch::NotFound => "NotFound",
2352 RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
2353 _ => "Found",
2354 }
2355 );
2356 }
2357 }
2358
2359 match app.match_route(&full_path, &other_method) {
2361 RouteMatch::MethodNotAllowed { allowed } => {
2362 prop_assert!(
2363 allowed.contains(®istered_method),
2364 "Allowed methods should contain {:?}",
2365 registered_method
2366 );
2367 }
2368 other => {
2369 prop_assert!(false, "Route should return MethodNotAllowed for other method, got: {:?}",
2370 match other {
2371 RouteMatch::NotFound => "NotFound",
2372 RouteMatch::Found { .. } => "Found",
2373 _ => "MethodNotAllowed",
2374 }
2375 );
2376 }
2377 }
2378 }
2379 }
2380
2381 proptest! {
2388 #![proptest_config(ProptestConfig::with_cases(100))]
2389
2390 #[test]
2395 fn prop_single_param_extraction(
2396 prefix in "[a-z][a-z0-9]{1,5}",
2397 param_name in "[a-z][a-z0-9]{1,5}",
2398 param_value in "[a-z0-9]{1,10}",
2399 ) {
2400 async fn handler() -> &'static str { "handler" }
2401
2402 let prefix = format!("/{}", prefix);
2403 let route_path = format!("/{{{}}}", param_name);
2404
2405 let nested_router = Router::new().route(&route_path, get(handler));
2406 let app = Router::new().nest(&prefix, nested_router);
2407
2408 let full_path = format!("{}/{}", prefix, param_value);
2409
2410 match app.match_route(&full_path, &Method::GET) {
2411 RouteMatch::Found { params, .. } => {
2412 prop_assert!(
2413 params.contains_key(¶m_name),
2414 "Should have '{}' parameter, got: {:?}",
2415 param_name, params
2416 );
2417 prop_assert_eq!(
2418 params.get(¶m_name).unwrap(),
2419 ¶m_value,
2420 "Parameter '{}' value should be '{}'",
2421 param_name, param_value
2422 );
2423 }
2424 _ => {
2425 prop_assert!(false, "Route should be found");
2426 }
2427 }
2428 }
2429
2430 #[test]
2435 fn prop_multiple_params_extraction(
2436 prefix in "[a-z][a-z0-9]{1,5}",
2437 param1_name in "[a-z]{1,5}",
2438 param1_value in "[a-z0-9]{1,10}",
2439 param2_name in "[a-z]{1,5}",
2440 param2_value in "[a-z0-9]{1,10}",
2441 ) {
2442 prop_assume!(param1_name != param2_name);
2444
2445 async fn handler() -> &'static str { "handler" }
2446
2447 let prefix = format!("/{}", prefix);
2448 let route_path = format!("/{{{}}}/items/{{{}}}", param1_name, param2_name);
2449
2450 let nested_router = Router::new().route(&route_path, get(handler));
2451 let app = Router::new().nest(&prefix, nested_router);
2452
2453 let full_path = format!("{}/{}/items/{}", prefix, param1_value, param2_value);
2454
2455 match app.match_route(&full_path, &Method::GET) {
2456 RouteMatch::Found { params, .. } => {
2457 prop_assert!(
2459 params.contains_key(¶m1_name),
2460 "Should have '{}' parameter, got: {:?}",
2461 param1_name, params
2462 );
2463 prop_assert_eq!(
2464 params.get(¶m1_name).unwrap(),
2465 ¶m1_value,
2466 "Parameter '{}' value should be '{}'",
2467 param1_name, param1_value
2468 );
2469
2470 prop_assert!(
2472 params.contains_key(¶m2_name),
2473 "Should have '{}' parameter, got: {:?}",
2474 param2_name, params
2475 );
2476 prop_assert_eq!(
2477 params.get(¶m2_name).unwrap(),
2478 ¶m2_value,
2479 "Parameter '{}' value should be '{}'",
2480 param2_name, param2_value
2481 );
2482 }
2483 _ => {
2484 prop_assert!(false, "Route should be found");
2485 }
2486 }
2487 }
2488
2489 #[test]
2494 fn prop_param_value_preservation(
2495 prefix in "[a-z]{1,5}",
2496 param_value in "[a-zA-Z0-9_-]{1,15}",
2498 ) {
2499 async fn handler() -> &'static str { "handler" }
2500
2501 let prefix = format!("/{}", prefix);
2502 let route_path = "/{id}".to_string();
2503
2504 let nested_router = Router::new().route(&route_path, get(handler));
2505 let app = Router::new().nest(&prefix, nested_router);
2506
2507 let full_path = format!("{}/{}", prefix, param_value);
2508
2509 match app.match_route(&full_path, &Method::GET) {
2510 RouteMatch::Found { params, .. } => {
2511 prop_assert_eq!(
2512 params.get("id").unwrap(),
2513 ¶m_value,
2514 "Parameter value should be preserved exactly"
2515 );
2516 }
2517 _ => {
2518 prop_assert!(false, "Route should be found");
2519 }
2520 }
2521 }
2522 }
2523
2524 proptest! {
2531 #![proptest_config(ProptestConfig::with_cases(100))]
2532
2533 #[test]
2538 fn prop_unregistered_path_returns_not_found(
2539 prefix in "[a-z][a-z0-9]{1,5}",
2540 route_segment in "[a-z][a-z0-9]{1,5}",
2541 unregistered_segment in "[a-z][a-z0-9]{6,10}",
2542 ) {
2543 prop_assume!(route_segment != unregistered_segment);
2545
2546 async fn handler() -> &'static str { "handler" }
2547
2548 let prefix = format!("/{}", prefix);
2549 let route_path = format!("/{}", route_segment);
2550
2551 let nested_router = Router::new().route(&route_path, get(handler));
2552 let app = Router::new().nest(&prefix, nested_router);
2553
2554 let unregistered_path = format!("{}/{}", prefix, unregistered_segment);
2556
2557 match app.match_route(&unregistered_path, &Method::GET) {
2558 RouteMatch::NotFound => {
2559 }
2561 RouteMatch::Found { .. } => {
2562 prop_assert!(false, "Path '{}' should not be found", unregistered_path);
2563 }
2564 RouteMatch::MethodNotAllowed { .. } => {
2565 prop_assert!(false, "Path '{}' should return NotFound, not MethodNotAllowed", unregistered_path);
2566 }
2567 }
2568 }
2569
2570 #[test]
2574 fn prop_wrong_prefix_returns_not_found(
2575 prefix1 in "[a-z][a-z0-9]{1,5}",
2576 prefix2 in "[a-z][a-z0-9]{6,10}",
2577 route_segment in "[a-z][a-z0-9]{1,5}",
2578 ) {
2579 prop_assume!(prefix1 != prefix2);
2581
2582 async fn handler() -> &'static str { "handler" }
2583
2584 let prefix = format!("/{}", prefix1);
2585 let route_path = format!("/{}", route_segment);
2586
2587 let nested_router = Router::new().route(&route_path, get(handler));
2588 let app = Router::new().nest(&prefix, nested_router);
2589
2590 let wrong_prefix_path = format!("/{}/{}", prefix2, route_segment);
2592
2593 match app.match_route(&wrong_prefix_path, &Method::GET) {
2594 RouteMatch::NotFound => {
2595 }
2597 _ => {
2598 prop_assert!(false, "Path '{}' with wrong prefix should return NotFound", wrong_prefix_path);
2599 }
2600 }
2601 }
2602
2603 #[test]
2608 fn prop_partial_path_returns_not_found(
2609 prefix in "[a-z][a-z0-9]{1,5}",
2610 segment1 in "[a-z][a-z0-9]{1,5}",
2611 segment2 in "[a-z][a-z0-9]{1,5}",
2612 ) {
2613 async fn handler() -> &'static str { "handler" }
2614
2615 let prefix = format!("/{}", prefix);
2616 let route_path = format!("/{}/{}", segment1, segment2);
2617
2618 let nested_router = Router::new().route(&route_path, get(handler));
2619 let app = Router::new().nest(&prefix, nested_router);
2620
2621 let partial_path = format!("{}/{}", prefix, segment1);
2623
2624 match app.match_route(&partial_path, &Method::GET) {
2625 RouteMatch::NotFound => {
2626 }
2628 _ => {
2629 prop_assert!(false, "Partial path '{}' should return NotFound", partial_path);
2630 }
2631 }
2632 }
2633 }
2634
2635 proptest! {
2642 #![proptest_config(ProptestConfig::with_cases(100))]
2643
2644 #[test]
2650 fn prop_unregistered_method_returns_method_not_allowed(
2651 prefix in "[a-z][a-z0-9]{1,5}",
2652 route_segment in "[a-z][a-z0-9]{1,5}",
2653 ) {
2654 async fn handler() -> &'static str { "handler" }
2655
2656 let prefix = format!("/{}", prefix);
2657 let route_path = format!("/{}", route_segment);
2658
2659 let nested_router = Router::new().route(&route_path, get(handler));
2661 let app = Router::new().nest(&prefix, nested_router);
2662
2663 let full_path = format!("{}{}", prefix, route_path);
2664
2665 match app.match_route(&full_path, &Method::POST) {
2667 RouteMatch::MethodNotAllowed { allowed } => {
2668 prop_assert!(
2669 allowed.contains(&Method::GET),
2670 "Allowed methods should contain GET, got: {:?}",
2671 allowed
2672 );
2673 prop_assert!(
2674 !allowed.contains(&Method::POST),
2675 "Allowed methods should not contain POST"
2676 );
2677 }
2678 RouteMatch::Found { .. } => {
2679 prop_assert!(false, "POST should not be found on GET-only route");
2680 }
2681 RouteMatch::NotFound => {
2682 prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
2683 }
2684 }
2685 }
2686
2687 #[test]
2692 fn prop_multiple_methods_in_allowed_list(
2693 prefix in "[a-z][a-z0-9]{1,5}",
2694 route_segment in "[a-z][a-z0-9]{1,5}",
2695 use_get in any::<bool>(),
2696 use_post in any::<bool>(),
2697 use_put in any::<bool>(),
2698 ) {
2699 prop_assume!(use_get || use_post || use_put);
2701
2702 async fn handler() -> &'static str { "handler" }
2703
2704 let prefix = format!("/{}", prefix);
2705 let route_path = format!("/{}", route_segment);
2706
2707 let mut method_router = MethodRouter::new();
2709 let mut expected_methods: Vec<Method> = Vec::new();
2710
2711 if use_get {
2712 let get_router = get(handler);
2713 for (method, h) in get_router.handlers {
2714 method_router.handlers.insert(method.clone(), h);
2715 expected_methods.push(method);
2716 }
2717 }
2718 if use_post {
2719 let post_router = post(handler);
2720 for (method, h) in post_router.handlers {
2721 method_router.handlers.insert(method.clone(), h);
2722 expected_methods.push(method);
2723 }
2724 }
2725 if use_put {
2726 let put_router = put(handler);
2727 for (method, h) in put_router.handlers {
2728 method_router.handlers.insert(method.clone(), h);
2729 expected_methods.push(method);
2730 }
2731 }
2732
2733 let nested_router = Router::new().route(&route_path, method_router);
2734 let app = Router::new().nest(&prefix, nested_router);
2735
2736 let full_path = format!("{}{}", prefix, route_path);
2737
2738 match app.match_route(&full_path, &Method::DELETE) {
2740 RouteMatch::MethodNotAllowed { allowed } => {
2741 for method in &expected_methods {
2743 prop_assert!(
2744 allowed.contains(method),
2745 "Allowed methods should contain {:?}, got: {:?}",
2746 method, allowed
2747 );
2748 }
2749 prop_assert!(
2751 !allowed.contains(&Method::DELETE),
2752 "Allowed methods should not contain DELETE"
2753 );
2754 }
2755 RouteMatch::Found { .. } => {
2756 prop_assert!(false, "DELETE should not be found");
2757 }
2758 RouteMatch::NotFound => {
2759 prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
2760 }
2761 }
2762 }
2763 }
2764
2765 proptest! {
2779 #![proptest_config(ProptestConfig::with_cases(100))]
2780
2781 #[test]
2787 fn prop_multiple_routers_all_routes_registered(
2788 prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2790 prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2791 num_routes1 in 1..4usize,
2793 num_routes2 in 1..4usize,
2794 ) {
2795 let prefix1 = format!("/{}", prefix1_segments.join("/"));
2797 let prefix2 = format!("/{}", prefix2_segments.join("/"));
2798
2799 prop_assume!(prefix1 != prefix2);
2801
2802 async fn handler() -> &'static str { "handler" }
2803
2804 let mut router1 = Router::new();
2806 for i in 0..num_routes1 {
2807 let path = format!("/route1_{}", i);
2808 router1 = router1.route(&path, get(handler));
2809 }
2810
2811 let mut router2 = Router::new();
2813 for i in 0..num_routes2 {
2814 let path = format!("/route2_{}", i);
2815 router2 = router2.route(&path, get(handler));
2816 }
2817
2818 let app = Router::new()
2820 .nest(&prefix1, router1)
2821 .nest(&prefix2, router2);
2822
2823 let routes = app.registered_routes();
2824
2825 let expected_count = num_routes1 + num_routes2;
2827 prop_assert_eq!(
2828 routes.len(),
2829 expected_count,
2830 "Should have {} routes ({}+{}), got {}",
2831 expected_count, num_routes1, num_routes2, routes.len()
2832 );
2833
2834 for i in 0..num_routes1 {
2836 let expected_path = format!("{}/route1_{}", prefix1, i);
2837 let matchit_path = convert_path_params(&expected_path);
2838 prop_assert!(
2839 routes.contains_key(&matchit_path),
2840 "Route '{}' should be registered",
2841 expected_path
2842 );
2843 }
2844
2845 for i in 0..num_routes2 {
2847 let expected_path = format!("{}/route2_{}", prefix2, i);
2848 let matchit_path = convert_path_params(&expected_path);
2849 prop_assert!(
2850 routes.contains_key(&matchit_path),
2851 "Route '{}' should be registered",
2852 expected_path
2853 );
2854 }
2855 }
2856
2857 #[test]
2862 fn prop_multiple_routers_no_interference(
2863 prefix1 in "[a-z][a-z0-9]{1,5}",
2864 prefix2 in "[a-z][a-z0-9]{1,5}",
2865 route_segment in "[a-z][a-z0-9]{1,5}",
2866 param_value1 in "[a-z0-9]{1,10}",
2867 param_value2 in "[a-z0-9]{1,10}",
2868 ) {
2869 prop_assume!(prefix1 != prefix2);
2871
2872 let prefix1 = format!("/{}", prefix1);
2873 let prefix2 = format!("/{}", prefix2);
2874
2875 async fn handler() -> &'static str { "handler" }
2876
2877 let router1 = Router::new()
2879 .route(&format!("/{}", route_segment), get(handler))
2880 .route("/{id}", get(handler));
2881
2882 let router2 = Router::new()
2883 .route(&format!("/{}", route_segment), get(handler))
2884 .route("/{id}", get(handler));
2885
2886 let app = Router::new()
2888 .nest(&prefix1, router1)
2889 .nest(&prefix2, router2);
2890
2891 let path1_static = format!("{}/{}", prefix1, route_segment);
2893 match app.match_route(&path1_static, &Method::GET) {
2894 RouteMatch::Found { params, .. } => {
2895 prop_assert!(params.is_empty(), "Static path should have no params");
2896 }
2897 _ => {
2898 prop_assert!(false, "Route '{}' should be found", path1_static);
2899 }
2900 }
2901
2902 let path1_param = format!("{}/{}", prefix1, param_value1);
2903 match app.match_route(&path1_param, &Method::GET) {
2904 RouteMatch::Found { params, .. } => {
2905 prop_assert_eq!(
2906 params.get("id"),
2907 Some(¶m_value1.to_string()),
2908 "Parameter should be extracted correctly"
2909 );
2910 }
2911 _ => {
2912 prop_assert!(false, "Route '{}' should be found", path1_param);
2913 }
2914 }
2915
2916 let path2_static = format!("{}/{}", prefix2, route_segment);
2918 match app.match_route(&path2_static, &Method::GET) {
2919 RouteMatch::Found { params, .. } => {
2920 prop_assert!(params.is_empty(), "Static path should have no params");
2921 }
2922 _ => {
2923 prop_assert!(false, "Route '{}' should be found", path2_static);
2924 }
2925 }
2926
2927 let path2_param = format!("{}/{}", prefix2, param_value2);
2928 match app.match_route(&path2_param, &Method::GET) {
2929 RouteMatch::Found { params, .. } => {
2930 prop_assert_eq!(
2931 params.get("id"),
2932 Some(¶m_value2.to_string()),
2933 "Parameter should be extracted correctly"
2934 );
2935 }
2936 _ => {
2937 prop_assert!(false, "Route '{}' should be found", path2_param);
2938 }
2939 }
2940 }
2941
2942 #[test]
2947 fn prop_multiple_routers_preserve_methods(
2948 prefix1 in "[a-z][a-z0-9]{1,5}",
2949 prefix2 in "[a-z][a-z0-9]{1,5}",
2950 route_segment in "[a-z][a-z0-9]{1,5}",
2951 router1_use_get in any::<bool>(),
2952 router1_use_post in any::<bool>(),
2953 router2_use_get in any::<bool>(),
2954 router2_use_put in any::<bool>(),
2955 ) {
2956 prop_assume!(router1_use_get || router1_use_post);
2958 prop_assume!(router2_use_get || router2_use_put);
2959 prop_assume!(prefix1 != prefix2);
2961
2962 let prefix1 = format!("/{}", prefix1);
2963 let prefix2 = format!("/{}", prefix2);
2964 let route_path = format!("/{}", route_segment);
2965
2966 async fn handler() -> &'static str { "handler" }
2967
2968 let mut method_router1 = MethodRouter::new();
2970 let mut expected_methods1: Vec<Method> = Vec::new();
2971 if router1_use_get {
2972 let get_router = get(handler);
2973 for (method, h) in get_router.handlers {
2974 method_router1.handlers.insert(method.clone(), h);
2975 expected_methods1.push(method);
2976 }
2977 }
2978 if router1_use_post {
2979 let post_router = post(handler);
2980 for (method, h) in post_router.handlers {
2981 method_router1.handlers.insert(method.clone(), h);
2982 expected_methods1.push(method);
2983 }
2984 }
2985
2986 let mut method_router2 = MethodRouter::new();
2988 let mut expected_methods2: Vec<Method> = Vec::new();
2989 if router2_use_get {
2990 let get_router = get(handler);
2991 for (method, h) in get_router.handlers {
2992 method_router2.handlers.insert(method.clone(), h);
2993 expected_methods2.push(method);
2994 }
2995 }
2996 if router2_use_put {
2997 let put_router = put(handler);
2998 for (method, h) in put_router.handlers {
2999 method_router2.handlers.insert(method.clone(), h);
3000 expected_methods2.push(method);
3001 }
3002 }
3003
3004 let router1 = Router::new().route(&route_path, method_router1);
3005 let router2 = Router::new().route(&route_path, method_router2);
3006
3007 let app = Router::new()
3008 .nest(&prefix1, router1)
3009 .nest(&prefix2, router2);
3010
3011 let full_path1 = format!("{}{}", prefix1, route_path);
3012 let full_path2 = format!("{}{}", prefix2, route_path);
3013
3014 for method in &expected_methods1 {
3016 match app.match_route(&full_path1, method) {
3017 RouteMatch::Found { .. } => {}
3018 _ => {
3019 prop_assert!(false, "Method {:?} should be found for {}", method, full_path1);
3020 }
3021 }
3022 }
3023
3024 for method in &expected_methods2 {
3026 match app.match_route(&full_path2, method) {
3027 RouteMatch::Found { .. } => {}
3028 _ => {
3029 prop_assert!(false, "Method {:?} should be found for {}", method, full_path2);
3030 }
3031 }
3032 }
3033
3034 if !expected_methods1.contains(&Method::DELETE) {
3036 match app.match_route(&full_path1, &Method::DELETE) {
3037 RouteMatch::MethodNotAllowed { allowed } => {
3038 for method in &expected_methods1 {
3039 prop_assert!(
3040 allowed.contains(method),
3041 "Allowed methods for {} should contain {:?}",
3042 full_path1, method
3043 );
3044 }
3045 }
3046 _ => {
3047 prop_assert!(false, "DELETE should return MethodNotAllowed for {}", full_path1);
3048 }
3049 }
3050 }
3051 }
3052
3053 #[test]
3058 fn prop_three_routers_composition(
3059 prefix1 in "[a-z]{1,3}",
3060 prefix2 in "[a-z]{4,6}",
3061 prefix3 in "[a-z]{7,9}",
3062 num_routes in 1..3usize,
3063 ) {
3064 let prefix1 = format!("/{}", prefix1);
3065 let prefix2 = format!("/{}", prefix2);
3066 let prefix3 = format!("/{}", prefix3);
3067
3068 async fn handler() -> &'static str { "handler" }
3069
3070 let mut router1 = Router::new();
3072 let mut router2 = Router::new();
3073 let mut router3 = Router::new();
3074
3075 for i in 0..num_routes {
3076 let path = format!("/item{}", i);
3077 router1 = router1.route(&path, get(handler));
3078 router2 = router2.route(&path, get(handler));
3079 router3 = router3.route(&path, get(handler));
3080 }
3081
3082 let app = Router::new()
3084 .nest(&prefix1, router1)
3085 .nest(&prefix2, router2)
3086 .nest(&prefix3, router3);
3087
3088 let routes = app.registered_routes();
3089
3090 let expected_count = 3 * num_routes;
3092 prop_assert_eq!(
3093 routes.len(),
3094 expected_count,
3095 "Should have {} routes, got {}",
3096 expected_count, routes.len()
3097 );
3098
3099 for i in 0..num_routes {
3101 let path1 = format!("{}/item{}", prefix1, i);
3102 let path2 = format!("{}/item{}", prefix2, i);
3103 let path3 = format!("{}/item{}", prefix3, i);
3104
3105 match app.match_route(&path1, &Method::GET) {
3106 RouteMatch::Found { .. } => {}
3107 _ => prop_assert!(false, "Route '{}' should be found", path1),
3108 }
3109 match app.match_route(&path2, &Method::GET) {
3110 RouteMatch::Found { .. } => {}
3111 _ => prop_assert!(false, "Route '{}' should be found", path2),
3112 }
3113 match app.match_route(&path3, &Method::GET) {
3114 RouteMatch::Found { .. } => {}
3115 _ => prop_assert!(false, "Route '{}' should be found", path3),
3116 }
3117 }
3118 }
3119 }
3120 proptest! {
3121 #![proptest_config(ProptestConfig::with_cases(100))]
3122
3123 #[test]
3129 fn prop_nested_route_conflict_different_param_names(
3130 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3131 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
3132 param1 in "[a-z][a-z0-9]{1,5}",
3133 param2 in "[a-z][a-z0-9]{1,5}",
3134 ) {
3135 prop_assume!(param1 != param2);
3137
3138 async fn handler1() -> &'static str { "handler1" }
3139 async fn handler2() -> &'static str { "handler2" }
3140
3141 let prefix = format!("/{}", prefix_segments.join("/"));
3142
3143 let existing_path = if route_segments.is_empty() {
3145 format!("{}/{{{}}}", prefix, param1)
3146 } else {
3147 format!("{}/{}/{{{}}}", prefix, route_segments.join("/"), param1)
3148 };
3149
3150 let nested_path = if route_segments.is_empty() {
3152 format!("/{{{}}}", param2)
3153 } else {
3154 format!("/{}/{{{}}}", route_segments.join("/"), param2)
3155 };
3156
3157 let result = catch_unwind(AssertUnwindSafe(|| {
3159 let parent = Router::new().route(&existing_path, get(handler1));
3160 let nested = Router::new().route(&nested_path, get(handler2));
3161 let _app = parent.nest(&prefix, nested);
3162 }));
3163
3164 prop_assert!(
3166 result.is_err(),
3167 "Nested route '{}{}' should conflict with existing route '{}' but didn't",
3168 prefix, nested_path, existing_path
3169 );
3170
3171 if let Err(panic_info) = result {
3173 if let Some(msg) = panic_info.downcast_ref::<String>() {
3174 prop_assert!(
3175 msg.contains("ROUTE CONFLICT DETECTED"),
3176 "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
3177 msg
3178 );
3179 prop_assert!(
3180 msg.contains("Existing:") && msg.contains("New:"),
3181 "Error should contain both 'Existing:' and 'New:' labels, got: {}",
3182 msg
3183 );
3184 }
3185 }
3186 }
3187
3188 #[test]
3193 fn prop_nested_route_conflict_exact_same_path(
3194 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3195 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3196 ) {
3197 async fn handler1() -> &'static str { "handler1" }
3198 async fn handler2() -> &'static str { "handler2" }
3199
3200 let prefix = format!("/{}", prefix_segments.join("/"));
3201 let route_path = format!("/{}", route_segments.join("/"));
3202
3203 let existing_path = format!("{}{}", prefix, route_path);
3205
3206 let result = catch_unwind(AssertUnwindSafe(|| {
3208 let parent = Router::new().route(&existing_path, get(handler1));
3209 let nested = Router::new().route(&route_path, get(handler2));
3210 let _app = parent.nest(&prefix, nested);
3211 }));
3212
3213 prop_assert!(
3215 result.is_err(),
3216 "Nested route '{}{}' should conflict with existing route '{}' but didn't",
3217 prefix, route_path, existing_path
3218 );
3219 }
3220
3221 #[test]
3226 fn prop_nested_routes_different_prefixes_no_conflict(
3227 prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3228 prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3229 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3230 has_param in any::<bool>(),
3231 ) {
3232 let prefix1 = format!("/{}", prefix1_segments.join("/"));
3234 let prefix2 = format!("/{}", prefix2_segments.join("/"));
3235
3236 prop_assume!(prefix1 != prefix2);
3238
3239 async fn handler1() -> &'static str { "handler1" }
3240 async fn handler2() -> &'static str { "handler2" }
3241
3242 let route_path = if has_param {
3244 format!("/{}/{{id}}", route_segments.join("/"))
3245 } else {
3246 format!("/{}", route_segments.join("/"))
3247 };
3248
3249 let result = catch_unwind(AssertUnwindSafe(|| {
3251 let nested1 = Router::new().route(&route_path, get(handler1));
3252 let nested2 = Router::new().route(&route_path, get(handler2));
3253
3254 let app = Router::new()
3255 .nest(&prefix1, nested1)
3256 .nest(&prefix2, nested2);
3257
3258 app.registered_routes().len()
3259 }));
3260
3261 prop_assert!(
3263 result.is_ok(),
3264 "Routes under different prefixes '{}' and '{}' should not conflict",
3265 prefix1, prefix2
3266 );
3267
3268 if let Ok(count) = result {
3269 prop_assert_eq!(count, 2, "Should have registered 2 routes");
3270 }
3271 }
3272
3273 #[test]
3278 fn prop_nested_conflict_error_contains_guidance(
3279 prefix in "[a-z][a-z0-9]{1,5}",
3280 segment in "[a-z][a-z0-9]{1,5}",
3281 param1 in "[a-z][a-z0-9]{1,5}",
3282 param2 in "[a-z][a-z0-9]{1,5}",
3283 ) {
3284 prop_assume!(param1 != param2);
3285
3286 async fn handler1() -> &'static str { "handler1" }
3287 async fn handler2() -> &'static str { "handler2" }
3288
3289 let prefix = format!("/{}", prefix);
3290 let existing_path = format!("{}/{}/{{{}}}", prefix, segment, param1);
3291 let nested_path = format!("/{}/{{{}}}", segment, param2);
3292
3293 let result = catch_unwind(AssertUnwindSafe(|| {
3294 let parent = Router::new().route(&existing_path, get(handler1));
3295 let nested = Router::new().route(&nested_path, get(handler2));
3296 let _app = parent.nest(&prefix, nested);
3297 }));
3298
3299 prop_assert!(result.is_err(), "Should have detected conflict");
3300
3301 if let Err(panic_info) = result {
3302 if let Some(msg) = panic_info.downcast_ref::<String>() {
3303 prop_assert!(
3304 msg.contains("How to resolve:"),
3305 "Error should contain 'How to resolve:' guidance, got: {}",
3306 msg
3307 );
3308 prop_assert!(
3309 msg.contains("Use different path patterns") ||
3310 msg.contains("different path patterns"),
3311 "Error should suggest using different path patterns, got: {}",
3312 msg
3313 );
3314 }
3315 }
3316 }
3317 }
3318}