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 {
463 let normalized_prefix = normalize_prefix(prefix);
465
466 for type_id in &router.state_type_ids {
470 if !self.state_type_ids.contains(type_id) {
471 self.state_type_ids.push(*type_id);
472 }
473 }
474
475 let nested_routes: Vec<(String, RouteInfo, MethodRouter)> = router
478 .registered_routes
479 .into_iter()
480 .filter_map(|(matchit_path, route_info)| {
481 router
482 .method_routers
483 .get(&matchit_path)
484 .map(|mr| (matchit_path, route_info, mr.clone()))
485 })
486 .collect();
487
488 for (matchit_path, route_info, method_router) in nested_routes {
490 let prefixed_matchit_path = if matchit_path == "/" {
494 normalized_prefix.clone()
495 } else {
496 format!("{}{}", normalized_prefix, matchit_path)
497 };
498
499 let prefixed_display_path = if route_info.path == "/" {
500 normalized_prefix.clone()
501 } else {
502 format!("{}{}", normalized_prefix, route_info.path)
503 };
504
505 self.method_routers
507 .insert(prefixed_matchit_path.clone(), method_router.clone());
508
509 match self
511 .inner
512 .insert(prefixed_matchit_path.clone(), method_router)
513 {
514 Ok(_) => {
515 self.registered_routes.insert(
517 prefixed_matchit_path,
518 RouteInfo {
519 path: prefixed_display_path,
520 methods: route_info.methods,
521 },
522 );
523 }
524 Err(e) => {
525 self.method_routers.remove(&prefixed_matchit_path);
527
528 let existing_path = self
530 .find_conflicting_route(&prefixed_matchit_path)
531 .map(|info| info.path.clone())
532 .unwrap_or_else(|| "<unknown>".to_string());
533
534 let conflict_error = RouteConflictError {
535 new_path: prefixed_display_path,
536 method: route_info.methods.first().cloned(),
537 existing_path,
538 details: e.to_string(),
539 };
540
541 panic!("{}", conflict_error);
542 }
543 }
544 }
545
546 self
547 }
548
549 pub fn merge_state<S: Clone + Send + Sync + 'static>(mut self, other: &Router) -> Self {
566 let type_id = std::any::TypeId::of::<S>();
567
568 if !self.state_type_ids.contains(&type_id) {
570 if let Some(state) = other.state.get::<S>() {
572 let extensions = Arc::make_mut(&mut self.state);
573 extensions.insert(state.clone());
574 self.state_type_ids.push(type_id);
575 }
576 }
577
578 self
579 }
580
581 pub fn match_route(&self, path: &str, method: &Method) -> RouteMatch<'_> {
583 match self.inner.at(path) {
584 Ok(matched) => {
585 let method_router = matched.value;
586
587 if let Some(handler) = method_router.get_handler(method) {
588 let params: PathParams = matched
590 .params
591 .iter()
592 .map(|(k, v)| (k.to_string(), v.to_string()))
593 .collect();
594
595 RouteMatch::Found { handler, params }
596 } else {
597 RouteMatch::MethodNotAllowed {
598 allowed: method_router.allowed_methods(),
599 }
600 }
601 }
602 Err(_) => RouteMatch::NotFound,
603 }
604 }
605
606 pub fn state_ref(&self) -> Arc<Extensions> {
608 self.state.clone()
609 }
610
611 pub fn registered_routes(&self) -> &HashMap<String, RouteInfo> {
613 &self.registered_routes
614 }
615
616 pub fn method_routers(&self) -> &HashMap<String, MethodRouter> {
618 &self.method_routers
619 }
620}
621
622impl Default for Router {
623 fn default() -> Self {
624 Self::new()
625 }
626}
627
628pub enum RouteMatch<'a> {
630 Found {
631 handler: &'a BoxedHandler,
632 params: PathParams,
633 },
634 NotFound,
635 MethodNotAllowed {
636 allowed: Vec<Method>,
637 },
638}
639
640fn convert_path_params(path: &str) -> String {
642 let mut result = String::with_capacity(path.len());
643
644 for ch in path.chars() {
645 match ch {
646 '{' => {
647 result.push(':');
648 }
649 '}' => {
650 }
652 _ => {
653 result.push(ch);
654 }
655 }
656 }
657
658 result
659}
660
661fn normalize_path_for_comparison(path: &str) -> String {
663 let mut result = String::with_capacity(path.len());
664 let mut in_param = false;
665
666 for ch in path.chars() {
667 match ch {
668 ':' => {
669 in_param = true;
670 result.push_str(":_");
671 }
672 '/' => {
673 in_param = false;
674 result.push('/');
675 }
676 _ if in_param => {
677 }
679 _ => {
680 result.push(ch);
681 }
682 }
683 }
684
685 result
686}
687
688pub(crate) fn normalize_prefix(prefix: &str) -> String {
705 if prefix.is_empty() {
707 return "/".to_string();
708 }
709
710 let segments: Vec<&str> = prefix.split('/').filter(|s| !s.is_empty()).collect();
712
713 if segments.is_empty() {
715 return "/".to_string();
716 }
717
718 let mut result = String::with_capacity(prefix.len() + 1);
720 for segment in segments {
721 result.push('/');
722 result.push_str(segment);
723 }
724
725 result
726}
727
728#[cfg(test)]
729mod tests {
730 use super::*;
731
732 #[test]
733 fn test_convert_path_params() {
734 assert_eq!(convert_path_params("/users/{id}"), "/users/:id");
735 assert_eq!(
736 convert_path_params("/users/{user_id}/posts/{post_id}"),
737 "/users/:user_id/posts/:post_id"
738 );
739 assert_eq!(convert_path_params("/static/path"), "/static/path");
740 }
741
742 #[test]
743 fn test_normalize_path_for_comparison() {
744 assert_eq!(normalize_path_for_comparison("/users/:id"), "/users/:_");
745 assert_eq!(
746 normalize_path_for_comparison("/users/:user_id"),
747 "/users/:_"
748 );
749 assert_eq!(
750 normalize_path_for_comparison("/users/:id/posts/:post_id"),
751 "/users/:_/posts/:_"
752 );
753 assert_eq!(
754 normalize_path_for_comparison("/static/path"),
755 "/static/path"
756 );
757 }
758
759 #[test]
760 fn test_normalize_prefix() {
761 assert_eq!(normalize_prefix("api"), "/api");
763 assert_eq!(normalize_prefix("/api"), "/api");
764 assert_eq!(normalize_prefix("/api/"), "/api");
765 assert_eq!(normalize_prefix("api/"), "/api");
766
767 assert_eq!(normalize_prefix("api/v1"), "/api/v1");
769 assert_eq!(normalize_prefix("/api/v1"), "/api/v1");
770 assert_eq!(normalize_prefix("/api/v1/"), "/api/v1");
771
772 assert_eq!(normalize_prefix(""), "/");
774 assert_eq!(normalize_prefix("/"), "/");
775
776 assert_eq!(normalize_prefix("//api"), "/api");
778 assert_eq!(normalize_prefix("api//v1"), "/api/v1");
779 assert_eq!(normalize_prefix("//api//v1//"), "/api/v1");
780 assert_eq!(normalize_prefix("///"), "/");
781 }
782
783 #[test]
784 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
785 fn test_route_conflict_detection() {
786 async fn handler1() -> &'static str {
787 "handler1"
788 }
789 async fn handler2() -> &'static str {
790 "handler2"
791 }
792
793 let _router = Router::new()
794 .route("/users/{id}", get(handler1))
795 .route("/users/{user_id}", get(handler2)); }
797
798 #[test]
799 fn test_no_conflict_different_paths() {
800 async fn handler1() -> &'static str {
801 "handler1"
802 }
803 async fn handler2() -> &'static str {
804 "handler2"
805 }
806
807 let router = Router::new()
808 .route("/users/{id}", get(handler1))
809 .route("/users/{id}/profile", get(handler2));
810
811 assert_eq!(router.registered_routes().len(), 2);
812 }
813
814 #[test]
815 fn test_route_info_tracking() {
816 async fn handler() -> &'static str {
817 "handler"
818 }
819
820 let router = Router::new().route("/users/{id}", get(handler));
821
822 let routes = router.registered_routes();
823 assert_eq!(routes.len(), 1);
824
825 let info = routes.get("/users/:id").unwrap();
826 assert_eq!(info.path, "/users/{id}");
827 assert_eq!(info.methods.len(), 1);
828 assert_eq!(info.methods[0], Method::GET);
829 }
830
831 #[test]
832 fn test_basic_router_nesting() {
833 async fn list_users() -> &'static str {
834 "list users"
835 }
836 async fn get_user() -> &'static str {
837 "get user"
838 }
839
840 let users_router = Router::new()
841 .route("/", get(list_users))
842 .route("/{id}", get(get_user));
843
844 let app = Router::new().nest("/api/users", users_router);
845
846 let routes = app.registered_routes();
847 assert_eq!(routes.len(), 2);
848
849 assert!(routes.contains_key("/api/users"));
851 assert!(routes.contains_key("/api/users/:id"));
852
853 let list_info = routes.get("/api/users").unwrap();
855 assert_eq!(list_info.path, "/api/users");
856
857 let get_info = routes.get("/api/users/:id").unwrap();
858 assert_eq!(get_info.path, "/api/users/{id}");
859 }
860
861 #[test]
862 fn test_nested_route_matching() {
863 async fn handler() -> &'static str {
864 "handler"
865 }
866
867 let users_router = Router::new().route("/{id}", get(handler));
868
869 let app = Router::new().nest("/api/users", users_router);
870
871 match app.match_route("/api/users/123", &Method::GET) {
873 RouteMatch::Found { params, .. } => {
874 assert_eq!(params.get("id"), Some(&"123".to_string()));
875 }
876 _ => panic!("Route should be found"),
877 }
878 }
879
880 #[test]
881 fn test_nested_route_matching_multiple_params() {
882 async fn handler() -> &'static str {
883 "handler"
884 }
885
886 let posts_router = Router::new().route("/{user_id}/posts/{post_id}", get(handler));
887
888 let app = Router::new().nest("/api", posts_router);
889
890 match app.match_route("/api/42/posts/100", &Method::GET) {
892 RouteMatch::Found { params, .. } => {
893 assert_eq!(params.get("user_id"), Some(&"42".to_string()));
894 assert_eq!(params.get("post_id"), Some(&"100".to_string()));
895 }
896 _ => panic!("Route should be found"),
897 }
898 }
899
900 #[test]
901 fn test_nested_route_matching_static_path() {
902 async fn handler() -> &'static str {
903 "handler"
904 }
905
906 let health_router = Router::new().route("/health", get(handler));
907
908 let app = Router::new().nest("/api/v1", health_router);
909
910 match app.match_route("/api/v1/health", &Method::GET) {
912 RouteMatch::Found { params, .. } => {
913 assert!(params.is_empty(), "Static path should have no params");
914 }
915 _ => panic!("Route should be found"),
916 }
917 }
918
919 #[test]
920 fn test_nested_route_not_found() {
921 async fn handler() -> &'static str {
922 "handler"
923 }
924
925 let users_router = Router::new().route("/users", get(handler));
926
927 let app = Router::new().nest("/api", users_router);
928
929 match app.match_route("/api/posts", &Method::GET) {
931 RouteMatch::NotFound => {
932 }
934 _ => panic!("Route should not be found"),
935 }
936
937 match app.match_route("/v2/users", &Method::GET) {
939 RouteMatch::NotFound => {
940 }
942 _ => panic!("Route with wrong prefix should not be found"),
943 }
944 }
945
946 #[test]
947 fn test_nested_route_method_not_allowed() {
948 async fn handler() -> &'static str {
949 "handler"
950 }
951
952 let users_router = Router::new().route("/users", get(handler));
953
954 let app = Router::new().nest("/api", users_router);
955
956 match app.match_route("/api/users", &Method::POST) {
958 RouteMatch::MethodNotAllowed { allowed } => {
959 assert!(allowed.contains(&Method::GET));
960 assert!(!allowed.contains(&Method::POST));
961 }
962 _ => panic!("Should return MethodNotAllowed"),
963 }
964 }
965
966 #[test]
967 fn test_nested_route_multiple_methods() {
968 async fn get_handler() -> &'static str {
969 "get"
970 }
971 async fn post_handler() -> &'static str {
972 "post"
973 }
974
975 let get_router = get(get_handler);
977 let post_router = post(post_handler);
978 let mut combined = MethodRouter::new();
979 for (method, handler) in get_router.handlers {
980 combined.handlers.insert(method, handler);
981 }
982 for (method, handler) in post_router.handlers {
983 combined.handlers.insert(method, handler);
984 }
985
986 let users_router = Router::new().route("/users", combined);
987 let app = Router::new().nest("/api", users_router);
988
989 match app.match_route("/api/users", &Method::GET) {
991 RouteMatch::Found { .. } => {}
992 _ => panic!("GET should be found"),
993 }
994
995 match app.match_route("/api/users", &Method::POST) {
996 RouteMatch::Found { .. } => {}
997 _ => panic!("POST should be found"),
998 }
999
1000 match app.match_route("/api/users", &Method::DELETE) {
1002 RouteMatch::MethodNotAllowed { allowed } => {
1003 assert!(allowed.contains(&Method::GET));
1004 assert!(allowed.contains(&Method::POST));
1005 }
1006 _ => panic!("DELETE should return MethodNotAllowed"),
1007 }
1008 }
1009
1010 #[test]
1011 fn test_nested_router_prefix_normalization() {
1012 async fn handler() -> &'static str {
1013 "handler"
1014 }
1015
1016 let router1 = Router::new().route("/test", get(handler));
1018 let app1 = Router::new().nest("api", router1);
1019 assert!(app1.registered_routes().contains_key("/api/test"));
1020
1021 let router2 = Router::new().route("/test", get(handler));
1022 let app2 = Router::new().nest("/api/", router2);
1023 assert!(app2.registered_routes().contains_key("/api/test"));
1024
1025 let router3 = Router::new().route("/test", get(handler));
1026 let app3 = Router::new().nest("//api//", router3);
1027 assert!(app3.registered_routes().contains_key("/api/test"));
1028 }
1029
1030 #[test]
1031 fn test_state_tracking() {
1032 #[derive(Clone)]
1033 struct MyState(#[allow(dead_code)] String);
1034
1035 let router = Router::new().state(MyState("test".to_string()));
1036
1037 assert!(router.has_state::<MyState>());
1038 assert!(!router.has_state::<String>());
1039 }
1040
1041 #[test]
1042 fn test_state_merge_nested_only() {
1043 #[derive(Clone, PartialEq, Debug)]
1044 struct NestedState(String);
1045
1046 async fn handler() -> &'static str {
1047 "handler"
1048 }
1049
1050 let state_source = Router::new().state(NestedState("nested".to_string()));
1052
1053 let nested = Router::new().route("/test", get(handler));
1054
1055 let parent = Router::new()
1056 .nest("/api", nested)
1057 .merge_state::<NestedState>(&state_source);
1058
1059 assert!(parent.has_state::<NestedState>());
1061
1062 let state = parent.state.get::<NestedState>().unwrap();
1064 assert_eq!(state.0, "nested");
1065 }
1066
1067 #[test]
1068 fn test_state_merge_parent_wins() {
1069 #[derive(Clone, PartialEq, Debug)]
1070 struct SharedState(String);
1071
1072 async fn handler() -> &'static str {
1073 "handler"
1074 }
1075
1076 let state_source = Router::new().state(SharedState("nested".to_string()));
1078
1079 let nested = Router::new().route("/test", get(handler));
1080
1081 let parent = Router::new()
1082 .state(SharedState("parent".to_string()))
1083 .nest("/api", nested)
1084 .merge_state::<SharedState>(&state_source);
1085
1086 assert!(parent.has_state::<SharedState>());
1088
1089 let state = parent.state.get::<SharedState>().unwrap();
1091 assert_eq!(state.0, "parent");
1092 }
1093
1094 #[test]
1095 fn test_state_type_ids_merged_on_nest() {
1096 #[derive(Clone)]
1097 struct NestedState(#[allow(dead_code)] String);
1098
1099 async fn handler() -> &'static str {
1100 "handler"
1101 }
1102
1103 let nested = Router::new()
1104 .route("/test", get(handler))
1105 .state(NestedState("nested".to_string()));
1106
1107 let parent = Router::new().nest("/api", nested);
1108
1109 assert!(parent
1111 .state_type_ids()
1112 .contains(&std::any::TypeId::of::<NestedState>()));
1113 }
1114
1115 #[test]
1116 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
1117 fn test_nested_route_conflict_with_existing_route() {
1118 async fn handler1() -> &'static str {
1119 "handler1"
1120 }
1121 async fn handler2() -> &'static str {
1122 "handler2"
1123 }
1124
1125 let parent = Router::new().route("/api/users/{id}", get(handler1));
1127
1128 let nested = Router::new().route("/{user_id}", get(handler2));
1130
1131 let _app = parent.nest("/api/users", nested);
1133 }
1134
1135 #[test]
1136 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
1137 fn test_nested_route_conflict_same_path_different_param_names() {
1138 async fn handler1() -> &'static str {
1139 "handler1"
1140 }
1141 async fn handler2() -> &'static str {
1142 "handler2"
1143 }
1144
1145 let nested1 = Router::new().route("/{id}", get(handler1));
1147 let nested2 = Router::new().route("/{user_id}", get(handler2));
1148
1149 let _app = Router::new()
1151 .nest("/api/users", nested1)
1152 .nest("/api/users", nested2);
1153 }
1154
1155 #[test]
1156 fn test_nested_route_conflict_error_contains_both_paths() {
1157 use std::panic::{catch_unwind, AssertUnwindSafe};
1158
1159 async fn handler1() -> &'static str {
1160 "handler1"
1161 }
1162 async fn handler2() -> &'static str {
1163 "handler2"
1164 }
1165
1166 let result = catch_unwind(AssertUnwindSafe(|| {
1167 let parent = Router::new().route("/api/users/{id}", get(handler1));
1168 let nested = Router::new().route("/{user_id}", get(handler2));
1169 let _app = parent.nest("/api/users", nested);
1170 }));
1171
1172 assert!(result.is_err(), "Should have panicked due to conflict");
1173
1174 if let Err(panic_info) = result {
1175 if let Some(msg) = panic_info.downcast_ref::<String>() {
1176 assert!(
1177 msg.contains("ROUTE CONFLICT DETECTED"),
1178 "Error should contain 'ROUTE CONFLICT DETECTED'"
1179 );
1180 assert!(
1181 msg.contains("Existing:") && msg.contains("New:"),
1182 "Error should contain both 'Existing:' and 'New:' labels"
1183 );
1184 assert!(
1185 msg.contains("How to resolve:"),
1186 "Error should contain resolution guidance"
1187 );
1188 }
1189 }
1190 }
1191
1192 #[test]
1193 fn test_nested_routes_no_conflict_different_prefixes() {
1194 async fn handler1() -> &'static str {
1195 "handler1"
1196 }
1197 async fn handler2() -> &'static str {
1198 "handler2"
1199 }
1200
1201 let nested1 = Router::new().route("/{id}", get(handler1));
1203 let nested2 = Router::new().route("/{id}", get(handler2));
1204
1205 let app = Router::new()
1207 .nest("/api/users", nested1)
1208 .nest("/api/posts", nested2);
1209
1210 assert_eq!(app.registered_routes().len(), 2);
1211 assert!(app.registered_routes().contains_key("/api/users/:id"));
1212 assert!(app.registered_routes().contains_key("/api/posts/:id"));
1213 }
1214
1215 #[test]
1220 fn test_multiple_router_composition_all_routes_registered() {
1221 async fn users_list() -> &'static str {
1222 "users list"
1223 }
1224 async fn users_get() -> &'static str {
1225 "users get"
1226 }
1227 async fn posts_list() -> &'static str {
1228 "posts list"
1229 }
1230 async fn posts_get() -> &'static str {
1231 "posts get"
1232 }
1233 async fn comments_list() -> &'static str {
1234 "comments list"
1235 }
1236
1237 let users_router = Router::new()
1239 .route("/", get(users_list))
1240 .route("/{id}", get(users_get));
1241
1242 let posts_router = Router::new()
1243 .route("/", get(posts_list))
1244 .route("/{id}", get(posts_get));
1245
1246 let comments_router = Router::new().route("/", get(comments_list));
1247
1248 let app = Router::new()
1250 .nest("/api/users", users_router)
1251 .nest("/api/posts", posts_router)
1252 .nest("/api/comments", comments_router);
1253
1254 let routes = app.registered_routes();
1256 assert_eq!(routes.len(), 5, "Should have 5 routes registered");
1257
1258 assert!(
1260 routes.contains_key("/api/users"),
1261 "Should have /api/users route"
1262 );
1263 assert!(
1264 routes.contains_key("/api/users/:id"),
1265 "Should have /api/users/:id route"
1266 );
1267
1268 assert!(
1270 routes.contains_key("/api/posts"),
1271 "Should have /api/posts route"
1272 );
1273 assert!(
1274 routes.contains_key("/api/posts/:id"),
1275 "Should have /api/posts/:id route"
1276 );
1277
1278 assert!(
1280 routes.contains_key("/api/comments"),
1281 "Should have /api/comments route"
1282 );
1283 }
1284
1285 #[test]
1286 fn test_multiple_router_composition_no_interference() {
1287 async fn users_handler() -> &'static str {
1288 "users"
1289 }
1290 async fn posts_handler() -> &'static str {
1291 "posts"
1292 }
1293 async fn admin_handler() -> &'static str {
1294 "admin"
1295 }
1296
1297 let users_router = Router::new()
1299 .route("/list", get(users_handler))
1300 .route("/{id}", get(users_handler));
1301
1302 let posts_router = Router::new()
1303 .route("/list", get(posts_handler))
1304 .route("/{id}", get(posts_handler));
1305
1306 let admin_router = Router::new()
1307 .route("/list", get(admin_handler))
1308 .route("/{id}", get(admin_handler));
1309
1310 let app = Router::new()
1312 .nest("/api/v1/users", users_router)
1313 .nest("/api/v1/posts", posts_router)
1314 .nest("/admin", admin_router);
1315
1316 let routes = app.registered_routes();
1318 assert_eq!(routes.len(), 6, "Should have 6 routes registered");
1319
1320 assert!(routes.contains_key("/api/v1/users/list"));
1322 assert!(routes.contains_key("/api/v1/users/:id"));
1323 assert!(routes.contains_key("/api/v1/posts/list"));
1324 assert!(routes.contains_key("/api/v1/posts/:id"));
1325 assert!(routes.contains_key("/admin/list"));
1326 assert!(routes.contains_key("/admin/:id"));
1327
1328 match app.match_route("/api/v1/users/list", &Method::GET) {
1330 RouteMatch::Found { params, .. } => {
1331 assert!(params.is_empty(), "Static path should have no params");
1332 }
1333 _ => panic!("Should find /api/v1/users/list"),
1334 }
1335
1336 match app.match_route("/api/v1/posts/123", &Method::GET) {
1337 RouteMatch::Found { params, .. } => {
1338 assert_eq!(params.get("id"), Some(&"123".to_string()));
1339 }
1340 _ => panic!("Should find /api/v1/posts/123"),
1341 }
1342
1343 match app.match_route("/admin/456", &Method::GET) {
1344 RouteMatch::Found { params, .. } => {
1345 assert_eq!(params.get("id"), Some(&"456".to_string()));
1346 }
1347 _ => panic!("Should find /admin/456"),
1348 }
1349 }
1350
1351 #[test]
1352 fn test_multiple_router_composition_with_multiple_methods() {
1353 async fn get_handler() -> &'static str {
1354 "get"
1355 }
1356 async fn post_handler() -> &'static str {
1357 "post"
1358 }
1359 async fn put_handler() -> &'static str {
1360 "put"
1361 }
1362
1363 let get_router = get(get_handler);
1366 let post_router = post(post_handler);
1367 let mut users_root_combined = MethodRouter::new();
1368 for (method, handler) in get_router.handlers {
1369 users_root_combined.handlers.insert(method, handler);
1370 }
1371 for (method, handler) in post_router.handlers {
1372 users_root_combined.handlers.insert(method, handler);
1373 }
1374
1375 let get_router2 = get(get_handler);
1377 let put_router = put(put_handler);
1378 let mut users_id_combined = MethodRouter::new();
1379 for (method, handler) in get_router2.handlers {
1380 users_id_combined.handlers.insert(method, handler);
1381 }
1382 for (method, handler) in put_router.handlers {
1383 users_id_combined.handlers.insert(method, handler);
1384 }
1385
1386 let users_router = Router::new()
1387 .route("/", users_root_combined)
1388 .route("/{id}", users_id_combined);
1389
1390 let get_router3 = get(get_handler);
1392 let post_router2 = post(post_handler);
1393 let mut posts_root_combined = MethodRouter::new();
1394 for (method, handler) in get_router3.handlers {
1395 posts_root_combined.handlers.insert(method, handler);
1396 }
1397 for (method, handler) in post_router2.handlers {
1398 posts_root_combined.handlers.insert(method, handler);
1399 }
1400
1401 let posts_router = Router::new().route("/", posts_root_combined);
1402
1403 let app = Router::new()
1405 .nest("/users", users_router)
1406 .nest("/posts", posts_router);
1407
1408 let routes = app.registered_routes();
1410 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1411
1412 let users_root = routes.get("/users").unwrap();
1414 assert!(users_root.methods.contains(&Method::GET));
1415 assert!(users_root.methods.contains(&Method::POST));
1416
1417 let users_id = routes.get("/users/:id").unwrap();
1418 assert!(users_id.methods.contains(&Method::GET));
1419 assert!(users_id.methods.contains(&Method::PUT));
1420
1421 let posts_root = routes.get("/posts").unwrap();
1423 assert!(posts_root.methods.contains(&Method::GET));
1424 assert!(posts_root.methods.contains(&Method::POST));
1425
1426 match app.match_route("/users", &Method::GET) {
1428 RouteMatch::Found { .. } => {}
1429 _ => panic!("GET /users should be found"),
1430 }
1431 match app.match_route("/users", &Method::POST) {
1432 RouteMatch::Found { .. } => {}
1433 _ => panic!("POST /users should be found"),
1434 }
1435 match app.match_route("/users/123", &Method::PUT) {
1436 RouteMatch::Found { .. } => {}
1437 _ => panic!("PUT /users/123 should be found"),
1438 }
1439 }
1440
1441 #[test]
1442 fn test_multiple_router_composition_deep_nesting() {
1443 async fn handler() -> &'static str {
1444 "handler"
1445 }
1446
1447 let deep_router = Router::new().route("/action", get(handler));
1449
1450 let mid_router = Router::new().route("/info", get(handler));
1451
1452 let shallow_router = Router::new().route("/status", get(handler));
1453
1454 let app = Router::new()
1456 .nest("/api/v1/resources/items", deep_router)
1457 .nest("/api/v1/resources", mid_router)
1458 .nest("/api", shallow_router);
1459
1460 let routes = app.registered_routes();
1462 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1463
1464 assert!(routes.contains_key("/api/v1/resources/items/action"));
1465 assert!(routes.contains_key("/api/v1/resources/info"));
1466 assert!(routes.contains_key("/api/status"));
1467
1468 match app.match_route("/api/v1/resources/items/action", &Method::GET) {
1470 RouteMatch::Found { .. } => {}
1471 _ => panic!("Should find deep route"),
1472 }
1473 match app.match_route("/api/v1/resources/info", &Method::GET) {
1474 RouteMatch::Found { .. } => {}
1475 _ => panic!("Should find mid route"),
1476 }
1477 match app.match_route("/api/status", &Method::GET) {
1478 RouteMatch::Found { .. } => {}
1479 _ => panic!("Should find shallow route"),
1480 }
1481 }
1482}
1483
1484#[cfg(test)]
1485mod property_tests {
1486 use super::*;
1487 use proptest::prelude::*;
1488 use std::panic::{catch_unwind, AssertUnwindSafe};
1489
1490 proptest! {
1498 #![proptest_config(ProptestConfig::with_cases(100))]
1499
1500 #[test]
1505 fn prop_normalized_prefix_starts_with_single_slash(
1506 leading_slashes in prop::collection::vec(Just('/'), 0..5),
1508 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 0..4),
1509 trailing_slashes in prop::collection::vec(Just('/'), 0..5),
1510 ) {
1511 let mut prefix = String::new();
1513 for _ in &leading_slashes {
1514 prefix.push('/');
1515 }
1516 for (i, segment) in segments.iter().enumerate() {
1517 if i > 0 {
1518 prefix.push('/');
1519 }
1520 prefix.push_str(segment);
1521 }
1522 for _ in &trailing_slashes {
1523 prefix.push('/');
1524 }
1525
1526 let normalized = normalize_prefix(&prefix);
1527
1528 prop_assert!(
1530 normalized.starts_with('/'),
1531 "Normalized prefix '{}' should start with '/', input was '{}'",
1532 normalized, prefix
1533 );
1534
1535 prop_assert!(
1537 !normalized.starts_with("//"),
1538 "Normalized prefix '{}' should not start with '//', input was '{}'",
1539 normalized, prefix
1540 );
1541 }
1542
1543 #[test]
1548 fn prop_normalized_prefix_no_trailing_slash(
1549 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1550 trailing_slashes in prop::collection::vec(Just('/'), 0..5),
1551 ) {
1552 let mut prefix = String::from("/");
1554 for (i, segment) in segments.iter().enumerate() {
1555 if i > 0 {
1556 prefix.push('/');
1557 }
1558 prefix.push_str(segment);
1559 }
1560 for _ in &trailing_slashes {
1561 prefix.push('/');
1562 }
1563
1564 let normalized = normalize_prefix(&prefix);
1565
1566 prop_assert!(
1568 !normalized.ends_with('/'),
1569 "Normalized prefix '{}' should not end with '/', input was '{}'",
1570 normalized, prefix
1571 );
1572 }
1573
1574 #[test]
1579 fn prop_normalized_prefix_no_double_slashes(
1580 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1582 extra_slashes in prop::collection::vec(0..4usize, 1..4),
1583 ) {
1584 let mut prefix = String::from("/");
1586 for (i, segment) in segments.iter().enumerate() {
1587 if i > 0 {
1588 let num_slashes = extra_slashes.get(i).copied().unwrap_or(1);
1590 for _ in 0..=num_slashes {
1591 prefix.push('/');
1592 }
1593 }
1594 prefix.push_str(segment);
1595 }
1596
1597 let normalized = normalize_prefix(&prefix);
1598
1599 prop_assert!(
1601 !normalized.contains("//"),
1602 "Normalized prefix '{}' should not contain '//', input was '{}'",
1603 normalized, prefix
1604 );
1605 }
1606
1607 #[test]
1612 fn prop_normalized_prefix_preserves_segments(
1613 segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..4),
1614 ) {
1615 let prefix = format!("/{}", segments.join("/"));
1617
1618 let normalized = normalize_prefix(&prefix);
1619
1620 let normalized_segments: Vec<&str> = normalized
1622 .split('/')
1623 .filter(|s| !s.is_empty())
1624 .collect();
1625
1626 prop_assert_eq!(
1627 segments.len(),
1628 normalized_segments.len(),
1629 "Segment count should be preserved"
1630 );
1631
1632 for (original, normalized_seg) in segments.iter().zip(normalized_segments.iter()) {
1633 prop_assert_eq!(
1634 original, normalized_seg,
1635 "Segment content should be preserved"
1636 );
1637 }
1638 }
1639
1640 #[test]
1645 fn prop_empty_or_slashes_normalize_to_root(
1646 num_slashes in 0..10usize,
1647 ) {
1648 let prefix: String = std::iter::repeat('/').take(num_slashes).collect();
1649
1650 let normalized = normalize_prefix(&prefix);
1651
1652 prop_assert_eq!(
1653 normalized, "/",
1654 "Empty or slash-only prefix '{}' should normalize to '/'",
1655 prefix
1656 );
1657 }
1658 }
1659
1660 proptest! {
1667 #![proptest_config(ProptestConfig::with_cases(100))]
1668
1669 #[test]
1674 fn prop_method_router_clone_preserves_methods(
1675 use_get in any::<bool>(),
1677 use_post in any::<bool>(),
1678 use_put in any::<bool>(),
1679 use_patch in any::<bool>(),
1680 use_delete in any::<bool>(),
1681 ) {
1682 prop_assume!(use_get || use_post || use_put || use_patch || use_delete);
1684
1685 let mut method_router = MethodRouter::new();
1687 let mut expected_methods: Vec<Method> = Vec::new();
1688
1689 async fn handler() -> &'static str { "handler" }
1690
1691 if use_get {
1692 method_router = get(handler);
1693 expected_methods.push(Method::GET);
1694 }
1695
1696 if use_post {
1697 let post_router = post(handler);
1698 for (method, handler) in post_router.handlers {
1699 method_router.handlers.insert(method.clone(), handler);
1700 if !expected_methods.contains(&method) {
1701 expected_methods.push(method);
1702 }
1703 }
1704 }
1705
1706 if use_put {
1707 let put_router = put(handler);
1708 for (method, handler) in put_router.handlers {
1709 method_router.handlers.insert(method.clone(), handler);
1710 if !expected_methods.contains(&method) {
1711 expected_methods.push(method);
1712 }
1713 }
1714 }
1715
1716 if use_patch {
1717 let patch_router = patch(handler);
1718 for (method, handler) in patch_router.handlers {
1719 method_router.handlers.insert(method.clone(), handler);
1720 if !expected_methods.contains(&method) {
1721 expected_methods.push(method);
1722 }
1723 }
1724 }
1725
1726 if use_delete {
1727 let delete_router = delete(handler);
1728 for (method, handler) in delete_router.handlers {
1729 method_router.handlers.insert(method.clone(), handler);
1730 if !expected_methods.contains(&method) {
1731 expected_methods.push(method);
1732 }
1733 }
1734 }
1735
1736 let cloned_router = method_router.clone();
1738
1739 let original_methods = method_router.allowed_methods();
1741 let cloned_methods = cloned_router.allowed_methods();
1742
1743 prop_assert_eq!(
1744 original_methods.len(),
1745 cloned_methods.len(),
1746 "Cloned router should have same number of methods"
1747 );
1748
1749 for method in &expected_methods {
1750 prop_assert!(
1751 cloned_router.get_handler(method).is_some(),
1752 "Cloned router should have handler for method {:?}",
1753 method
1754 );
1755 }
1756
1757 for method in &cloned_methods {
1759 prop_assert!(
1760 cloned_router.get_handler(method).is_some(),
1761 "Handler for {:?} should be accessible after clone",
1762 method
1763 );
1764 }
1765 }
1766 }
1767
1768 proptest! {
1776 #![proptest_config(ProptestConfig::with_cases(100))]
1777
1778 #[test]
1783 fn prop_nested_routes_have_prefix(
1784 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1786 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1788 has_param in any::<bool>(),
1789 ) {
1790 async fn handler() -> &'static str { "handler" }
1791
1792 let prefix = format!("/{}", prefix_segments.join("/"));
1794
1795 let mut route_path = format!("/{}", route_segments.join("/"));
1797 if has_param {
1798 route_path.push_str("/{id}");
1799 }
1800
1801 let nested_router = Router::new().route(&route_path, get(handler));
1803 let app = Router::new().nest(&prefix, nested_router);
1804
1805 let expected_matchit_path = if has_param {
1807 format!("{}/{}/:id", prefix, route_segments.join("/"))
1808 } else {
1809 format!("{}/{}", prefix, route_segments.join("/"))
1810 };
1811
1812 let routes = app.registered_routes();
1813
1814 prop_assert!(
1816 routes.contains_key(&expected_matchit_path),
1817 "Expected route '{}' not found. Available routes: {:?}",
1818 expected_matchit_path,
1819 routes.keys().collect::<Vec<_>>()
1820 );
1821
1822 let route_info = routes.get(&expected_matchit_path).unwrap();
1824 let expected_display_path = format!("{}{}", prefix, route_path);
1825 prop_assert_eq!(
1826 &route_info.path, &expected_display_path,
1827 "Display path should be prefix + original path"
1828 );
1829 }
1830
1831 #[test]
1836 fn prop_route_count_preserved_after_nesting(
1837 num_routes in 1..4usize,
1839 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1840 ) {
1841 async fn handler() -> &'static str { "handler" }
1842
1843 let prefix = format!("/{}", prefix_segments.join("/"));
1844
1845 let mut nested_router = Router::new();
1847 for i in 0..num_routes {
1848 let path = format!("/route{}", i);
1849 nested_router = nested_router.route(&path, get(handler));
1850 }
1851
1852 let app = Router::new().nest(&prefix, nested_router);
1853
1854 prop_assert_eq!(
1855 app.registered_routes().len(),
1856 num_routes,
1857 "Number of routes should be preserved after nesting"
1858 );
1859 }
1860
1861 #[test]
1865 fn prop_nested_routes_are_matchable(
1866 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1867 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1868 ) {
1869 async fn handler() -> &'static str { "handler" }
1870
1871 let prefix = format!("/{}", prefix_segments.join("/"));
1872 let route_path = format!("/{}", route_segments.join("/"));
1873
1874 let nested_router = Router::new().route(&route_path, get(handler));
1875 let app = Router::new().nest(&prefix, nested_router);
1876
1877 let full_path = format!("{}{}", prefix, route_path);
1879
1880 match app.match_route(&full_path, &Method::GET) {
1882 RouteMatch::Found { .. } => {
1883 }
1885 RouteMatch::NotFound => {
1886 prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
1887 }
1888 RouteMatch::MethodNotAllowed { .. } => {
1889 prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
1890 }
1891 }
1892 }
1893 }
1894
1895 proptest! {
1902 #![proptest_config(ProptestConfig::with_cases(100))]
1903
1904 #[test]
1909 fn prop_state_type_ids_merged(
1910 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1911 has_nested_state in any::<bool>(),
1912 ) {
1913 #[derive(Clone)]
1914 struct TestState(#[allow(dead_code)] i32);
1915
1916 async fn handler() -> &'static str { "handler" }
1917
1918 let prefix = format!("/{}", prefix_segments.join("/"));
1919
1920 let mut nested = Router::new().route("/test", get(handler));
1921 if has_nested_state {
1922 nested = nested.state(TestState(42));
1923 }
1924
1925 let parent = Router::new().nest(&prefix, nested);
1926
1927 if has_nested_state {
1929 prop_assert!(
1930 parent.state_type_ids().contains(&std::any::TypeId::of::<TestState>()),
1931 "Parent should track nested state type ID"
1932 );
1933 }
1934 }
1935
1936 #[test]
1941 fn prop_merge_state_adds_nested_state(
1942 state_value in any::<i32>(),
1943 ) {
1944 #[derive(Clone, PartialEq, Debug)]
1945 struct UniqueState(i32);
1946
1947 let source = Router::new().state(UniqueState(state_value));
1949
1950 let parent = Router::new().merge_state::<UniqueState>(&source);
1952
1953 prop_assert!(
1955 parent.has_state::<UniqueState>(),
1956 "Parent should have state after merge"
1957 );
1958
1959 let merged_state = parent.state.get::<UniqueState>().unwrap();
1961 prop_assert_eq!(
1962 merged_state.0, state_value,
1963 "Merged state value should match source"
1964 );
1965 }
1966 }
1967
1968 proptest! {
1975 #![proptest_config(ProptestConfig::with_cases(100))]
1976
1977 #[test]
1982 fn prop_parent_state_takes_precedence(
1983 parent_value in any::<i32>(),
1984 nested_value in any::<i32>(),
1985 ) {
1986 prop_assume!(parent_value != nested_value);
1988
1989 #[derive(Clone, PartialEq, Debug)]
1990 struct SharedState(i32);
1991
1992 let source = Router::new().state(SharedState(nested_value));
1994
1995 let parent = Router::new()
1997 .state(SharedState(parent_value))
1998 .merge_state::<SharedState>(&source);
1999
2000 prop_assert!(
2002 parent.has_state::<SharedState>(),
2003 "Parent should have state"
2004 );
2005
2006 let final_state = parent.state.get::<SharedState>().unwrap();
2008 prop_assert_eq!(
2009 final_state.0, parent_value,
2010 "Parent state value should be preserved, not overwritten by nested"
2011 );
2012 }
2013
2014 #[test]
2019 fn prop_state_precedence_consistent(
2020 parent_value in any::<i32>(),
2021 source1_value in any::<i32>(),
2022 source2_value in any::<i32>(),
2023 ) {
2024 #[derive(Clone, PartialEq, Debug)]
2025 struct ConsistentState(i32);
2026
2027 let source1 = Router::new().state(ConsistentState(source1_value));
2029 let source2 = Router::new().state(ConsistentState(source2_value));
2030
2031 let parent = Router::new()
2033 .state(ConsistentState(parent_value))
2034 .merge_state::<ConsistentState>(&source1)
2035 .merge_state::<ConsistentState>(&source2);
2036
2037 let final_state = parent.state.get::<ConsistentState>().unwrap();
2039 prop_assert_eq!(
2040 final_state.0, parent_value,
2041 "Parent state should be preserved after multiple merges"
2042 );
2043 }
2044 }
2045
2046 proptest! {
2054 #![proptest_config(ProptestConfig::with_cases(100))]
2055
2056 #[test]
2061 fn prop_same_structure_different_param_names_conflict(
2062 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
2064 param1 in "[a-z][a-z0-9]{0,5}",
2066 param2 in "[a-z][a-z0-9]{0,5}",
2067 ) {
2068 prop_assume!(param1 != param2);
2070
2071 let mut path1 = String::from("/");
2073 let mut path2 = String::from("/");
2074
2075 for segment in &segments {
2076 path1.push_str(segment);
2077 path1.push('/');
2078 path2.push_str(segment);
2079 path2.push('/');
2080 }
2081
2082 path1.push('{');
2083 path1.push_str(¶m1);
2084 path1.push('}');
2085
2086 path2.push('{');
2087 path2.push_str(¶m2);
2088 path2.push('}');
2089
2090 let result = catch_unwind(AssertUnwindSafe(|| {
2092 async fn handler1() -> &'static str { "handler1" }
2093 async fn handler2() -> &'static str { "handler2" }
2094
2095 let _router = Router::new()
2096 .route(&path1, get(handler1))
2097 .route(&path2, get(handler2));
2098 }));
2099
2100 prop_assert!(
2101 result.is_err(),
2102 "Routes '{}' and '{}' should conflict but didn't",
2103 path1, path2
2104 );
2105 }
2106
2107 #[test]
2112 fn prop_different_structures_no_conflict(
2113 segments1 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2115 segments2 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2116 has_param1 in any::<bool>(),
2118 has_param2 in any::<bool>(),
2119 ) {
2120 let mut path1 = String::from("/");
2122 let mut path2 = String::from("/");
2123
2124 for segment in &segments1 {
2125 path1.push_str(segment);
2126 path1.push('/');
2127 }
2128 path1.pop(); for segment in &segments2 {
2131 path2.push_str(segment);
2132 path2.push('/');
2133 }
2134 path2.pop(); if has_param1 {
2137 path1.push_str("/{id}");
2138 }
2139
2140 if has_param2 {
2141 path2.push_str("/{id}");
2142 }
2143
2144 let norm1 = normalize_path_for_comparison(&convert_path_params(&path1));
2146 let norm2 = normalize_path_for_comparison(&convert_path_params(&path2));
2147
2148 prop_assume!(norm1 != norm2);
2150
2151 let result = catch_unwind(AssertUnwindSafe(|| {
2153 async fn handler1() -> &'static str { "handler1" }
2154 async fn handler2() -> &'static str { "handler2" }
2155
2156 let router = Router::new()
2157 .route(&path1, get(handler1))
2158 .route(&path2, get(handler2));
2159
2160 router.registered_routes().len()
2161 }));
2162
2163 prop_assert!(
2164 result.is_ok(),
2165 "Routes '{}' and '{}' should not conflict but did",
2166 path1, path2
2167 );
2168
2169 if let Ok(count) = result {
2170 prop_assert_eq!(count, 2, "Should have registered 2 routes");
2171 }
2172 }
2173
2174 #[test]
2179 fn prop_conflict_error_contains_both_paths(
2180 segment in "[a-z][a-z0-9]{1,5}",
2182 param1 in "[a-z][a-z0-9]{1,5}",
2183 param2 in "[a-z][a-z0-9]{1,5}",
2184 ) {
2185 prop_assume!(param1 != param2);
2186
2187 let path1 = format!("/{}/{{{}}}", segment, param1);
2188 let path2 = format!("/{}/{{{}}}", segment, param2);
2189
2190 let result = catch_unwind(AssertUnwindSafe(|| {
2191 async fn handler1() -> &'static str { "handler1" }
2192 async fn handler2() -> &'static str { "handler2" }
2193
2194 let _router = Router::new()
2195 .route(&path1, get(handler1))
2196 .route(&path2, get(handler2));
2197 }));
2198
2199 prop_assert!(result.is_err(), "Should have panicked due to conflict");
2200
2201 if let Err(panic_info) = result {
2203 if let Some(msg) = panic_info.downcast_ref::<String>() {
2204 prop_assert!(
2205 msg.contains("ROUTE CONFLICT DETECTED"),
2206 "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
2207 msg
2208 );
2209 prop_assert!(
2210 msg.contains("Existing:") && msg.contains("New:"),
2211 "Error should contain both 'Existing:' and 'New:' labels, got: {}",
2212 msg
2213 );
2214 prop_assert!(
2215 msg.contains("How to resolve:"),
2216 "Error should contain resolution guidance, got: {}",
2217 msg
2218 );
2219 }
2220 }
2221 }
2222
2223 #[test]
2227 fn prop_exact_duplicate_paths_conflict(
2228 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
2230 has_param in any::<bool>(),
2231 ) {
2232 let mut path = String::from("/");
2234
2235 for segment in &segments {
2236 path.push_str(segment);
2237 path.push('/');
2238 }
2239 path.pop(); if has_param {
2242 path.push_str("/{id}");
2243 }
2244
2245 let result = catch_unwind(AssertUnwindSafe(|| {
2247 async fn handler1() -> &'static str { "handler1" }
2248 async fn handler2() -> &'static str { "handler2" }
2249
2250 let _router = Router::new()
2251 .route(&path, get(handler1))
2252 .route(&path, get(handler2));
2253 }));
2254
2255 prop_assert!(
2256 result.is_err(),
2257 "Registering path '{}' twice should conflict but didn't",
2258 path
2259 );
2260 }
2261 }
2262
2263 proptest! {
2270 #![proptest_config(ProptestConfig::with_cases(100))]
2271
2272 #[test]
2277 fn prop_nested_route_with_params_matches(
2278 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2279 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
2280 param_value in "[a-z0-9]{1,10}",
2281 ) {
2282 async fn handler() -> &'static str { "handler" }
2283
2284 let prefix = format!("/{}", prefix_segments.join("/"));
2285 let route_path = if route_segments.is_empty() {
2286 "/{id}".to_string()
2287 } else {
2288 format!("/{}/{{id}}", route_segments.join("/"))
2289 };
2290
2291 let nested_router = Router::new().route(&route_path, get(handler));
2292 let app = Router::new().nest(&prefix, nested_router);
2293
2294 let full_path = if route_segments.is_empty() {
2296 format!("{}/{}", prefix, param_value)
2297 } else {
2298 format!("{}/{}/{}", prefix, route_segments.join("/"), param_value)
2299 };
2300
2301 match app.match_route(&full_path, &Method::GET) {
2303 RouteMatch::Found { params, .. } => {
2304 prop_assert!(
2306 params.contains_key("id"),
2307 "Should have 'id' parameter, got: {:?}",
2308 params
2309 );
2310 prop_assert_eq!(
2311 params.get("id").unwrap(),
2312 ¶m_value,
2313 "Parameter value should match"
2314 );
2315 }
2316 RouteMatch::NotFound => {
2317 prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
2318 }
2319 RouteMatch::MethodNotAllowed { .. } => {
2320 prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
2321 }
2322 }
2323 }
2324
2325 #[test]
2330 fn prop_nested_route_matches_correct_method(
2331 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2332 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2333 use_get in any::<bool>(),
2334 ) {
2335 async fn handler() -> &'static str { "handler" }
2336
2337 let prefix = format!("/{}", prefix_segments.join("/"));
2338 let route_path = format!("/{}", route_segments.join("/"));
2339
2340 let method_router = if use_get { get(handler) } else { post(handler) };
2342 let nested_router = Router::new().route(&route_path, method_router);
2343 let app = Router::new().nest(&prefix, nested_router);
2344
2345 let full_path = format!("{}{}", prefix, route_path);
2346 let registered_method = if use_get { Method::GET } else { Method::POST };
2347 let other_method = if use_get { Method::POST } else { Method::GET };
2348
2349 match app.match_route(&full_path, ®istered_method) {
2351 RouteMatch::Found { .. } => {
2352 }
2354 other => {
2355 prop_assert!(false, "Route should be found for registered method, got: {:?}",
2356 match other {
2357 RouteMatch::NotFound => "NotFound",
2358 RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
2359 _ => "Found",
2360 }
2361 );
2362 }
2363 }
2364
2365 match app.match_route(&full_path, &other_method) {
2367 RouteMatch::MethodNotAllowed { allowed } => {
2368 prop_assert!(
2369 allowed.contains(®istered_method),
2370 "Allowed methods should contain {:?}",
2371 registered_method
2372 );
2373 }
2374 other => {
2375 prop_assert!(false, "Route should return MethodNotAllowed for other method, got: {:?}",
2376 match other {
2377 RouteMatch::NotFound => "NotFound",
2378 RouteMatch::Found { .. } => "Found",
2379 _ => "MethodNotAllowed",
2380 }
2381 );
2382 }
2383 }
2384 }
2385 }
2386
2387 proptest! {
2394 #![proptest_config(ProptestConfig::with_cases(100))]
2395
2396 #[test]
2401 fn prop_single_param_extraction(
2402 prefix in "[a-z][a-z0-9]{1,5}",
2403 param_name in "[a-z][a-z0-9]{1,5}",
2404 param_value in "[a-z0-9]{1,10}",
2405 ) {
2406 async fn handler() -> &'static str { "handler" }
2407
2408 let prefix = format!("/{}", prefix);
2409 let route_path = format!("/{{{}}}", param_name);
2410
2411 let nested_router = Router::new().route(&route_path, get(handler));
2412 let app = Router::new().nest(&prefix, nested_router);
2413
2414 let full_path = format!("{}/{}", prefix, param_value);
2415
2416 match app.match_route(&full_path, &Method::GET) {
2417 RouteMatch::Found { params, .. } => {
2418 prop_assert!(
2419 params.contains_key(¶m_name),
2420 "Should have '{}' parameter, got: {:?}",
2421 param_name, params
2422 );
2423 prop_assert_eq!(
2424 params.get(¶m_name).unwrap(),
2425 ¶m_value,
2426 "Parameter '{}' value should be '{}'",
2427 param_name, param_value
2428 );
2429 }
2430 _ => {
2431 prop_assert!(false, "Route should be found");
2432 }
2433 }
2434 }
2435
2436 #[test]
2441 fn prop_multiple_params_extraction(
2442 prefix in "[a-z][a-z0-9]{1,5}",
2443 param1_name in "[a-z]{1,5}",
2444 param1_value in "[a-z0-9]{1,10}",
2445 param2_name in "[a-z]{1,5}",
2446 param2_value in "[a-z0-9]{1,10}",
2447 ) {
2448 prop_assume!(param1_name != param2_name);
2450
2451 async fn handler() -> &'static str { "handler" }
2452
2453 let prefix = format!("/{}", prefix);
2454 let route_path = format!("/{{{}}}/items/{{{}}}", param1_name, param2_name);
2455
2456 let nested_router = Router::new().route(&route_path, get(handler));
2457 let app = Router::new().nest(&prefix, nested_router);
2458
2459 let full_path = format!("{}/{}/items/{}", prefix, param1_value, param2_value);
2460
2461 match app.match_route(&full_path, &Method::GET) {
2462 RouteMatch::Found { params, .. } => {
2463 prop_assert!(
2465 params.contains_key(¶m1_name),
2466 "Should have '{}' parameter, got: {:?}",
2467 param1_name, params
2468 );
2469 prop_assert_eq!(
2470 params.get(¶m1_name).unwrap(),
2471 ¶m1_value,
2472 "Parameter '{}' value should be '{}'",
2473 param1_name, param1_value
2474 );
2475
2476 prop_assert!(
2478 params.contains_key(¶m2_name),
2479 "Should have '{}' parameter, got: {:?}",
2480 param2_name, params
2481 );
2482 prop_assert_eq!(
2483 params.get(¶m2_name).unwrap(),
2484 ¶m2_value,
2485 "Parameter '{}' value should be '{}'",
2486 param2_name, param2_value
2487 );
2488 }
2489 _ => {
2490 prop_assert!(false, "Route should be found");
2491 }
2492 }
2493 }
2494
2495 #[test]
2500 fn prop_param_value_preservation(
2501 prefix in "[a-z]{1,5}",
2502 param_value in "[a-zA-Z0-9_-]{1,15}",
2504 ) {
2505 async fn handler() -> &'static str { "handler" }
2506
2507 let prefix = format!("/{}", prefix);
2508 let route_path = "/{id}".to_string();
2509
2510 let nested_router = Router::new().route(&route_path, get(handler));
2511 let app = Router::new().nest(&prefix, nested_router);
2512
2513 let full_path = format!("{}/{}", prefix, param_value);
2514
2515 match app.match_route(&full_path, &Method::GET) {
2516 RouteMatch::Found { params, .. } => {
2517 prop_assert_eq!(
2518 params.get("id").unwrap(),
2519 ¶m_value,
2520 "Parameter value should be preserved exactly"
2521 );
2522 }
2523 _ => {
2524 prop_assert!(false, "Route should be found");
2525 }
2526 }
2527 }
2528 }
2529
2530 proptest! {
2537 #![proptest_config(ProptestConfig::with_cases(100))]
2538
2539 #[test]
2544 fn prop_unregistered_path_returns_not_found(
2545 prefix in "[a-z][a-z0-9]{1,5}",
2546 route_segment in "[a-z][a-z0-9]{1,5}",
2547 unregistered_segment in "[a-z][a-z0-9]{6,10}",
2548 ) {
2549 prop_assume!(route_segment != unregistered_segment);
2551
2552 async fn handler() -> &'static str { "handler" }
2553
2554 let prefix = format!("/{}", prefix);
2555 let route_path = format!("/{}", route_segment);
2556
2557 let nested_router = Router::new().route(&route_path, get(handler));
2558 let app = Router::new().nest(&prefix, nested_router);
2559
2560 let unregistered_path = format!("{}/{}", prefix, unregistered_segment);
2562
2563 match app.match_route(&unregistered_path, &Method::GET) {
2564 RouteMatch::NotFound => {
2565 }
2567 RouteMatch::Found { .. } => {
2568 prop_assert!(false, "Path '{}' should not be found", unregistered_path);
2569 }
2570 RouteMatch::MethodNotAllowed { .. } => {
2571 prop_assert!(false, "Path '{}' should return NotFound, not MethodNotAllowed", unregistered_path);
2572 }
2573 }
2574 }
2575
2576 #[test]
2580 fn prop_wrong_prefix_returns_not_found(
2581 prefix1 in "[a-z][a-z0-9]{1,5}",
2582 prefix2 in "[a-z][a-z0-9]{6,10}",
2583 route_segment in "[a-z][a-z0-9]{1,5}",
2584 ) {
2585 prop_assume!(prefix1 != prefix2);
2587
2588 async fn handler() -> &'static str { "handler" }
2589
2590 let prefix = format!("/{}", prefix1);
2591 let route_path = format!("/{}", route_segment);
2592
2593 let nested_router = Router::new().route(&route_path, get(handler));
2594 let app = Router::new().nest(&prefix, nested_router);
2595
2596 let wrong_prefix_path = format!("/{}/{}", prefix2, route_segment);
2598
2599 match app.match_route(&wrong_prefix_path, &Method::GET) {
2600 RouteMatch::NotFound => {
2601 }
2603 _ => {
2604 prop_assert!(false, "Path '{}' with wrong prefix should return NotFound", wrong_prefix_path);
2605 }
2606 }
2607 }
2608
2609 #[test]
2614 fn prop_partial_path_returns_not_found(
2615 prefix in "[a-z][a-z0-9]{1,5}",
2616 segment1 in "[a-z][a-z0-9]{1,5}",
2617 segment2 in "[a-z][a-z0-9]{1,5}",
2618 ) {
2619 async fn handler() -> &'static str { "handler" }
2620
2621 let prefix = format!("/{}", prefix);
2622 let route_path = format!("/{}/{}", segment1, segment2);
2623
2624 let nested_router = Router::new().route(&route_path, get(handler));
2625 let app = Router::new().nest(&prefix, nested_router);
2626
2627 let partial_path = format!("{}/{}", prefix, segment1);
2629
2630 match app.match_route(&partial_path, &Method::GET) {
2631 RouteMatch::NotFound => {
2632 }
2634 _ => {
2635 prop_assert!(false, "Partial path '{}' should return NotFound", partial_path);
2636 }
2637 }
2638 }
2639 }
2640
2641 proptest! {
2648 #![proptest_config(ProptestConfig::with_cases(100))]
2649
2650 #[test]
2656 fn prop_unregistered_method_returns_method_not_allowed(
2657 prefix in "[a-z][a-z0-9]{1,5}",
2658 route_segment in "[a-z][a-z0-9]{1,5}",
2659 ) {
2660 async fn handler() -> &'static str { "handler" }
2661
2662 let prefix = format!("/{}", prefix);
2663 let route_path = format!("/{}", route_segment);
2664
2665 let nested_router = Router::new().route(&route_path, get(handler));
2667 let app = Router::new().nest(&prefix, nested_router);
2668
2669 let full_path = format!("{}{}", prefix, route_path);
2670
2671 match app.match_route(&full_path, &Method::POST) {
2673 RouteMatch::MethodNotAllowed { allowed } => {
2674 prop_assert!(
2675 allowed.contains(&Method::GET),
2676 "Allowed methods should contain GET, got: {:?}",
2677 allowed
2678 );
2679 prop_assert!(
2680 !allowed.contains(&Method::POST),
2681 "Allowed methods should not contain POST"
2682 );
2683 }
2684 RouteMatch::Found { .. } => {
2685 prop_assert!(false, "POST should not be found on GET-only route");
2686 }
2687 RouteMatch::NotFound => {
2688 prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
2689 }
2690 }
2691 }
2692
2693 #[test]
2698 fn prop_multiple_methods_in_allowed_list(
2699 prefix in "[a-z][a-z0-9]{1,5}",
2700 route_segment in "[a-z][a-z0-9]{1,5}",
2701 use_get in any::<bool>(),
2702 use_post in any::<bool>(),
2703 use_put in any::<bool>(),
2704 ) {
2705 prop_assume!(use_get || use_post || use_put);
2707
2708 async fn handler() -> &'static str { "handler" }
2709
2710 let prefix = format!("/{}", prefix);
2711 let route_path = format!("/{}", route_segment);
2712
2713 let mut method_router = MethodRouter::new();
2715 let mut expected_methods: Vec<Method> = Vec::new();
2716
2717 if use_get {
2718 let get_router = get(handler);
2719 for (method, h) in get_router.handlers {
2720 method_router.handlers.insert(method.clone(), h);
2721 expected_methods.push(method);
2722 }
2723 }
2724 if use_post {
2725 let post_router = post(handler);
2726 for (method, h) in post_router.handlers {
2727 method_router.handlers.insert(method.clone(), h);
2728 expected_methods.push(method);
2729 }
2730 }
2731 if use_put {
2732 let put_router = put(handler);
2733 for (method, h) in put_router.handlers {
2734 method_router.handlers.insert(method.clone(), h);
2735 expected_methods.push(method);
2736 }
2737 }
2738
2739 let nested_router = Router::new().route(&route_path, method_router);
2740 let app = Router::new().nest(&prefix, nested_router);
2741
2742 let full_path = format!("{}{}", prefix, route_path);
2743
2744 match app.match_route(&full_path, &Method::DELETE) {
2746 RouteMatch::MethodNotAllowed { allowed } => {
2747 for method in &expected_methods {
2749 prop_assert!(
2750 allowed.contains(method),
2751 "Allowed methods should contain {:?}, got: {:?}",
2752 method, allowed
2753 );
2754 }
2755 prop_assert!(
2757 !allowed.contains(&Method::DELETE),
2758 "Allowed methods should not contain DELETE"
2759 );
2760 }
2761 RouteMatch::Found { .. } => {
2762 prop_assert!(false, "DELETE should not be found");
2763 }
2764 RouteMatch::NotFound => {
2765 prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
2766 }
2767 }
2768 }
2769 }
2770
2771 proptest! {
2785 #![proptest_config(ProptestConfig::with_cases(100))]
2786
2787 #[test]
2793 fn prop_multiple_routers_all_routes_registered(
2794 prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2796 prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2797 num_routes1 in 1..4usize,
2799 num_routes2 in 1..4usize,
2800 ) {
2801 let prefix1 = format!("/{}", prefix1_segments.join("/"));
2803 let prefix2 = format!("/{}", prefix2_segments.join("/"));
2804
2805 prop_assume!(prefix1 != prefix2);
2807
2808 async fn handler() -> &'static str { "handler" }
2809
2810 let mut router1 = Router::new();
2812 for i in 0..num_routes1 {
2813 let path = format!("/route1_{}", i);
2814 router1 = router1.route(&path, get(handler));
2815 }
2816
2817 let mut router2 = Router::new();
2819 for i in 0..num_routes2 {
2820 let path = format!("/route2_{}", i);
2821 router2 = router2.route(&path, get(handler));
2822 }
2823
2824 let app = Router::new()
2826 .nest(&prefix1, router1)
2827 .nest(&prefix2, router2);
2828
2829 let routes = app.registered_routes();
2830
2831 let expected_count = num_routes1 + num_routes2;
2833 prop_assert_eq!(
2834 routes.len(),
2835 expected_count,
2836 "Should have {} routes ({}+{}), got {}",
2837 expected_count, num_routes1, num_routes2, routes.len()
2838 );
2839
2840 for i in 0..num_routes1 {
2842 let expected_path = format!("{}/route1_{}", prefix1, i);
2843 let matchit_path = convert_path_params(&expected_path);
2844 prop_assert!(
2845 routes.contains_key(&matchit_path),
2846 "Route '{}' should be registered",
2847 expected_path
2848 );
2849 }
2850
2851 for i in 0..num_routes2 {
2853 let expected_path = format!("{}/route2_{}", prefix2, i);
2854 let matchit_path = convert_path_params(&expected_path);
2855 prop_assert!(
2856 routes.contains_key(&matchit_path),
2857 "Route '{}' should be registered",
2858 expected_path
2859 );
2860 }
2861 }
2862
2863 #[test]
2868 fn prop_multiple_routers_no_interference(
2869 prefix1 in "[a-z][a-z0-9]{1,5}",
2870 prefix2 in "[a-z][a-z0-9]{1,5}",
2871 route_segment in "[a-z][a-z0-9]{1,5}",
2872 param_value1 in "[a-z0-9]{1,10}",
2873 param_value2 in "[a-z0-9]{1,10}",
2874 ) {
2875 prop_assume!(prefix1 != prefix2);
2877
2878 let prefix1 = format!("/{}", prefix1);
2879 let prefix2 = format!("/{}", prefix2);
2880
2881 async fn handler() -> &'static str { "handler" }
2882
2883 let router1 = Router::new()
2885 .route(&format!("/{}", route_segment), get(handler))
2886 .route("/{id}", get(handler));
2887
2888 let router2 = Router::new()
2889 .route(&format!("/{}", route_segment), get(handler))
2890 .route("/{id}", get(handler));
2891
2892 let app = Router::new()
2894 .nest(&prefix1, router1)
2895 .nest(&prefix2, router2);
2896
2897 let path1_static = format!("{}/{}", prefix1, route_segment);
2899 match app.match_route(&path1_static, &Method::GET) {
2900 RouteMatch::Found { params, .. } => {
2901 prop_assert!(params.is_empty(), "Static path should have no params");
2902 }
2903 _ => {
2904 prop_assert!(false, "Route '{}' should be found", path1_static);
2905 }
2906 }
2907
2908 let path1_param = format!("{}/{}", prefix1, param_value1);
2909 match app.match_route(&path1_param, &Method::GET) {
2910 RouteMatch::Found { params, .. } => {
2911 prop_assert_eq!(
2912 params.get("id"),
2913 Some(¶m_value1.to_string()),
2914 "Parameter should be extracted correctly"
2915 );
2916 }
2917 _ => {
2918 prop_assert!(false, "Route '{}' should be found", path1_param);
2919 }
2920 }
2921
2922 let path2_static = format!("{}/{}", prefix2, route_segment);
2924 match app.match_route(&path2_static, &Method::GET) {
2925 RouteMatch::Found { params, .. } => {
2926 prop_assert!(params.is_empty(), "Static path should have no params");
2927 }
2928 _ => {
2929 prop_assert!(false, "Route '{}' should be found", path2_static);
2930 }
2931 }
2932
2933 let path2_param = format!("{}/{}", prefix2, param_value2);
2934 match app.match_route(&path2_param, &Method::GET) {
2935 RouteMatch::Found { params, .. } => {
2936 prop_assert_eq!(
2937 params.get("id"),
2938 Some(¶m_value2.to_string()),
2939 "Parameter should be extracted correctly"
2940 );
2941 }
2942 _ => {
2943 prop_assert!(false, "Route '{}' should be found", path2_param);
2944 }
2945 }
2946 }
2947
2948 #[test]
2953 fn prop_multiple_routers_preserve_methods(
2954 prefix1 in "[a-z][a-z0-9]{1,5}",
2955 prefix2 in "[a-z][a-z0-9]{1,5}",
2956 route_segment in "[a-z][a-z0-9]{1,5}",
2957 router1_use_get in any::<bool>(),
2958 router1_use_post in any::<bool>(),
2959 router2_use_get in any::<bool>(),
2960 router2_use_put in any::<bool>(),
2961 ) {
2962 prop_assume!(router1_use_get || router1_use_post);
2964 prop_assume!(router2_use_get || router2_use_put);
2965 prop_assume!(prefix1 != prefix2);
2967
2968 let prefix1 = format!("/{}", prefix1);
2969 let prefix2 = format!("/{}", prefix2);
2970 let route_path = format!("/{}", route_segment);
2971
2972 async fn handler() -> &'static str { "handler" }
2973
2974 let mut method_router1 = MethodRouter::new();
2976 let mut expected_methods1: Vec<Method> = Vec::new();
2977 if router1_use_get {
2978 let get_router = get(handler);
2979 for (method, h) in get_router.handlers {
2980 method_router1.handlers.insert(method.clone(), h);
2981 expected_methods1.push(method);
2982 }
2983 }
2984 if router1_use_post {
2985 let post_router = post(handler);
2986 for (method, h) in post_router.handlers {
2987 method_router1.handlers.insert(method.clone(), h);
2988 expected_methods1.push(method);
2989 }
2990 }
2991
2992 let mut method_router2 = MethodRouter::new();
2994 let mut expected_methods2: Vec<Method> = Vec::new();
2995 if router2_use_get {
2996 let get_router = get(handler);
2997 for (method, h) in get_router.handlers {
2998 method_router2.handlers.insert(method.clone(), h);
2999 expected_methods2.push(method);
3000 }
3001 }
3002 if router2_use_put {
3003 let put_router = put(handler);
3004 for (method, h) in put_router.handlers {
3005 method_router2.handlers.insert(method.clone(), h);
3006 expected_methods2.push(method);
3007 }
3008 }
3009
3010 let router1 = Router::new().route(&route_path, method_router1);
3011 let router2 = Router::new().route(&route_path, method_router2);
3012
3013 let app = Router::new()
3014 .nest(&prefix1, router1)
3015 .nest(&prefix2, router2);
3016
3017 let full_path1 = format!("{}{}", prefix1, route_path);
3018 let full_path2 = format!("{}{}", prefix2, route_path);
3019
3020 for method in &expected_methods1 {
3022 match app.match_route(&full_path1, method) {
3023 RouteMatch::Found { .. } => {}
3024 _ => {
3025 prop_assert!(false, "Method {:?} should be found for {}", method, full_path1);
3026 }
3027 }
3028 }
3029
3030 for method in &expected_methods2 {
3032 match app.match_route(&full_path2, method) {
3033 RouteMatch::Found { .. } => {}
3034 _ => {
3035 prop_assert!(false, "Method {:?} should be found for {}", method, full_path2);
3036 }
3037 }
3038 }
3039
3040 if !expected_methods1.contains(&Method::DELETE) {
3042 match app.match_route(&full_path1, &Method::DELETE) {
3043 RouteMatch::MethodNotAllowed { allowed } => {
3044 for method in &expected_methods1 {
3045 prop_assert!(
3046 allowed.contains(method),
3047 "Allowed methods for {} should contain {:?}",
3048 full_path1, method
3049 );
3050 }
3051 }
3052 _ => {
3053 prop_assert!(false, "DELETE should return MethodNotAllowed for {}", full_path1);
3054 }
3055 }
3056 }
3057 }
3058
3059 #[test]
3064 fn prop_three_routers_composition(
3065 prefix1 in "[a-z]{1,3}",
3066 prefix2 in "[a-z]{4,6}",
3067 prefix3 in "[a-z]{7,9}",
3068 num_routes in 1..3usize,
3069 ) {
3070 let prefix1 = format!("/{}", prefix1);
3071 let prefix2 = format!("/{}", prefix2);
3072 let prefix3 = format!("/{}", prefix3);
3073
3074 async fn handler() -> &'static str { "handler" }
3075
3076 let mut router1 = Router::new();
3078 let mut router2 = Router::new();
3079 let mut router3 = Router::new();
3080
3081 for i in 0..num_routes {
3082 let path = format!("/item{}", i);
3083 router1 = router1.route(&path, get(handler));
3084 router2 = router2.route(&path, get(handler));
3085 router3 = router3.route(&path, get(handler));
3086 }
3087
3088 let app = Router::new()
3090 .nest(&prefix1, router1)
3091 .nest(&prefix2, router2)
3092 .nest(&prefix3, router3);
3093
3094 let routes = app.registered_routes();
3095
3096 let expected_count = 3 * num_routes;
3098 prop_assert_eq!(
3099 routes.len(),
3100 expected_count,
3101 "Should have {} routes, got {}",
3102 expected_count, routes.len()
3103 );
3104
3105 for i in 0..num_routes {
3107 let path1 = format!("{}/item{}", prefix1, i);
3108 let path2 = format!("{}/item{}", prefix2, i);
3109 let path3 = format!("{}/item{}", prefix3, i);
3110
3111 match app.match_route(&path1, &Method::GET) {
3112 RouteMatch::Found { .. } => {}
3113 _ => prop_assert!(false, "Route '{}' should be found", path1),
3114 }
3115 match app.match_route(&path2, &Method::GET) {
3116 RouteMatch::Found { .. } => {}
3117 _ => prop_assert!(false, "Route '{}' should be found", path2),
3118 }
3119 match app.match_route(&path3, &Method::GET) {
3120 RouteMatch::Found { .. } => {}
3121 _ => prop_assert!(false, "Route '{}' should be found", path3),
3122 }
3123 }
3124 }
3125 }
3126 proptest! {
3127 #![proptest_config(ProptestConfig::with_cases(100))]
3128
3129 #[test]
3135 fn prop_nested_route_conflict_different_param_names(
3136 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3137 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
3138 param1 in "[a-z][a-z0-9]{1,5}",
3139 param2 in "[a-z][a-z0-9]{1,5}",
3140 ) {
3141 prop_assume!(param1 != param2);
3143
3144 async fn handler1() -> &'static str { "handler1" }
3145 async fn handler2() -> &'static str { "handler2" }
3146
3147 let prefix = format!("/{}", prefix_segments.join("/"));
3148
3149 let existing_path = if route_segments.is_empty() {
3151 format!("{}/{{{}}}", prefix, param1)
3152 } else {
3153 format!("{}/{}/{{{}}}", prefix, route_segments.join("/"), param1)
3154 };
3155
3156 let nested_path = if route_segments.is_empty() {
3158 format!("/{{{}}}", param2)
3159 } else {
3160 format!("/{}/{{{}}}", route_segments.join("/"), param2)
3161 };
3162
3163 let result = catch_unwind(AssertUnwindSafe(|| {
3165 let parent = Router::new().route(&existing_path, get(handler1));
3166 let nested = Router::new().route(&nested_path, get(handler2));
3167 let _app = parent.nest(&prefix, nested);
3168 }));
3169
3170 prop_assert!(
3172 result.is_err(),
3173 "Nested route '{}{}' should conflict with existing route '{}' but didn't",
3174 prefix, nested_path, existing_path
3175 );
3176
3177 if let Err(panic_info) = result {
3179 if let Some(msg) = panic_info.downcast_ref::<String>() {
3180 prop_assert!(
3181 msg.contains("ROUTE CONFLICT DETECTED"),
3182 "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
3183 msg
3184 );
3185 prop_assert!(
3186 msg.contains("Existing:") && msg.contains("New:"),
3187 "Error should contain both 'Existing:' and 'New:' labels, got: {}",
3188 msg
3189 );
3190 }
3191 }
3192 }
3193
3194 #[test]
3199 fn prop_nested_route_conflict_exact_same_path(
3200 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3201 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3202 ) {
3203 async fn handler1() -> &'static str { "handler1" }
3204 async fn handler2() -> &'static str { "handler2" }
3205
3206 let prefix = format!("/{}", prefix_segments.join("/"));
3207 let route_path = format!("/{}", route_segments.join("/"));
3208
3209 let existing_path = format!("{}{}", prefix, route_path);
3211
3212 let result = catch_unwind(AssertUnwindSafe(|| {
3214 let parent = Router::new().route(&existing_path, get(handler1));
3215 let nested = Router::new().route(&route_path, get(handler2));
3216 let _app = parent.nest(&prefix, nested);
3217 }));
3218
3219 prop_assert!(
3221 result.is_err(),
3222 "Nested route '{}{}' should conflict with existing route '{}' but didn't",
3223 prefix, route_path, existing_path
3224 );
3225 }
3226
3227 #[test]
3232 fn prop_nested_routes_different_prefixes_no_conflict(
3233 prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3234 prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3235 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3236 has_param in any::<bool>(),
3237 ) {
3238 let prefix1 = format!("/{}", prefix1_segments.join("/"));
3240 let prefix2 = format!("/{}", prefix2_segments.join("/"));
3241
3242 prop_assume!(prefix1 != prefix2);
3244
3245 async fn handler1() -> &'static str { "handler1" }
3246 async fn handler2() -> &'static str { "handler2" }
3247
3248 let route_path = if has_param {
3250 format!("/{}/{{id}}", route_segments.join("/"))
3251 } else {
3252 format!("/{}", route_segments.join("/"))
3253 };
3254
3255 let result = catch_unwind(AssertUnwindSafe(|| {
3257 let nested1 = Router::new().route(&route_path, get(handler1));
3258 let nested2 = Router::new().route(&route_path, get(handler2));
3259
3260 let app = Router::new()
3261 .nest(&prefix1, nested1)
3262 .nest(&prefix2, nested2);
3263
3264 app.registered_routes().len()
3265 }));
3266
3267 prop_assert!(
3269 result.is_ok(),
3270 "Routes under different prefixes '{}' and '{}' should not conflict",
3271 prefix1, prefix2
3272 );
3273
3274 if let Ok(count) = result {
3275 prop_assert_eq!(count, 2, "Should have registered 2 routes");
3276 }
3277 }
3278
3279 #[test]
3284 fn prop_nested_conflict_error_contains_guidance(
3285 prefix in "[a-z][a-z0-9]{1,5}",
3286 segment in "[a-z][a-z0-9]{1,5}",
3287 param1 in "[a-z][a-z0-9]{1,5}",
3288 param2 in "[a-z][a-z0-9]{1,5}",
3289 ) {
3290 prop_assume!(param1 != param2);
3291
3292 async fn handler1() -> &'static str { "handler1" }
3293 async fn handler2() -> &'static str { "handler2" }
3294
3295 let prefix = format!("/{}", prefix);
3296 let existing_path = format!("{}/{}/{{{}}}", prefix, segment, param1);
3297 let nested_path = format!("/{}/{{{}}}", segment, param2);
3298
3299 let result = catch_unwind(AssertUnwindSafe(|| {
3300 let parent = Router::new().route(&existing_path, get(handler1));
3301 let nested = Router::new().route(&nested_path, get(handler2));
3302 let _app = parent.nest(&prefix, nested);
3303 }));
3304
3305 prop_assert!(result.is_err(), "Should have detected conflict");
3306
3307 if let Err(panic_info) = result {
3308 if let Some(msg) = panic_info.downcast_ref::<String>() {
3309 prop_assert!(
3310 msg.contains("How to resolve:"),
3311 "Error should contain 'How to resolve:' guidance, got: {}",
3312 msg
3313 );
3314 prop_assert!(
3315 msg.contains("Use different path patterns") ||
3316 msg.contains("different path patterns"),
3317 "Error should suggest using different path patterns, got: {}",
3318 msg
3319 );
3320 }
3321 }
3322 }
3323 }
3324}