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 pub(crate) component_registrars: Vec<fn(&mut rustapi_openapi::OpenApiSpec)>,
130}
131
132impl Clone for MethodRouter {
133 fn clone(&self) -> Self {
134 Self {
135 handlers: self.handlers.clone(),
136 operations: self.operations.clone(),
137 component_registrars: self.component_registrars.clone(),
138 }
139 }
140}
141
142impl MethodRouter {
143 pub fn new() -> Self {
145 Self {
146 handlers: HashMap::new(),
147 operations: HashMap::new(),
148 component_registrars: Vec::new(),
149 }
150 }
151
152 fn on(
154 mut self,
155 method: Method,
156 handler: BoxedHandler,
157 operation: Operation,
158 component_registrar: fn(&mut rustapi_openapi::OpenApiSpec),
159 ) -> Self {
160 self.handlers.insert(method.clone(), handler);
161 self.operations.insert(method, operation);
162 self.component_registrars.push(component_registrar);
163 self
164 }
165
166 pub(crate) fn get_handler(&self, method: &Method) -> Option<&BoxedHandler> {
168 self.handlers.get(method)
169 }
170
171 pub(crate) fn allowed_methods(&self) -> Vec<Method> {
173 self.handlers.keys().cloned().collect()
174 }
175
176 pub(crate) fn from_boxed(handlers: HashMap<Method, BoxedHandler>) -> Self {
178 Self {
179 handlers,
180 operations: HashMap::new(), component_registrars: Vec::new(),
182 }
183 }
184
185 pub(crate) fn insert_boxed_with_operation(
189 &mut self,
190 method: Method,
191 handler: BoxedHandler,
192 operation: Operation,
193 ) {
194 if self.handlers.contains_key(&method) {
195 panic!(
196 "Duplicate handler for method {} on the same path",
197 method.as_str()
198 );
199 }
200
201 self.handlers.insert(method.clone(), handler);
202 self.operations.insert(method, operation);
203 }
204
205 pub fn get<H, T>(self, handler: H) -> Self
207 where
208 H: Handler<T>,
209 T: 'static,
210 {
211 let mut op = Operation::new();
212 H::update_operation(&mut op);
213 self.on(
214 Method::GET,
215 into_boxed_handler(handler),
216 op,
217 <H as Handler<T>>::register_components,
218 )
219 }
220
221 pub fn post<H, T>(self, handler: H) -> Self
223 where
224 H: Handler<T>,
225 T: 'static,
226 {
227 let mut op = Operation::new();
228 H::update_operation(&mut op);
229 self.on(
230 Method::POST,
231 into_boxed_handler(handler),
232 op,
233 <H as Handler<T>>::register_components,
234 )
235 }
236
237 pub fn put<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(
246 Method::PUT,
247 into_boxed_handler(handler),
248 op,
249 <H as Handler<T>>::register_components,
250 )
251 }
252
253 pub fn patch<H, T>(self, handler: H) -> Self
255 where
256 H: Handler<T>,
257 T: 'static,
258 {
259 let mut op = Operation::new();
260 H::update_operation(&mut op);
261 self.on(
262 Method::PATCH,
263 into_boxed_handler(handler),
264 op,
265 <H as Handler<T>>::register_components,
266 )
267 }
268
269 pub fn delete<H, T>(self, handler: H) -> Self
271 where
272 H: Handler<T>,
273 T: 'static,
274 {
275 let mut op = Operation::new();
276 H::update_operation(&mut op);
277 self.on(
278 Method::DELETE,
279 into_boxed_handler(handler),
280 op,
281 <H as Handler<T>>::register_components,
282 )
283 }
284}
285
286impl Default for MethodRouter {
287 fn default() -> Self {
288 Self::new()
289 }
290}
291
292pub fn get<H, T>(handler: H) -> MethodRouter
294where
295 H: Handler<T>,
296 T: 'static,
297{
298 let mut op = Operation::new();
299 H::update_operation(&mut op);
300 MethodRouter::new().on(
301 Method::GET,
302 into_boxed_handler(handler),
303 op,
304 <H as Handler<T>>::register_components,
305 )
306}
307
308pub fn post<H, T>(handler: H) -> MethodRouter
310where
311 H: Handler<T>,
312 T: 'static,
313{
314 let mut op = Operation::new();
315 H::update_operation(&mut op);
316 MethodRouter::new().on(
317 Method::POST,
318 into_boxed_handler(handler),
319 op,
320 <H as Handler<T>>::register_components,
321 )
322}
323
324pub fn put<H, T>(handler: H) -> MethodRouter
326where
327 H: Handler<T>,
328 T: 'static,
329{
330 let mut op = Operation::new();
331 H::update_operation(&mut op);
332 MethodRouter::new().on(
333 Method::PUT,
334 into_boxed_handler(handler),
335 op,
336 <H as Handler<T>>::register_components,
337 )
338}
339
340pub fn patch<H, T>(handler: H) -> MethodRouter
342where
343 H: Handler<T>,
344 T: 'static,
345{
346 let mut op = Operation::new();
347 H::update_operation(&mut op);
348 MethodRouter::new().on(
349 Method::PATCH,
350 into_boxed_handler(handler),
351 op,
352 <H as Handler<T>>::register_components,
353 )
354}
355
356pub fn delete<H, T>(handler: H) -> MethodRouter
358where
359 H: Handler<T>,
360 T: 'static,
361{
362 let mut op = Operation::new();
363 H::update_operation(&mut op);
364 MethodRouter::new().on(
365 Method::DELETE,
366 into_boxed_handler(handler),
367 op,
368 <H as Handler<T>>::register_components,
369 )
370}
371
372pub struct Router {
374 inner: MatchitRouter<MethodRouter>,
375 state: Arc<Extensions>,
376 registered_routes: HashMap<String, RouteInfo>,
378 method_routers: HashMap<String, MethodRouter>,
380 state_type_ids: Vec<std::any::TypeId>,
383}
384
385impl Router {
386 pub fn new() -> Self {
388 Self {
389 inner: MatchitRouter::new(),
390 state: Arc::new(Extensions::new()),
391 registered_routes: HashMap::new(),
392 method_routers: HashMap::new(),
393 state_type_ids: Vec::new(),
394 }
395 }
396
397 pub fn typed<P: TypedPath>(self, method_router: MethodRouter) -> Self {
399 self.route(P::PATH, method_router)
400 }
401
402 pub fn route(mut self, path: &str, method_router: MethodRouter) -> Self {
404 let matchit_path = convert_path_params(path);
406
407 let methods: Vec<Method> = method_router.handlers.keys().cloned().collect();
409
410 self.method_routers
412 .insert(matchit_path.clone(), method_router.clone());
413
414 match self.inner.insert(matchit_path.clone(), method_router) {
415 Ok(_) => {
416 self.registered_routes.insert(
418 matchit_path.clone(),
419 RouteInfo {
420 path: path.to_string(),
421 methods,
422 },
423 );
424 }
425 Err(e) => {
426 self.method_routers.remove(&matchit_path);
428
429 let existing_path = self
431 .find_conflicting_route(&matchit_path)
432 .map(|info| info.path.clone())
433 .unwrap_or_else(|| "<unknown>".to_string());
434
435 let conflict_error = RouteConflictError {
436 new_path: path.to_string(),
437 method: methods.first().cloned(),
438 existing_path,
439 details: e.to_string(),
440 };
441
442 panic!("{}", conflict_error);
443 }
444 }
445 self
446 }
447
448 fn find_conflicting_route(&self, matchit_path: &str) -> Option<&RouteInfo> {
450 if let Some(info) = self.registered_routes.get(matchit_path) {
452 return Some(info);
453 }
454
455 let normalized_new = normalize_path_for_comparison(matchit_path);
457
458 for (registered_path, info) in &self.registered_routes {
459 let normalized_existing = normalize_path_for_comparison(registered_path);
460 if normalized_new == normalized_existing {
461 return Some(info);
462 }
463 }
464
465 None
466 }
467
468 pub fn state<S: Clone + Send + Sync + 'static>(mut self, state: S) -> Self {
470 let type_id = std::any::TypeId::of::<S>();
471 let extensions = Arc::make_mut(&mut self.state);
472 extensions.insert(state);
473 if !self.state_type_ids.contains(&type_id) {
474 self.state_type_ids.push(type_id);
475 }
476 self
477 }
478
479 pub fn has_state<S: 'static>(&self) -> bool {
481 self.state_type_ids.contains(&std::any::TypeId::of::<S>())
482 }
483
484 pub fn state_type_ids(&self) -> &[std::any::TypeId] {
486 &self.state_type_ids
487 }
488
489 pub fn nest(mut self, prefix: &str, router: Router) -> Self {
557 let normalized_prefix = normalize_prefix(prefix);
559
560 for type_id in &router.state_type_ids {
564 if !self.state_type_ids.contains(type_id) {
565 self.state_type_ids.push(*type_id);
566 }
567 }
568
569 let nested_routes: Vec<(String, RouteInfo, MethodRouter)> = router
572 .registered_routes
573 .into_iter()
574 .filter_map(|(matchit_path, route_info)| {
575 router
576 .method_routers
577 .get(&matchit_path)
578 .map(|mr| (matchit_path, route_info, mr.clone()))
579 })
580 .collect();
581
582 for (matchit_path, route_info, method_router) in nested_routes {
584 let prefixed_matchit_path = if matchit_path == "/" {
588 normalized_prefix.clone()
589 } else {
590 format!("{}{}", normalized_prefix, matchit_path)
591 };
592
593 let prefixed_display_path = if route_info.path == "/" {
594 normalized_prefix.clone()
595 } else {
596 format!("{}{}", normalized_prefix, route_info.path)
597 };
598
599 self.method_routers
601 .insert(prefixed_matchit_path.clone(), method_router.clone());
602
603 match self
605 .inner
606 .insert(prefixed_matchit_path.clone(), method_router)
607 {
608 Ok(_) => {
609 self.registered_routes.insert(
611 prefixed_matchit_path,
612 RouteInfo {
613 path: prefixed_display_path,
614 methods: route_info.methods,
615 },
616 );
617 }
618 Err(e) => {
619 self.method_routers.remove(&prefixed_matchit_path);
621
622 let existing_path = self
624 .find_conflicting_route(&prefixed_matchit_path)
625 .map(|info| info.path.clone())
626 .unwrap_or_else(|| "<unknown>".to_string());
627
628 let conflict_error = RouteConflictError {
629 new_path: prefixed_display_path,
630 method: route_info.methods.first().cloned(),
631 existing_path,
632 details: e.to_string(),
633 };
634
635 panic!("{}", conflict_error);
636 }
637 }
638 }
639
640 self
641 }
642
643 pub fn merge_state<S: Clone + Send + Sync + 'static>(mut self, other: &Router) -> Self {
660 let type_id = std::any::TypeId::of::<S>();
661
662 if !self.state_type_ids.contains(&type_id) {
664 if let Some(state) = other.state.get::<S>() {
666 let extensions = Arc::make_mut(&mut self.state);
667 extensions.insert(state.clone());
668 self.state_type_ids.push(type_id);
669 }
670 }
671
672 self
673 }
674
675 pub fn match_route(&self, path: &str, method: &Method) -> RouteMatch<'_> {
677 match self.inner.at(path) {
678 Ok(matched) => {
679 let method_router = matched.value;
680
681 if let Some(handler) = method_router.get_handler(method) {
682 let params: PathParams = matched
684 .params
685 .iter()
686 .map(|(k, v)| (k.to_string(), v.to_string()))
687 .collect();
688
689 RouteMatch::Found { handler, params }
690 } else {
691 RouteMatch::MethodNotAllowed {
692 allowed: method_router.allowed_methods(),
693 }
694 }
695 }
696 Err(_) => RouteMatch::NotFound,
697 }
698 }
699
700 pub fn state_ref(&self) -> Arc<Extensions> {
702 self.state.clone()
703 }
704
705 pub fn registered_routes(&self) -> &HashMap<String, RouteInfo> {
707 &self.registered_routes
708 }
709
710 pub fn method_routers(&self) -> &HashMap<String, MethodRouter> {
712 &self.method_routers
713 }
714}
715
716impl Default for Router {
717 fn default() -> Self {
718 Self::new()
719 }
720}
721
722pub enum RouteMatch<'a> {
724 Found {
725 handler: &'a BoxedHandler,
726 params: PathParams,
727 },
728 NotFound,
729 MethodNotAllowed {
730 allowed: Vec<Method>,
731 },
732}
733
734fn convert_path_params(path: &str) -> String {
736 let mut result = String::with_capacity(path.len());
737
738 for ch in path.chars() {
739 match ch {
740 '{' => {
741 result.push(':');
742 }
743 '}' => {
744 }
746 _ => {
747 result.push(ch);
748 }
749 }
750 }
751
752 result
753}
754
755fn normalize_path_for_comparison(path: &str) -> String {
757 let mut result = String::with_capacity(path.len());
758 let mut in_param = false;
759
760 for ch in path.chars() {
761 match ch {
762 ':' => {
763 in_param = true;
764 result.push_str(":_");
765 }
766 '/' => {
767 in_param = false;
768 result.push('/');
769 }
770 _ if in_param => {
771 }
773 _ => {
774 result.push(ch);
775 }
776 }
777 }
778
779 result
780}
781
782pub(crate) fn normalize_prefix(prefix: &str) -> String {
799 if prefix.is_empty() {
801 return "/".to_string();
802 }
803
804 let segments: Vec<&str> = prefix.split('/').filter(|s| !s.is_empty()).collect();
806
807 if segments.is_empty() {
809 return "/".to_string();
810 }
811
812 let mut result = String::with_capacity(prefix.len() + 1);
814 for segment in segments {
815 result.push('/');
816 result.push_str(segment);
817 }
818
819 result
820}
821
822#[cfg(test)]
823mod tests {
824 use super::*;
825
826 #[test]
827 fn test_convert_path_params() {
828 assert_eq!(convert_path_params("/users/{id}"), "/users/:id");
829 assert_eq!(
830 convert_path_params("/users/{user_id}/posts/{post_id}"),
831 "/users/:user_id/posts/:post_id"
832 );
833 assert_eq!(convert_path_params("/static/path"), "/static/path");
834 }
835
836 #[test]
837 fn test_normalize_path_for_comparison() {
838 assert_eq!(normalize_path_for_comparison("/users/:id"), "/users/:_");
839 assert_eq!(
840 normalize_path_for_comparison("/users/:user_id"),
841 "/users/:_"
842 );
843 assert_eq!(
844 normalize_path_for_comparison("/users/:id/posts/:post_id"),
845 "/users/:_/posts/:_"
846 );
847 assert_eq!(
848 normalize_path_for_comparison("/static/path"),
849 "/static/path"
850 );
851 }
852
853 #[test]
854 fn test_normalize_prefix() {
855 assert_eq!(normalize_prefix("api"), "/api");
857 assert_eq!(normalize_prefix("/api"), "/api");
858 assert_eq!(normalize_prefix("/api/"), "/api");
859 assert_eq!(normalize_prefix("api/"), "/api");
860
861 assert_eq!(normalize_prefix("api/v1"), "/api/v1");
863 assert_eq!(normalize_prefix("/api/v1"), "/api/v1");
864 assert_eq!(normalize_prefix("/api/v1/"), "/api/v1");
865
866 assert_eq!(normalize_prefix(""), "/");
868 assert_eq!(normalize_prefix("/"), "/");
869
870 assert_eq!(normalize_prefix("//api"), "/api");
872 assert_eq!(normalize_prefix("api//v1"), "/api/v1");
873 assert_eq!(normalize_prefix("//api//v1//"), "/api/v1");
874 assert_eq!(normalize_prefix("///"), "/");
875 }
876
877 #[test]
878 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
879 fn test_route_conflict_detection() {
880 async fn handler1() -> &'static str {
881 "handler1"
882 }
883 async fn handler2() -> &'static str {
884 "handler2"
885 }
886
887 let _router = Router::new()
888 .route("/users/{id}", get(handler1))
889 .route("/users/{user_id}", get(handler2)); }
891
892 #[test]
893 fn test_no_conflict_different_paths() {
894 async fn handler1() -> &'static str {
895 "handler1"
896 }
897 async fn handler2() -> &'static str {
898 "handler2"
899 }
900
901 let router = Router::new()
902 .route("/users/{id}", get(handler1))
903 .route("/users/{id}/profile", get(handler2));
904
905 assert_eq!(router.registered_routes().len(), 2);
906 }
907
908 #[test]
909 fn test_route_info_tracking() {
910 async fn handler() -> &'static str {
911 "handler"
912 }
913
914 let router = Router::new().route("/users/{id}", get(handler));
915
916 let routes = router.registered_routes();
917 assert_eq!(routes.len(), 1);
918
919 let info = routes.get("/users/:id").unwrap();
920 assert_eq!(info.path, "/users/{id}");
921 assert_eq!(info.methods.len(), 1);
922 assert_eq!(info.methods[0], Method::GET);
923 }
924
925 #[test]
926 fn test_basic_router_nesting() {
927 async fn list_users() -> &'static str {
928 "list users"
929 }
930 async fn get_user() -> &'static str {
931 "get user"
932 }
933
934 let users_router = Router::new()
935 .route("/", get(list_users))
936 .route("/{id}", get(get_user));
937
938 let app = Router::new().nest("/api/users", users_router);
939
940 let routes = app.registered_routes();
941 assert_eq!(routes.len(), 2);
942
943 assert!(routes.contains_key("/api/users"));
945 assert!(routes.contains_key("/api/users/:id"));
946
947 let list_info = routes.get("/api/users").unwrap();
949 assert_eq!(list_info.path, "/api/users");
950
951 let get_info = routes.get("/api/users/:id").unwrap();
952 assert_eq!(get_info.path, "/api/users/{id}");
953 }
954
955 #[test]
956 fn test_nested_route_matching() {
957 async fn handler() -> &'static str {
958 "handler"
959 }
960
961 let users_router = Router::new().route("/{id}", get(handler));
962
963 let app = Router::new().nest("/api/users", users_router);
964
965 match app.match_route("/api/users/123", &Method::GET) {
967 RouteMatch::Found { params, .. } => {
968 assert_eq!(params.get("id"), Some(&"123".to_string()));
969 }
970 _ => panic!("Route should be found"),
971 }
972 }
973
974 #[test]
975 fn test_nested_route_matching_multiple_params() {
976 async fn handler() -> &'static str {
977 "handler"
978 }
979
980 let posts_router = Router::new().route("/{user_id}/posts/{post_id}", get(handler));
981
982 let app = Router::new().nest("/api", posts_router);
983
984 match app.match_route("/api/42/posts/100", &Method::GET) {
986 RouteMatch::Found { params, .. } => {
987 assert_eq!(params.get("user_id"), Some(&"42".to_string()));
988 assert_eq!(params.get("post_id"), Some(&"100".to_string()));
989 }
990 _ => panic!("Route should be found"),
991 }
992 }
993
994 #[test]
995 fn test_nested_route_matching_static_path() {
996 async fn handler() -> &'static str {
997 "handler"
998 }
999
1000 let health_router = Router::new().route("/health", get(handler));
1001
1002 let app = Router::new().nest("/api/v1", health_router);
1003
1004 match app.match_route("/api/v1/health", &Method::GET) {
1006 RouteMatch::Found { params, .. } => {
1007 assert!(params.is_empty(), "Static path should have no params");
1008 }
1009 _ => panic!("Route should be found"),
1010 }
1011 }
1012
1013 #[test]
1014 fn test_nested_route_not_found() {
1015 async fn handler() -> &'static str {
1016 "handler"
1017 }
1018
1019 let users_router = Router::new().route("/users", get(handler));
1020
1021 let app = Router::new().nest("/api", users_router);
1022
1023 match app.match_route("/api/posts", &Method::GET) {
1025 RouteMatch::NotFound => {
1026 }
1028 _ => panic!("Route should not be found"),
1029 }
1030
1031 match app.match_route("/v2/users", &Method::GET) {
1033 RouteMatch::NotFound => {
1034 }
1036 _ => panic!("Route with wrong prefix should not be found"),
1037 }
1038 }
1039
1040 #[test]
1041 fn test_nested_route_method_not_allowed() {
1042 async fn handler() -> &'static str {
1043 "handler"
1044 }
1045
1046 let users_router = Router::new().route("/users", get(handler));
1047
1048 let app = Router::new().nest("/api", users_router);
1049
1050 match app.match_route("/api/users", &Method::POST) {
1052 RouteMatch::MethodNotAllowed { allowed } => {
1053 assert!(allowed.contains(&Method::GET));
1054 assert!(!allowed.contains(&Method::POST));
1055 }
1056 _ => panic!("Should return MethodNotAllowed"),
1057 }
1058 }
1059
1060 #[test]
1061 fn test_nested_route_multiple_methods() {
1062 async fn get_handler() -> &'static str {
1063 "get"
1064 }
1065 async fn post_handler() -> &'static str {
1066 "post"
1067 }
1068
1069 let get_router = get(get_handler);
1071 let post_router = post(post_handler);
1072 let mut combined = MethodRouter::new();
1073 for (method, handler) in get_router.handlers {
1074 combined.handlers.insert(method, handler);
1075 }
1076 for (method, handler) in post_router.handlers {
1077 combined.handlers.insert(method, handler);
1078 }
1079
1080 let users_router = Router::new().route("/users", combined);
1081 let app = Router::new().nest("/api", users_router);
1082
1083 match app.match_route("/api/users", &Method::GET) {
1085 RouteMatch::Found { .. } => {}
1086 _ => panic!("GET should be found"),
1087 }
1088
1089 match app.match_route("/api/users", &Method::POST) {
1090 RouteMatch::Found { .. } => {}
1091 _ => panic!("POST should be found"),
1092 }
1093
1094 match app.match_route("/api/users", &Method::DELETE) {
1096 RouteMatch::MethodNotAllowed { allowed } => {
1097 assert!(allowed.contains(&Method::GET));
1098 assert!(allowed.contains(&Method::POST));
1099 }
1100 _ => panic!("DELETE should return MethodNotAllowed"),
1101 }
1102 }
1103
1104 #[test]
1105 fn test_nested_router_prefix_normalization() {
1106 async fn handler() -> &'static str {
1107 "handler"
1108 }
1109
1110 let router1 = Router::new().route("/test", get(handler));
1112 let app1 = Router::new().nest("api", router1);
1113 assert!(app1.registered_routes().contains_key("/api/test"));
1114
1115 let router2 = Router::new().route("/test", get(handler));
1116 let app2 = Router::new().nest("/api/", router2);
1117 assert!(app2.registered_routes().contains_key("/api/test"));
1118
1119 let router3 = Router::new().route("/test", get(handler));
1120 let app3 = Router::new().nest("//api//", router3);
1121 assert!(app3.registered_routes().contains_key("/api/test"));
1122 }
1123
1124 #[test]
1125 fn test_state_tracking() {
1126 #[derive(Clone)]
1127 struct MyState(#[allow(dead_code)] String);
1128
1129 let router = Router::new().state(MyState("test".to_string()));
1130
1131 assert!(router.has_state::<MyState>());
1132 assert!(!router.has_state::<String>());
1133 }
1134
1135 #[test]
1136 fn test_state_merge_nested_only() {
1137 #[derive(Clone, PartialEq, Debug)]
1138 struct NestedState(String);
1139
1140 async fn handler() -> &'static str {
1141 "handler"
1142 }
1143
1144 let state_source = Router::new().state(NestedState("nested".to_string()));
1146
1147 let nested = Router::new().route("/test", get(handler));
1148
1149 let parent = Router::new()
1150 .nest("/api", nested)
1151 .merge_state::<NestedState>(&state_source);
1152
1153 assert!(parent.has_state::<NestedState>());
1155
1156 let state = parent.state.get::<NestedState>().unwrap();
1158 assert_eq!(state.0, "nested");
1159 }
1160
1161 #[test]
1162 fn test_state_merge_parent_wins() {
1163 #[derive(Clone, PartialEq, Debug)]
1164 struct SharedState(String);
1165
1166 async fn handler() -> &'static str {
1167 "handler"
1168 }
1169
1170 let state_source = Router::new().state(SharedState("nested".to_string()));
1172
1173 let nested = Router::new().route("/test", get(handler));
1174
1175 let parent = Router::new()
1176 .state(SharedState("parent".to_string()))
1177 .nest("/api", nested)
1178 .merge_state::<SharedState>(&state_source);
1179
1180 assert!(parent.has_state::<SharedState>());
1182
1183 let state = parent.state.get::<SharedState>().unwrap();
1185 assert_eq!(state.0, "parent");
1186 }
1187
1188 #[test]
1189 fn test_state_type_ids_merged_on_nest() {
1190 #[derive(Clone)]
1191 struct NestedState(#[allow(dead_code)] String);
1192
1193 async fn handler() -> &'static str {
1194 "handler"
1195 }
1196
1197 let nested = Router::new()
1198 .route("/test", get(handler))
1199 .state(NestedState("nested".to_string()));
1200
1201 let parent = Router::new().nest("/api", nested);
1202
1203 assert!(parent
1205 .state_type_ids()
1206 .contains(&std::any::TypeId::of::<NestedState>()));
1207 }
1208
1209 #[test]
1210 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
1211 fn test_nested_route_conflict_with_existing_route() {
1212 async fn handler1() -> &'static str {
1213 "handler1"
1214 }
1215 async fn handler2() -> &'static str {
1216 "handler2"
1217 }
1218
1219 let parent = Router::new().route("/api/users/{id}", get(handler1));
1221
1222 let nested = Router::new().route("/{user_id}", get(handler2));
1224
1225 let _app = parent.nest("/api/users", nested);
1227 }
1228
1229 #[test]
1230 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
1231 fn test_nested_route_conflict_same_path_different_param_names() {
1232 async fn handler1() -> &'static str {
1233 "handler1"
1234 }
1235 async fn handler2() -> &'static str {
1236 "handler2"
1237 }
1238
1239 let nested1 = Router::new().route("/{id}", get(handler1));
1241 let nested2 = Router::new().route("/{user_id}", get(handler2));
1242
1243 let _app = Router::new()
1245 .nest("/api/users", nested1)
1246 .nest("/api/users", nested2);
1247 }
1248
1249 #[test]
1250 fn test_nested_route_conflict_error_contains_both_paths() {
1251 use std::panic::{catch_unwind, AssertUnwindSafe};
1252
1253 async fn handler1() -> &'static str {
1254 "handler1"
1255 }
1256 async fn handler2() -> &'static str {
1257 "handler2"
1258 }
1259
1260 let result = catch_unwind(AssertUnwindSafe(|| {
1261 let parent = Router::new().route("/api/users/{id}", get(handler1));
1262 let nested = Router::new().route("/{user_id}", get(handler2));
1263 let _app = parent.nest("/api/users", nested);
1264 }));
1265
1266 assert!(result.is_err(), "Should have panicked due to conflict");
1267
1268 if let Err(panic_info) = result {
1269 if let Some(msg) = panic_info.downcast_ref::<String>() {
1270 assert!(
1271 msg.contains("ROUTE CONFLICT DETECTED"),
1272 "Error should contain 'ROUTE CONFLICT DETECTED'"
1273 );
1274 assert!(
1275 msg.contains("Existing:") && msg.contains("New:"),
1276 "Error should contain both 'Existing:' and 'New:' labels"
1277 );
1278 assert!(
1279 msg.contains("How to resolve:"),
1280 "Error should contain resolution guidance"
1281 );
1282 }
1283 }
1284 }
1285
1286 #[test]
1287 fn test_nested_routes_no_conflict_different_prefixes() {
1288 async fn handler1() -> &'static str {
1289 "handler1"
1290 }
1291 async fn handler2() -> &'static str {
1292 "handler2"
1293 }
1294
1295 let nested1 = Router::new().route("/{id}", get(handler1));
1297 let nested2 = Router::new().route("/{id}", get(handler2));
1298
1299 let app = Router::new()
1301 .nest("/api/users", nested1)
1302 .nest("/api/posts", nested2);
1303
1304 assert_eq!(app.registered_routes().len(), 2);
1305 assert!(app.registered_routes().contains_key("/api/users/:id"));
1306 assert!(app.registered_routes().contains_key("/api/posts/:id"));
1307 }
1308
1309 #[test]
1314 fn test_multiple_router_composition_all_routes_registered() {
1315 async fn users_list() -> &'static str {
1316 "users list"
1317 }
1318 async fn users_get() -> &'static str {
1319 "users get"
1320 }
1321 async fn posts_list() -> &'static str {
1322 "posts list"
1323 }
1324 async fn posts_get() -> &'static str {
1325 "posts get"
1326 }
1327 async fn comments_list() -> &'static str {
1328 "comments list"
1329 }
1330
1331 let users_router = Router::new()
1333 .route("/", get(users_list))
1334 .route("/{id}", get(users_get));
1335
1336 let posts_router = Router::new()
1337 .route("/", get(posts_list))
1338 .route("/{id}", get(posts_get));
1339
1340 let comments_router = Router::new().route("/", get(comments_list));
1341
1342 let app = Router::new()
1344 .nest("/api/users", users_router)
1345 .nest("/api/posts", posts_router)
1346 .nest("/api/comments", comments_router);
1347
1348 let routes = app.registered_routes();
1350 assert_eq!(routes.len(), 5, "Should have 5 routes registered");
1351
1352 assert!(
1354 routes.contains_key("/api/users"),
1355 "Should have /api/users route"
1356 );
1357 assert!(
1358 routes.contains_key("/api/users/:id"),
1359 "Should have /api/users/:id route"
1360 );
1361
1362 assert!(
1364 routes.contains_key("/api/posts"),
1365 "Should have /api/posts route"
1366 );
1367 assert!(
1368 routes.contains_key("/api/posts/:id"),
1369 "Should have /api/posts/:id route"
1370 );
1371
1372 assert!(
1374 routes.contains_key("/api/comments"),
1375 "Should have /api/comments route"
1376 );
1377 }
1378
1379 #[test]
1380 fn test_multiple_router_composition_no_interference() {
1381 async fn users_handler() -> &'static str {
1382 "users"
1383 }
1384 async fn posts_handler() -> &'static str {
1385 "posts"
1386 }
1387 async fn admin_handler() -> &'static str {
1388 "admin"
1389 }
1390
1391 let users_router = Router::new()
1393 .route("/list", get(users_handler))
1394 .route("/{id}", get(users_handler));
1395
1396 let posts_router = Router::new()
1397 .route("/list", get(posts_handler))
1398 .route("/{id}", get(posts_handler));
1399
1400 let admin_router = Router::new()
1401 .route("/list", get(admin_handler))
1402 .route("/{id}", get(admin_handler));
1403
1404 let app = Router::new()
1406 .nest("/api/v1/users", users_router)
1407 .nest("/api/v1/posts", posts_router)
1408 .nest("/admin", admin_router);
1409
1410 let routes = app.registered_routes();
1412 assert_eq!(routes.len(), 6, "Should have 6 routes registered");
1413
1414 assert!(routes.contains_key("/api/v1/users/list"));
1416 assert!(routes.contains_key("/api/v1/users/:id"));
1417 assert!(routes.contains_key("/api/v1/posts/list"));
1418 assert!(routes.contains_key("/api/v1/posts/:id"));
1419 assert!(routes.contains_key("/admin/list"));
1420 assert!(routes.contains_key("/admin/:id"));
1421
1422 match app.match_route("/api/v1/users/list", &Method::GET) {
1424 RouteMatch::Found { params, .. } => {
1425 assert!(params.is_empty(), "Static path should have no params");
1426 }
1427 _ => panic!("Should find /api/v1/users/list"),
1428 }
1429
1430 match app.match_route("/api/v1/posts/123", &Method::GET) {
1431 RouteMatch::Found { params, .. } => {
1432 assert_eq!(params.get("id"), Some(&"123".to_string()));
1433 }
1434 _ => panic!("Should find /api/v1/posts/123"),
1435 }
1436
1437 match app.match_route("/admin/456", &Method::GET) {
1438 RouteMatch::Found { params, .. } => {
1439 assert_eq!(params.get("id"), Some(&"456".to_string()));
1440 }
1441 _ => panic!("Should find /admin/456"),
1442 }
1443 }
1444
1445 #[test]
1446 fn test_multiple_router_composition_with_multiple_methods() {
1447 async fn get_handler() -> &'static str {
1448 "get"
1449 }
1450 async fn post_handler() -> &'static str {
1451 "post"
1452 }
1453 async fn put_handler() -> &'static str {
1454 "put"
1455 }
1456
1457 let get_router = get(get_handler);
1460 let post_router = post(post_handler);
1461 let mut users_root_combined = MethodRouter::new();
1462 for (method, handler) in get_router.handlers {
1463 users_root_combined.handlers.insert(method, handler);
1464 }
1465 for (method, handler) in post_router.handlers {
1466 users_root_combined.handlers.insert(method, handler);
1467 }
1468
1469 let get_router2 = get(get_handler);
1471 let put_router = put(put_handler);
1472 let mut users_id_combined = MethodRouter::new();
1473 for (method, handler) in get_router2.handlers {
1474 users_id_combined.handlers.insert(method, handler);
1475 }
1476 for (method, handler) in put_router.handlers {
1477 users_id_combined.handlers.insert(method, handler);
1478 }
1479
1480 let users_router = Router::new()
1481 .route("/", users_root_combined)
1482 .route("/{id}", users_id_combined);
1483
1484 let get_router3 = get(get_handler);
1486 let post_router2 = post(post_handler);
1487 let mut posts_root_combined = MethodRouter::new();
1488 for (method, handler) in get_router3.handlers {
1489 posts_root_combined.handlers.insert(method, handler);
1490 }
1491 for (method, handler) in post_router2.handlers {
1492 posts_root_combined.handlers.insert(method, handler);
1493 }
1494
1495 let posts_router = Router::new().route("/", posts_root_combined);
1496
1497 let app = Router::new()
1499 .nest("/users", users_router)
1500 .nest("/posts", posts_router);
1501
1502 let routes = app.registered_routes();
1504 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1505
1506 let users_root = routes.get("/users").unwrap();
1508 assert!(users_root.methods.contains(&Method::GET));
1509 assert!(users_root.methods.contains(&Method::POST));
1510
1511 let users_id = routes.get("/users/:id").unwrap();
1512 assert!(users_id.methods.contains(&Method::GET));
1513 assert!(users_id.methods.contains(&Method::PUT));
1514
1515 let posts_root = routes.get("/posts").unwrap();
1517 assert!(posts_root.methods.contains(&Method::GET));
1518 assert!(posts_root.methods.contains(&Method::POST));
1519
1520 match app.match_route("/users", &Method::GET) {
1522 RouteMatch::Found { .. } => {}
1523 _ => panic!("GET /users should be found"),
1524 }
1525 match app.match_route("/users", &Method::POST) {
1526 RouteMatch::Found { .. } => {}
1527 _ => panic!("POST /users should be found"),
1528 }
1529 match app.match_route("/users/123", &Method::PUT) {
1530 RouteMatch::Found { .. } => {}
1531 _ => panic!("PUT /users/123 should be found"),
1532 }
1533 }
1534
1535 #[test]
1536 fn test_multiple_router_composition_deep_nesting() {
1537 async fn handler() -> &'static str {
1538 "handler"
1539 }
1540
1541 let deep_router = Router::new().route("/action", get(handler));
1543
1544 let mid_router = Router::new().route("/info", get(handler));
1545
1546 let shallow_router = Router::new().route("/status", get(handler));
1547
1548 let app = Router::new()
1550 .nest("/api/v1/resources/items", deep_router)
1551 .nest("/api/v1/resources", mid_router)
1552 .nest("/api", shallow_router);
1553
1554 let routes = app.registered_routes();
1556 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1557
1558 assert!(routes.contains_key("/api/v1/resources/items/action"));
1559 assert!(routes.contains_key("/api/v1/resources/info"));
1560 assert!(routes.contains_key("/api/status"));
1561
1562 match app.match_route("/api/v1/resources/items/action", &Method::GET) {
1564 RouteMatch::Found { .. } => {}
1565 _ => panic!("Should find deep route"),
1566 }
1567 match app.match_route("/api/v1/resources/info", &Method::GET) {
1568 RouteMatch::Found { .. } => {}
1569 _ => panic!("Should find mid route"),
1570 }
1571 match app.match_route("/api/status", &Method::GET) {
1572 RouteMatch::Found { .. } => {}
1573 _ => panic!("Should find shallow route"),
1574 }
1575 }
1576}
1577
1578#[cfg(test)]
1579mod property_tests {
1580 use super::*;
1581 use proptest::prelude::*;
1582 use std::panic::{catch_unwind, AssertUnwindSafe};
1583
1584 proptest! {
1592 #![proptest_config(ProptestConfig::with_cases(100))]
1593
1594 #[test]
1599 fn prop_normalized_prefix_starts_with_single_slash(
1600 leading_slashes in prop::collection::vec(Just('/'), 0..5),
1602 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 0..4),
1603 trailing_slashes in prop::collection::vec(Just('/'), 0..5),
1604 ) {
1605 let mut prefix = String::new();
1607 for _ in &leading_slashes {
1608 prefix.push('/');
1609 }
1610 for (i, segment) in segments.iter().enumerate() {
1611 if i > 0 {
1612 prefix.push('/');
1613 }
1614 prefix.push_str(segment);
1615 }
1616 for _ in &trailing_slashes {
1617 prefix.push('/');
1618 }
1619
1620 let normalized = normalize_prefix(&prefix);
1621
1622 prop_assert!(
1624 normalized.starts_with('/'),
1625 "Normalized prefix '{}' should start with '/', input was '{}'",
1626 normalized, prefix
1627 );
1628
1629 prop_assert!(
1631 !normalized.starts_with("//"),
1632 "Normalized prefix '{}' should not start with '//', input was '{}'",
1633 normalized, prefix
1634 );
1635 }
1636
1637 #[test]
1642 fn prop_normalized_prefix_no_trailing_slash(
1643 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1644 trailing_slashes in prop::collection::vec(Just('/'), 0..5),
1645 ) {
1646 let mut prefix = String::from("/");
1648 for (i, segment) in segments.iter().enumerate() {
1649 if i > 0 {
1650 prefix.push('/');
1651 }
1652 prefix.push_str(segment);
1653 }
1654 for _ in &trailing_slashes {
1655 prefix.push('/');
1656 }
1657
1658 let normalized = normalize_prefix(&prefix);
1659
1660 prop_assert!(
1662 !normalized.ends_with('/'),
1663 "Normalized prefix '{}' should not end with '/', input was '{}'",
1664 normalized, prefix
1665 );
1666 }
1667
1668 #[test]
1673 fn prop_normalized_prefix_no_double_slashes(
1674 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1676 extra_slashes in prop::collection::vec(0..4usize, 1..4),
1677 ) {
1678 let mut prefix = String::from("/");
1680 for (i, segment) in segments.iter().enumerate() {
1681 if i > 0 {
1682 let num_slashes = extra_slashes.get(i).copied().unwrap_or(1);
1684 for _ in 0..=num_slashes {
1685 prefix.push('/');
1686 }
1687 }
1688 prefix.push_str(segment);
1689 }
1690
1691 let normalized = normalize_prefix(&prefix);
1692
1693 prop_assert!(
1695 !normalized.contains("//"),
1696 "Normalized prefix '{}' should not contain '//', input was '{}'",
1697 normalized, prefix
1698 );
1699 }
1700
1701 #[test]
1706 fn prop_normalized_prefix_preserves_segments(
1707 segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..4),
1708 ) {
1709 let prefix = format!("/{}", segments.join("/"));
1711
1712 let normalized = normalize_prefix(&prefix);
1713
1714 let normalized_segments: Vec<&str> = normalized
1716 .split('/')
1717 .filter(|s| !s.is_empty())
1718 .collect();
1719
1720 prop_assert_eq!(
1721 segments.len(),
1722 normalized_segments.len(),
1723 "Segment count should be preserved"
1724 );
1725
1726 for (original, normalized_seg) in segments.iter().zip(normalized_segments.iter()) {
1727 prop_assert_eq!(
1728 original, normalized_seg,
1729 "Segment content should be preserved"
1730 );
1731 }
1732 }
1733
1734 #[test]
1739 fn prop_empty_or_slashes_normalize_to_root(
1740 num_slashes in 0..10usize,
1741 ) {
1742 let prefix = "/".repeat(num_slashes);
1743
1744 let normalized = normalize_prefix(&prefix);
1745
1746 prop_assert_eq!(
1747 normalized, "/",
1748 "Empty or slash-only prefix '{}' should normalize to '/'",
1749 prefix
1750 );
1751 }
1752 }
1753
1754 proptest! {
1761 #![proptest_config(ProptestConfig::with_cases(100))]
1762
1763 #[test]
1768 fn prop_method_router_clone_preserves_methods(
1769 use_get in any::<bool>(),
1771 use_post in any::<bool>(),
1772 use_put in any::<bool>(),
1773 use_patch in any::<bool>(),
1774 use_delete in any::<bool>(),
1775 ) {
1776 prop_assume!(use_get || use_post || use_put || use_patch || use_delete);
1778
1779 let mut method_router = MethodRouter::new();
1781 let mut expected_methods: Vec<Method> = Vec::new();
1782
1783 async fn handler() -> &'static str { "handler" }
1784
1785 if use_get {
1786 method_router = get(handler);
1787 expected_methods.push(Method::GET);
1788 }
1789
1790 if use_post {
1791 let post_router = post(handler);
1792 for (method, handler) in post_router.handlers {
1793 method_router.handlers.insert(method.clone(), handler);
1794 if !expected_methods.contains(&method) {
1795 expected_methods.push(method);
1796 }
1797 }
1798 }
1799
1800 if use_put {
1801 let put_router = put(handler);
1802 for (method, handler) in put_router.handlers {
1803 method_router.handlers.insert(method.clone(), handler);
1804 if !expected_methods.contains(&method) {
1805 expected_methods.push(method);
1806 }
1807 }
1808 }
1809
1810 if use_patch {
1811 let patch_router = patch(handler);
1812 for (method, handler) in patch_router.handlers {
1813 method_router.handlers.insert(method.clone(), handler);
1814 if !expected_methods.contains(&method) {
1815 expected_methods.push(method);
1816 }
1817 }
1818 }
1819
1820 if use_delete {
1821 let delete_router = delete(handler);
1822 for (method, handler) in delete_router.handlers {
1823 method_router.handlers.insert(method.clone(), handler);
1824 if !expected_methods.contains(&method) {
1825 expected_methods.push(method);
1826 }
1827 }
1828 }
1829
1830 let cloned_router = method_router.clone();
1832
1833 let original_methods = method_router.allowed_methods();
1835 let cloned_methods = cloned_router.allowed_methods();
1836
1837 prop_assert_eq!(
1838 original_methods.len(),
1839 cloned_methods.len(),
1840 "Cloned router should have same number of methods"
1841 );
1842
1843 for method in &expected_methods {
1844 prop_assert!(
1845 cloned_router.get_handler(method).is_some(),
1846 "Cloned router should have handler for method {:?}",
1847 method
1848 );
1849 }
1850
1851 for method in &cloned_methods {
1853 prop_assert!(
1854 cloned_router.get_handler(method).is_some(),
1855 "Handler for {:?} should be accessible after clone",
1856 method
1857 );
1858 }
1859 }
1860 }
1861
1862 proptest! {
1870 #![proptest_config(ProptestConfig::with_cases(100))]
1871
1872 #[test]
1877 fn prop_nested_routes_have_prefix(
1878 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1880 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1882 has_param in any::<bool>(),
1883 ) {
1884 async fn handler() -> &'static str { "handler" }
1885
1886 let prefix = format!("/{}", prefix_segments.join("/"));
1888
1889 let mut route_path = format!("/{}", route_segments.join("/"));
1891 if has_param {
1892 route_path.push_str("/{id}");
1893 }
1894
1895 let nested_router = Router::new().route(&route_path, get(handler));
1897 let app = Router::new().nest(&prefix, nested_router);
1898
1899 let expected_matchit_path = if has_param {
1901 format!("{}/{}/:id", prefix, route_segments.join("/"))
1902 } else {
1903 format!("{}/{}", prefix, route_segments.join("/"))
1904 };
1905
1906 let routes = app.registered_routes();
1907
1908 prop_assert!(
1910 routes.contains_key(&expected_matchit_path),
1911 "Expected route '{}' not found. Available routes: {:?}",
1912 expected_matchit_path,
1913 routes.keys().collect::<Vec<_>>()
1914 );
1915
1916 let route_info = routes.get(&expected_matchit_path).unwrap();
1918 let expected_display_path = format!("{}{}", prefix, route_path);
1919 prop_assert_eq!(
1920 &route_info.path, &expected_display_path,
1921 "Display path should be prefix + original path"
1922 );
1923 }
1924
1925 #[test]
1930 fn prop_route_count_preserved_after_nesting(
1931 num_routes in 1..4usize,
1933 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1934 ) {
1935 async fn handler() -> &'static str { "handler" }
1936
1937 let prefix = format!("/{}", prefix_segments.join("/"));
1938
1939 let mut nested_router = Router::new();
1941 for i in 0..num_routes {
1942 let path = format!("/route{}", i);
1943 nested_router = nested_router.route(&path, get(handler));
1944 }
1945
1946 let app = Router::new().nest(&prefix, nested_router);
1947
1948 prop_assert_eq!(
1949 app.registered_routes().len(),
1950 num_routes,
1951 "Number of routes should be preserved after nesting"
1952 );
1953 }
1954
1955 #[test]
1959 fn prop_nested_routes_are_matchable(
1960 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1961 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1962 ) {
1963 async fn handler() -> &'static str { "handler" }
1964
1965 let prefix = format!("/{}", prefix_segments.join("/"));
1966 let route_path = format!("/{}", route_segments.join("/"));
1967
1968 let nested_router = Router::new().route(&route_path, get(handler));
1969 let app = Router::new().nest(&prefix, nested_router);
1970
1971 let full_path = format!("{}{}", prefix, route_path);
1973
1974 match app.match_route(&full_path, &Method::GET) {
1976 RouteMatch::Found { .. } => {
1977 }
1979 RouteMatch::NotFound => {
1980 prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
1981 }
1982 RouteMatch::MethodNotAllowed { .. } => {
1983 prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
1984 }
1985 }
1986 }
1987 }
1988
1989 proptest! {
1996 #![proptest_config(ProptestConfig::with_cases(100))]
1997
1998 #[test]
2003 fn prop_state_type_ids_merged(
2004 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2005 has_nested_state in any::<bool>(),
2006 ) {
2007 #[derive(Clone)]
2008 struct TestState(#[allow(dead_code)] i32);
2009
2010 async fn handler() -> &'static str { "handler" }
2011
2012 let prefix = format!("/{}", prefix_segments.join("/"));
2013
2014 let mut nested = Router::new().route("/test", get(handler));
2015 if has_nested_state {
2016 nested = nested.state(TestState(42));
2017 }
2018
2019 let parent = Router::new().nest(&prefix, nested);
2020
2021 if has_nested_state {
2023 prop_assert!(
2024 parent.state_type_ids().contains(&std::any::TypeId::of::<TestState>()),
2025 "Parent should track nested state type ID"
2026 );
2027 }
2028 }
2029
2030 #[test]
2035 fn prop_merge_state_adds_nested_state(
2036 state_value in any::<i32>(),
2037 ) {
2038 #[derive(Clone, PartialEq, Debug)]
2039 struct UniqueState(i32);
2040
2041 let source = Router::new().state(UniqueState(state_value));
2043
2044 let parent = Router::new().merge_state::<UniqueState>(&source);
2046
2047 prop_assert!(
2049 parent.has_state::<UniqueState>(),
2050 "Parent should have state after merge"
2051 );
2052
2053 let merged_state = parent.state.get::<UniqueState>().unwrap();
2055 prop_assert_eq!(
2056 merged_state.0, state_value,
2057 "Merged state value should match source"
2058 );
2059 }
2060 }
2061
2062 proptest! {
2069 #![proptest_config(ProptestConfig::with_cases(100))]
2070
2071 #[test]
2076 fn prop_parent_state_takes_precedence(
2077 parent_value in any::<i32>(),
2078 nested_value in any::<i32>(),
2079 ) {
2080 prop_assume!(parent_value != nested_value);
2082
2083 #[derive(Clone, PartialEq, Debug)]
2084 struct SharedState(i32);
2085
2086 let source = Router::new().state(SharedState(nested_value));
2088
2089 let parent = Router::new()
2091 .state(SharedState(parent_value))
2092 .merge_state::<SharedState>(&source);
2093
2094 prop_assert!(
2096 parent.has_state::<SharedState>(),
2097 "Parent should have state"
2098 );
2099
2100 let final_state = parent.state.get::<SharedState>().unwrap();
2102 prop_assert_eq!(
2103 final_state.0, parent_value,
2104 "Parent state value should be preserved, not overwritten by nested"
2105 );
2106 }
2107
2108 #[test]
2113 fn prop_state_precedence_consistent(
2114 parent_value in any::<i32>(),
2115 source1_value in any::<i32>(),
2116 source2_value in any::<i32>(),
2117 ) {
2118 #[derive(Clone, PartialEq, Debug)]
2119 struct ConsistentState(i32);
2120
2121 let source1 = Router::new().state(ConsistentState(source1_value));
2123 let source2 = Router::new().state(ConsistentState(source2_value));
2124
2125 let parent = Router::new()
2127 .state(ConsistentState(parent_value))
2128 .merge_state::<ConsistentState>(&source1)
2129 .merge_state::<ConsistentState>(&source2);
2130
2131 let final_state = parent.state.get::<ConsistentState>().unwrap();
2133 prop_assert_eq!(
2134 final_state.0, parent_value,
2135 "Parent state should be preserved after multiple merges"
2136 );
2137 }
2138 }
2139
2140 proptest! {
2148 #![proptest_config(ProptestConfig::with_cases(100))]
2149
2150 #[test]
2155 fn prop_same_structure_different_param_names_conflict(
2156 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
2158 param1 in "[a-z][a-z0-9]{0,5}",
2160 param2 in "[a-z][a-z0-9]{0,5}",
2161 ) {
2162 prop_assume!(param1 != param2);
2164
2165 let mut path1 = String::from("/");
2167 let mut path2 = String::from("/");
2168
2169 for segment in &segments {
2170 path1.push_str(segment);
2171 path1.push('/');
2172 path2.push_str(segment);
2173 path2.push('/');
2174 }
2175
2176 path1.push('{');
2177 path1.push_str(¶m1);
2178 path1.push('}');
2179
2180 path2.push('{');
2181 path2.push_str(¶m2);
2182 path2.push('}');
2183
2184 let result = catch_unwind(AssertUnwindSafe(|| {
2186 async fn handler1() -> &'static str { "handler1" }
2187 async fn handler2() -> &'static str { "handler2" }
2188
2189 let _router = Router::new()
2190 .route(&path1, get(handler1))
2191 .route(&path2, get(handler2));
2192 }));
2193
2194 prop_assert!(
2195 result.is_err(),
2196 "Routes '{}' and '{}' should conflict but didn't",
2197 path1, path2
2198 );
2199 }
2200
2201 #[test]
2206 fn prop_different_structures_no_conflict(
2207 segments1 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2209 segments2 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2210 has_param1 in any::<bool>(),
2212 has_param2 in any::<bool>(),
2213 ) {
2214 let mut path1 = String::from("/");
2216 let mut path2 = String::from("/");
2217
2218 for segment in &segments1 {
2219 path1.push_str(segment);
2220 path1.push('/');
2221 }
2222 path1.pop(); for segment in &segments2 {
2225 path2.push_str(segment);
2226 path2.push('/');
2227 }
2228 path2.pop(); if has_param1 {
2231 path1.push_str("/{id}");
2232 }
2233
2234 if has_param2 {
2235 path2.push_str("/{id}");
2236 }
2237
2238 let norm1 = normalize_path_for_comparison(&convert_path_params(&path1));
2240 let norm2 = normalize_path_for_comparison(&convert_path_params(&path2));
2241
2242 prop_assume!(norm1 != norm2);
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(&path1, get(handler1))
2252 .route(&path2, get(handler2));
2253
2254 router.registered_routes().len()
2255 }));
2256
2257 prop_assert!(
2258 result.is_ok(),
2259 "Routes '{}' and '{}' should not conflict but did",
2260 path1, path2
2261 );
2262
2263 if let Ok(count) = result {
2264 prop_assert_eq!(count, 2, "Should have registered 2 routes");
2265 }
2266 }
2267
2268 #[test]
2273 fn prop_conflict_error_contains_both_paths(
2274 segment in "[a-z][a-z0-9]{1,5}",
2276 param1 in "[a-z][a-z0-9]{1,5}",
2277 param2 in "[a-z][a-z0-9]{1,5}",
2278 ) {
2279 prop_assume!(param1 != param2);
2280
2281 let path1 = format!("/{}/{{{}}}", segment, param1);
2282 let path2 = format!("/{}/{{{}}}", segment, param2);
2283
2284 let result = catch_unwind(AssertUnwindSafe(|| {
2285 async fn handler1() -> &'static str { "handler1" }
2286 async fn handler2() -> &'static str { "handler2" }
2287
2288 let _router = Router::new()
2289 .route(&path1, get(handler1))
2290 .route(&path2, get(handler2));
2291 }));
2292
2293 prop_assert!(result.is_err(), "Should have panicked due to conflict");
2294
2295 if let Err(panic_info) = result {
2297 if let Some(msg) = panic_info.downcast_ref::<String>() {
2298 prop_assert!(
2299 msg.contains("ROUTE CONFLICT DETECTED"),
2300 "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
2301 msg
2302 );
2303 prop_assert!(
2304 msg.contains("Existing:") && msg.contains("New:"),
2305 "Error should contain both 'Existing:' and 'New:' labels, got: {}",
2306 msg
2307 );
2308 prop_assert!(
2309 msg.contains("How to resolve:"),
2310 "Error should contain resolution guidance, got: {}",
2311 msg
2312 );
2313 }
2314 }
2315 }
2316
2317 #[test]
2321 fn prop_exact_duplicate_paths_conflict(
2322 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
2324 has_param in any::<bool>(),
2325 ) {
2326 let mut path = String::from("/");
2328
2329 for segment in &segments {
2330 path.push_str(segment);
2331 path.push('/');
2332 }
2333 path.pop(); if has_param {
2336 path.push_str("/{id}");
2337 }
2338
2339 let result = catch_unwind(AssertUnwindSafe(|| {
2341 async fn handler1() -> &'static str { "handler1" }
2342 async fn handler2() -> &'static str { "handler2" }
2343
2344 let _router = Router::new()
2345 .route(&path, get(handler1))
2346 .route(&path, get(handler2));
2347 }));
2348
2349 prop_assert!(
2350 result.is_err(),
2351 "Registering path '{}' twice should conflict but didn't",
2352 path
2353 );
2354 }
2355 }
2356
2357 proptest! {
2364 #![proptest_config(ProptestConfig::with_cases(100))]
2365
2366 #[test]
2371 fn prop_nested_route_with_params_matches(
2372 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2373 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
2374 param_value in "[a-z0-9]{1,10}",
2375 ) {
2376 async fn handler() -> &'static str { "handler" }
2377
2378 let prefix = format!("/{}", prefix_segments.join("/"));
2379 let route_path = if route_segments.is_empty() {
2380 "/{id}".to_string()
2381 } else {
2382 format!("/{}/{{id}}", route_segments.join("/"))
2383 };
2384
2385 let nested_router = Router::new().route(&route_path, get(handler));
2386 let app = Router::new().nest(&prefix, nested_router);
2387
2388 let full_path = if route_segments.is_empty() {
2390 format!("{}/{}", prefix, param_value)
2391 } else {
2392 format!("{}/{}/{}", prefix, route_segments.join("/"), param_value)
2393 };
2394
2395 match app.match_route(&full_path, &Method::GET) {
2397 RouteMatch::Found { params, .. } => {
2398 prop_assert!(
2400 params.contains_key("id"),
2401 "Should have 'id' parameter, got: {:?}",
2402 params
2403 );
2404 prop_assert_eq!(
2405 params.get("id").unwrap(),
2406 ¶m_value,
2407 "Parameter value should match"
2408 );
2409 }
2410 RouteMatch::NotFound => {
2411 prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
2412 }
2413 RouteMatch::MethodNotAllowed { .. } => {
2414 prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
2415 }
2416 }
2417 }
2418
2419 #[test]
2424 fn prop_nested_route_matches_correct_method(
2425 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2426 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2427 use_get in any::<bool>(),
2428 ) {
2429 async fn handler() -> &'static str { "handler" }
2430
2431 let prefix = format!("/{}", prefix_segments.join("/"));
2432 let route_path = format!("/{}", route_segments.join("/"));
2433
2434 let method_router = if use_get { get(handler) } else { post(handler) };
2436 let nested_router = Router::new().route(&route_path, method_router);
2437 let app = Router::new().nest(&prefix, nested_router);
2438
2439 let full_path = format!("{}{}", prefix, route_path);
2440 let registered_method = if use_get { Method::GET } else { Method::POST };
2441 let other_method = if use_get { Method::POST } else { Method::GET };
2442
2443 match app.match_route(&full_path, ®istered_method) {
2445 RouteMatch::Found { .. } => {
2446 }
2448 other => {
2449 prop_assert!(false, "Route should be found for registered method, got: {:?}",
2450 match other {
2451 RouteMatch::NotFound => "NotFound",
2452 RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
2453 _ => "Found",
2454 }
2455 );
2456 }
2457 }
2458
2459 match app.match_route(&full_path, &other_method) {
2461 RouteMatch::MethodNotAllowed { allowed } => {
2462 prop_assert!(
2463 allowed.contains(®istered_method),
2464 "Allowed methods should contain {:?}",
2465 registered_method
2466 );
2467 }
2468 other => {
2469 prop_assert!(false, "Route should return MethodNotAllowed for other method, got: {:?}",
2470 match other {
2471 RouteMatch::NotFound => "NotFound",
2472 RouteMatch::Found { .. } => "Found",
2473 _ => "MethodNotAllowed",
2474 }
2475 );
2476 }
2477 }
2478 }
2479 }
2480
2481 proptest! {
2488 #![proptest_config(ProptestConfig::with_cases(100))]
2489
2490 #[test]
2495 fn prop_single_param_extraction(
2496 prefix in "[a-z][a-z0-9]{1,5}",
2497 param_name in "[a-z][a-z0-9]{1,5}",
2498 param_value in "[a-z0-9]{1,10}",
2499 ) {
2500 async fn handler() -> &'static str { "handler" }
2501
2502 let prefix = format!("/{}", prefix);
2503 let route_path = format!("/{{{}}}", param_name);
2504
2505 let nested_router = Router::new().route(&route_path, get(handler));
2506 let app = Router::new().nest(&prefix, nested_router);
2507
2508 let full_path = format!("{}/{}", prefix, param_value);
2509
2510 match app.match_route(&full_path, &Method::GET) {
2511 RouteMatch::Found { params, .. } => {
2512 prop_assert!(
2513 params.contains_key(¶m_name),
2514 "Should have '{}' parameter, got: {:?}",
2515 param_name, params
2516 );
2517 prop_assert_eq!(
2518 params.get(¶m_name).unwrap(),
2519 ¶m_value,
2520 "Parameter '{}' value should be '{}'",
2521 param_name, param_value
2522 );
2523 }
2524 _ => {
2525 prop_assert!(false, "Route should be found");
2526 }
2527 }
2528 }
2529
2530 #[test]
2535 fn prop_multiple_params_extraction(
2536 prefix in "[a-z][a-z0-9]{1,5}",
2537 param1_name in "[a-z]{1,5}",
2538 param1_value in "[a-z0-9]{1,10}",
2539 param2_name in "[a-z]{1,5}",
2540 param2_value in "[a-z0-9]{1,10}",
2541 ) {
2542 prop_assume!(param1_name != param2_name);
2544
2545 async fn handler() -> &'static str { "handler" }
2546
2547 let prefix = format!("/{}", prefix);
2548 let route_path = format!("/{{{}}}/items/{{{}}}", param1_name, param2_name);
2549
2550 let nested_router = Router::new().route(&route_path, get(handler));
2551 let app = Router::new().nest(&prefix, nested_router);
2552
2553 let full_path = format!("{}/{}/items/{}", prefix, param1_value, param2_value);
2554
2555 match app.match_route(&full_path, &Method::GET) {
2556 RouteMatch::Found { params, .. } => {
2557 prop_assert!(
2559 params.contains_key(¶m1_name),
2560 "Should have '{}' parameter, got: {:?}",
2561 param1_name, params
2562 );
2563 prop_assert_eq!(
2564 params.get(¶m1_name).unwrap(),
2565 ¶m1_value,
2566 "Parameter '{}' value should be '{}'",
2567 param1_name, param1_value
2568 );
2569
2570 prop_assert!(
2572 params.contains_key(¶m2_name),
2573 "Should have '{}' parameter, got: {:?}",
2574 param2_name, params
2575 );
2576 prop_assert_eq!(
2577 params.get(¶m2_name).unwrap(),
2578 ¶m2_value,
2579 "Parameter '{}' value should be '{}'",
2580 param2_name, param2_value
2581 );
2582 }
2583 _ => {
2584 prop_assert!(false, "Route should be found");
2585 }
2586 }
2587 }
2588
2589 #[test]
2594 fn prop_param_value_preservation(
2595 prefix in "[a-z]{1,5}",
2596 param_value in "[a-zA-Z0-9_-]{1,15}",
2598 ) {
2599 async fn handler() -> &'static str { "handler" }
2600
2601 let prefix = format!("/{}", prefix);
2602 let route_path = "/{id}".to_string();
2603
2604 let nested_router = Router::new().route(&route_path, get(handler));
2605 let app = Router::new().nest(&prefix, nested_router);
2606
2607 let full_path = format!("{}/{}", prefix, param_value);
2608
2609 match app.match_route(&full_path, &Method::GET) {
2610 RouteMatch::Found { params, .. } => {
2611 prop_assert_eq!(
2612 params.get("id").unwrap(),
2613 ¶m_value,
2614 "Parameter value should be preserved exactly"
2615 );
2616 }
2617 _ => {
2618 prop_assert!(false, "Route should be found");
2619 }
2620 }
2621 }
2622 }
2623
2624 proptest! {
2631 #![proptest_config(ProptestConfig::with_cases(100))]
2632
2633 #[test]
2638 fn prop_unregistered_path_returns_not_found(
2639 prefix in "[a-z][a-z0-9]{1,5}",
2640 route_segment in "[a-z][a-z0-9]{1,5}",
2641 unregistered_segment in "[a-z][a-z0-9]{6,10}",
2642 ) {
2643 prop_assume!(route_segment != unregistered_segment);
2645
2646 async fn handler() -> &'static str { "handler" }
2647
2648 let prefix = format!("/{}", prefix);
2649 let route_path = format!("/{}", route_segment);
2650
2651 let nested_router = Router::new().route(&route_path, get(handler));
2652 let app = Router::new().nest(&prefix, nested_router);
2653
2654 let unregistered_path = format!("{}/{}", prefix, unregistered_segment);
2656
2657 match app.match_route(&unregistered_path, &Method::GET) {
2658 RouteMatch::NotFound => {
2659 }
2661 RouteMatch::Found { .. } => {
2662 prop_assert!(false, "Path '{}' should not be found", unregistered_path);
2663 }
2664 RouteMatch::MethodNotAllowed { .. } => {
2665 prop_assert!(false, "Path '{}' should return NotFound, not MethodNotAllowed", unregistered_path);
2666 }
2667 }
2668 }
2669
2670 #[test]
2674 fn prop_wrong_prefix_returns_not_found(
2675 prefix1 in "[a-z][a-z0-9]{1,5}",
2676 prefix2 in "[a-z][a-z0-9]{6,10}",
2677 route_segment in "[a-z][a-z0-9]{1,5}",
2678 ) {
2679 prop_assume!(prefix1 != prefix2);
2681
2682 async fn handler() -> &'static str { "handler" }
2683
2684 let prefix = format!("/{}", prefix1);
2685 let route_path = format!("/{}", route_segment);
2686
2687 let nested_router = Router::new().route(&route_path, get(handler));
2688 let app = Router::new().nest(&prefix, nested_router);
2689
2690 let wrong_prefix_path = format!("/{}/{}", prefix2, route_segment);
2692
2693 match app.match_route(&wrong_prefix_path, &Method::GET) {
2694 RouteMatch::NotFound => {
2695 }
2697 _ => {
2698 prop_assert!(false, "Path '{}' with wrong prefix should return NotFound", wrong_prefix_path);
2699 }
2700 }
2701 }
2702
2703 #[test]
2708 fn prop_partial_path_returns_not_found(
2709 prefix in "[a-z][a-z0-9]{1,5}",
2710 segment1 in "[a-z][a-z0-9]{1,5}",
2711 segment2 in "[a-z][a-z0-9]{1,5}",
2712 ) {
2713 async fn handler() -> &'static str { "handler" }
2714
2715 let prefix = format!("/{}", prefix);
2716 let route_path = format!("/{}/{}", segment1, segment2);
2717
2718 let nested_router = Router::new().route(&route_path, get(handler));
2719 let app = Router::new().nest(&prefix, nested_router);
2720
2721 let partial_path = format!("{}/{}", prefix, segment1);
2723
2724 match app.match_route(&partial_path, &Method::GET) {
2725 RouteMatch::NotFound => {
2726 }
2728 _ => {
2729 prop_assert!(false, "Partial path '{}' should return NotFound", partial_path);
2730 }
2731 }
2732 }
2733 }
2734
2735 proptest! {
2742 #![proptest_config(ProptestConfig::with_cases(100))]
2743
2744 #[test]
2750 fn prop_unregistered_method_returns_method_not_allowed(
2751 prefix in "[a-z][a-z0-9]{1,5}",
2752 route_segment in "[a-z][a-z0-9]{1,5}",
2753 ) {
2754 async fn handler() -> &'static str { "handler" }
2755
2756 let prefix = format!("/{}", prefix);
2757 let route_path = format!("/{}", route_segment);
2758
2759 let nested_router = Router::new().route(&route_path, get(handler));
2761 let app = Router::new().nest(&prefix, nested_router);
2762
2763 let full_path = format!("{}{}", prefix, route_path);
2764
2765 match app.match_route(&full_path, &Method::POST) {
2767 RouteMatch::MethodNotAllowed { allowed } => {
2768 prop_assert!(
2769 allowed.contains(&Method::GET),
2770 "Allowed methods should contain GET, got: {:?}",
2771 allowed
2772 );
2773 prop_assert!(
2774 !allowed.contains(&Method::POST),
2775 "Allowed methods should not contain POST"
2776 );
2777 }
2778 RouteMatch::Found { .. } => {
2779 prop_assert!(false, "POST should not be found on GET-only route");
2780 }
2781 RouteMatch::NotFound => {
2782 prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
2783 }
2784 }
2785 }
2786
2787 #[test]
2792 fn prop_multiple_methods_in_allowed_list(
2793 prefix in "[a-z][a-z0-9]{1,5}",
2794 route_segment in "[a-z][a-z0-9]{1,5}",
2795 use_get in any::<bool>(),
2796 use_post in any::<bool>(),
2797 use_put in any::<bool>(),
2798 ) {
2799 prop_assume!(use_get || use_post || use_put);
2801
2802 async fn handler() -> &'static str { "handler" }
2803
2804 let prefix = format!("/{}", prefix);
2805 let route_path = format!("/{}", route_segment);
2806
2807 let mut method_router = MethodRouter::new();
2809 let mut expected_methods: Vec<Method> = Vec::new();
2810
2811 if use_get {
2812 let get_router = get(handler);
2813 for (method, h) in get_router.handlers {
2814 method_router.handlers.insert(method.clone(), h);
2815 expected_methods.push(method);
2816 }
2817 }
2818 if use_post {
2819 let post_router = post(handler);
2820 for (method, h) in post_router.handlers {
2821 method_router.handlers.insert(method.clone(), h);
2822 expected_methods.push(method);
2823 }
2824 }
2825 if use_put {
2826 let put_router = put(handler);
2827 for (method, h) in put_router.handlers {
2828 method_router.handlers.insert(method.clone(), h);
2829 expected_methods.push(method);
2830 }
2831 }
2832
2833 let nested_router = Router::new().route(&route_path, method_router);
2834 let app = Router::new().nest(&prefix, nested_router);
2835
2836 let full_path = format!("{}{}", prefix, route_path);
2837
2838 match app.match_route(&full_path, &Method::DELETE) {
2840 RouteMatch::MethodNotAllowed { allowed } => {
2841 for method in &expected_methods {
2843 prop_assert!(
2844 allowed.contains(method),
2845 "Allowed methods should contain {:?}, got: {:?}",
2846 method, allowed
2847 );
2848 }
2849 prop_assert!(
2851 !allowed.contains(&Method::DELETE),
2852 "Allowed methods should not contain DELETE"
2853 );
2854 }
2855 RouteMatch::Found { .. } => {
2856 prop_assert!(false, "DELETE should not be found");
2857 }
2858 RouteMatch::NotFound => {
2859 prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
2860 }
2861 }
2862 }
2863 }
2864
2865 proptest! {
2879 #![proptest_config(ProptestConfig::with_cases(100))]
2880
2881 #[test]
2887 fn prop_multiple_routers_all_routes_registered(
2888 prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2890 prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2891 num_routes1 in 1..4usize,
2893 num_routes2 in 1..4usize,
2894 ) {
2895 let prefix1 = format!("/{}", prefix1_segments.join("/"));
2897 let prefix2 = format!("/{}", prefix2_segments.join("/"));
2898
2899 prop_assume!(prefix1 != prefix2);
2901
2902 async fn handler() -> &'static str { "handler" }
2903
2904 let mut router1 = Router::new();
2906 for i in 0..num_routes1 {
2907 let path = format!("/route1_{}", i);
2908 router1 = router1.route(&path, get(handler));
2909 }
2910
2911 let mut router2 = Router::new();
2913 for i in 0..num_routes2 {
2914 let path = format!("/route2_{}", i);
2915 router2 = router2.route(&path, get(handler));
2916 }
2917
2918 let app = Router::new()
2920 .nest(&prefix1, router1)
2921 .nest(&prefix2, router2);
2922
2923 let routes = app.registered_routes();
2924
2925 let expected_count = num_routes1 + num_routes2;
2927 prop_assert_eq!(
2928 routes.len(),
2929 expected_count,
2930 "Should have {} routes ({}+{}), got {}",
2931 expected_count, num_routes1, num_routes2, routes.len()
2932 );
2933
2934 for i in 0..num_routes1 {
2936 let expected_path = format!("{}/route1_{}", prefix1, i);
2937 let matchit_path = convert_path_params(&expected_path);
2938 prop_assert!(
2939 routes.contains_key(&matchit_path),
2940 "Route '{}' should be registered",
2941 expected_path
2942 );
2943 }
2944
2945 for i in 0..num_routes2 {
2947 let expected_path = format!("{}/route2_{}", prefix2, i);
2948 let matchit_path = convert_path_params(&expected_path);
2949 prop_assert!(
2950 routes.contains_key(&matchit_path),
2951 "Route '{}' should be registered",
2952 expected_path
2953 );
2954 }
2955 }
2956
2957 #[test]
2962 fn prop_multiple_routers_no_interference(
2963 prefix1 in "[a-z][a-z0-9]{1,5}",
2964 prefix2 in "[a-z][a-z0-9]{1,5}",
2965 route_segment in "[a-z][a-z0-9]{1,5}",
2966 param_value1 in "[a-z0-9]{1,10}",
2967 param_value2 in "[a-z0-9]{1,10}",
2968 ) {
2969 prop_assume!(prefix1 != prefix2);
2971
2972 let prefix1 = format!("/{}", prefix1);
2973 let prefix2 = format!("/{}", prefix2);
2974
2975 async fn handler() -> &'static str { "handler" }
2976
2977 let router1 = Router::new()
2979 .route(&format!("/{}", route_segment), get(handler))
2980 .route("/{id}", get(handler));
2981
2982 let router2 = Router::new()
2983 .route(&format!("/{}", route_segment), get(handler))
2984 .route("/{id}", get(handler));
2985
2986 let app = Router::new()
2988 .nest(&prefix1, router1)
2989 .nest(&prefix2, router2);
2990
2991 let path1_static = format!("{}/{}", prefix1, route_segment);
2993 match app.match_route(&path1_static, &Method::GET) {
2994 RouteMatch::Found { params, .. } => {
2995 prop_assert!(params.is_empty(), "Static path should have no params");
2996 }
2997 _ => {
2998 prop_assert!(false, "Route '{}' should be found", path1_static);
2999 }
3000 }
3001
3002 let path1_param = format!("{}/{}", prefix1, param_value1);
3003 match app.match_route(&path1_param, &Method::GET) {
3004 RouteMatch::Found { params, .. } => {
3005 prop_assert_eq!(
3006 params.get("id"),
3007 Some(¶m_value1.to_string()),
3008 "Parameter should be extracted correctly"
3009 );
3010 }
3011 _ => {
3012 prop_assert!(false, "Route '{}' should be found", path1_param);
3013 }
3014 }
3015
3016 let path2_static = format!("{}/{}", prefix2, route_segment);
3018 match app.match_route(&path2_static, &Method::GET) {
3019 RouteMatch::Found { params, .. } => {
3020 prop_assert!(params.is_empty(), "Static path should have no params");
3021 }
3022 _ => {
3023 prop_assert!(false, "Route '{}' should be found", path2_static);
3024 }
3025 }
3026
3027 let path2_param = format!("{}/{}", prefix2, param_value2);
3028 match app.match_route(&path2_param, &Method::GET) {
3029 RouteMatch::Found { params, .. } => {
3030 prop_assert_eq!(
3031 params.get("id"),
3032 Some(¶m_value2.to_string()),
3033 "Parameter should be extracted correctly"
3034 );
3035 }
3036 _ => {
3037 prop_assert!(false, "Route '{}' should be found", path2_param);
3038 }
3039 }
3040 }
3041
3042 #[test]
3047 fn prop_multiple_routers_preserve_methods(
3048 prefix1 in "[a-z][a-z0-9]{1,5}",
3049 prefix2 in "[a-z][a-z0-9]{1,5}",
3050 route_segment in "[a-z][a-z0-9]{1,5}",
3051 router1_use_get in any::<bool>(),
3052 router1_use_post in any::<bool>(),
3053 router2_use_get in any::<bool>(),
3054 router2_use_put in any::<bool>(),
3055 ) {
3056 prop_assume!(router1_use_get || router1_use_post);
3058 prop_assume!(router2_use_get || router2_use_put);
3059 prop_assume!(prefix1 != prefix2);
3061
3062 let prefix1 = format!("/{}", prefix1);
3063 let prefix2 = format!("/{}", prefix2);
3064 let route_path = format!("/{}", route_segment);
3065
3066 async fn handler() -> &'static str { "handler" }
3067
3068 let mut method_router1 = MethodRouter::new();
3070 let mut expected_methods1: Vec<Method> = Vec::new();
3071 if router1_use_get {
3072 let get_router = get(handler);
3073 for (method, h) in get_router.handlers {
3074 method_router1.handlers.insert(method.clone(), h);
3075 expected_methods1.push(method);
3076 }
3077 }
3078 if router1_use_post {
3079 let post_router = post(handler);
3080 for (method, h) in post_router.handlers {
3081 method_router1.handlers.insert(method.clone(), h);
3082 expected_methods1.push(method);
3083 }
3084 }
3085
3086 let mut method_router2 = MethodRouter::new();
3088 let mut expected_methods2: Vec<Method> = Vec::new();
3089 if router2_use_get {
3090 let get_router = get(handler);
3091 for (method, h) in get_router.handlers {
3092 method_router2.handlers.insert(method.clone(), h);
3093 expected_methods2.push(method);
3094 }
3095 }
3096 if router2_use_put {
3097 let put_router = put(handler);
3098 for (method, h) in put_router.handlers {
3099 method_router2.handlers.insert(method.clone(), h);
3100 expected_methods2.push(method);
3101 }
3102 }
3103
3104 let router1 = Router::new().route(&route_path, method_router1);
3105 let router2 = Router::new().route(&route_path, method_router2);
3106
3107 let app = Router::new()
3108 .nest(&prefix1, router1)
3109 .nest(&prefix2, router2);
3110
3111 let full_path1 = format!("{}{}", prefix1, route_path);
3112 let full_path2 = format!("{}{}", prefix2, route_path);
3113
3114 for method in &expected_methods1 {
3116 match app.match_route(&full_path1, method) {
3117 RouteMatch::Found { .. } => {}
3118 _ => {
3119 prop_assert!(false, "Method {:?} should be found for {}", method, full_path1);
3120 }
3121 }
3122 }
3123
3124 for method in &expected_methods2 {
3126 match app.match_route(&full_path2, method) {
3127 RouteMatch::Found { .. } => {}
3128 _ => {
3129 prop_assert!(false, "Method {:?} should be found for {}", method, full_path2);
3130 }
3131 }
3132 }
3133
3134 if !expected_methods1.contains(&Method::DELETE) {
3136 match app.match_route(&full_path1, &Method::DELETE) {
3137 RouteMatch::MethodNotAllowed { allowed } => {
3138 for method in &expected_methods1 {
3139 prop_assert!(
3140 allowed.contains(method),
3141 "Allowed methods for {} should contain {:?}",
3142 full_path1, method
3143 );
3144 }
3145 }
3146 _ => {
3147 prop_assert!(false, "DELETE should return MethodNotAllowed for {}", full_path1);
3148 }
3149 }
3150 }
3151 }
3152
3153 #[test]
3158 fn prop_three_routers_composition(
3159 prefix1 in "[a-z]{1,3}",
3160 prefix2 in "[a-z]{4,6}",
3161 prefix3 in "[a-z]{7,9}",
3162 num_routes in 1..3usize,
3163 ) {
3164 let prefix1 = format!("/{}", prefix1);
3165 let prefix2 = format!("/{}", prefix2);
3166 let prefix3 = format!("/{}", prefix3);
3167
3168 async fn handler() -> &'static str { "handler" }
3169
3170 let mut router1 = Router::new();
3172 let mut router2 = Router::new();
3173 let mut router3 = Router::new();
3174
3175 for i in 0..num_routes {
3176 let path = format!("/item{}", i);
3177 router1 = router1.route(&path, get(handler));
3178 router2 = router2.route(&path, get(handler));
3179 router3 = router3.route(&path, get(handler));
3180 }
3181
3182 let app = Router::new()
3184 .nest(&prefix1, router1)
3185 .nest(&prefix2, router2)
3186 .nest(&prefix3, router3);
3187
3188 let routes = app.registered_routes();
3189
3190 let expected_count = 3 * num_routes;
3192 prop_assert_eq!(
3193 routes.len(),
3194 expected_count,
3195 "Should have {} routes, got {}",
3196 expected_count, routes.len()
3197 );
3198
3199 for i in 0..num_routes {
3201 let path1 = format!("{}/item{}", prefix1, i);
3202 let path2 = format!("{}/item{}", prefix2, i);
3203 let path3 = format!("{}/item{}", prefix3, i);
3204
3205 match app.match_route(&path1, &Method::GET) {
3206 RouteMatch::Found { .. } => {}
3207 _ => prop_assert!(false, "Route '{}' should be found", path1),
3208 }
3209 match app.match_route(&path2, &Method::GET) {
3210 RouteMatch::Found { .. } => {}
3211 _ => prop_assert!(false, "Route '{}' should be found", path2),
3212 }
3213 match app.match_route(&path3, &Method::GET) {
3214 RouteMatch::Found { .. } => {}
3215 _ => prop_assert!(false, "Route '{}' should be found", path3),
3216 }
3217 }
3218 }
3219 }
3220 proptest! {
3221 #![proptest_config(ProptestConfig::with_cases(100))]
3222
3223 #[test]
3229 fn prop_nested_route_conflict_different_param_names(
3230 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3231 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
3232 param1 in "[a-z][a-z0-9]{1,5}",
3233 param2 in "[a-z][a-z0-9]{1,5}",
3234 ) {
3235 prop_assume!(param1 != param2);
3237
3238 async fn handler1() -> &'static str { "handler1" }
3239 async fn handler2() -> &'static str { "handler2" }
3240
3241 let prefix = format!("/{}", prefix_segments.join("/"));
3242
3243 let existing_path = if route_segments.is_empty() {
3245 format!("{}/{{{}}}", prefix, param1)
3246 } else {
3247 format!("{}/{}/{{{}}}", prefix, route_segments.join("/"), param1)
3248 };
3249
3250 let nested_path = if route_segments.is_empty() {
3252 format!("/{{{}}}", param2)
3253 } else {
3254 format!("/{}/{{{}}}", route_segments.join("/"), param2)
3255 };
3256
3257 let result = catch_unwind(AssertUnwindSafe(|| {
3259 let parent = Router::new().route(&existing_path, get(handler1));
3260 let nested = Router::new().route(&nested_path, get(handler2));
3261 let _app = parent.nest(&prefix, nested);
3262 }));
3263
3264 prop_assert!(
3266 result.is_err(),
3267 "Nested route '{}{}' should conflict with existing route '{}' but didn't",
3268 prefix, nested_path, existing_path
3269 );
3270
3271 if let Err(panic_info) = result {
3273 if let Some(msg) = panic_info.downcast_ref::<String>() {
3274 prop_assert!(
3275 msg.contains("ROUTE CONFLICT DETECTED"),
3276 "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
3277 msg
3278 );
3279 prop_assert!(
3280 msg.contains("Existing:") && msg.contains("New:"),
3281 "Error should contain both 'Existing:' and 'New:' labels, got: {}",
3282 msg
3283 );
3284 }
3285 }
3286 }
3287
3288 #[test]
3293 fn prop_nested_route_conflict_exact_same_path(
3294 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3295 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3296 ) {
3297 async fn handler1() -> &'static str { "handler1" }
3298 async fn handler2() -> &'static str { "handler2" }
3299
3300 let prefix = format!("/{}", prefix_segments.join("/"));
3301 let route_path = format!("/{}", route_segments.join("/"));
3302
3303 let existing_path = format!("{}{}", prefix, route_path);
3305
3306 let result = catch_unwind(AssertUnwindSafe(|| {
3308 let parent = Router::new().route(&existing_path, get(handler1));
3309 let nested = Router::new().route(&route_path, get(handler2));
3310 let _app = parent.nest(&prefix, nested);
3311 }));
3312
3313 prop_assert!(
3315 result.is_err(),
3316 "Nested route '{}{}' should conflict with existing route '{}' but didn't",
3317 prefix, route_path, existing_path
3318 );
3319 }
3320
3321 #[test]
3326 fn prop_nested_routes_different_prefixes_no_conflict(
3327 prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3328 prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3329 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3330 has_param in any::<bool>(),
3331 ) {
3332 let prefix1 = format!("/{}", prefix1_segments.join("/"));
3334 let prefix2 = format!("/{}", prefix2_segments.join("/"));
3335
3336 prop_assume!(prefix1 != prefix2);
3338
3339 async fn handler1() -> &'static str { "handler1" }
3340 async fn handler2() -> &'static str { "handler2" }
3341
3342 let route_path = if has_param {
3344 format!("/{}/{{id}}", route_segments.join("/"))
3345 } else {
3346 format!("/{}", route_segments.join("/"))
3347 };
3348
3349 let result = catch_unwind(AssertUnwindSafe(|| {
3351 let nested1 = Router::new().route(&route_path, get(handler1));
3352 let nested2 = Router::new().route(&route_path, get(handler2));
3353
3354 let app = Router::new()
3355 .nest(&prefix1, nested1)
3356 .nest(&prefix2, nested2);
3357
3358 app.registered_routes().len()
3359 }));
3360
3361 prop_assert!(
3363 result.is_ok(),
3364 "Routes under different prefixes '{}' and '{}' should not conflict",
3365 prefix1, prefix2
3366 );
3367
3368 if let Ok(count) = result {
3369 prop_assert_eq!(count, 2, "Should have registered 2 routes");
3370 }
3371 }
3372
3373 #[test]
3378 fn prop_nested_conflict_error_contains_guidance(
3379 prefix in "[a-z][a-z0-9]{1,5}",
3380 segment in "[a-z][a-z0-9]{1,5}",
3381 param1 in "[a-z][a-z0-9]{1,5}",
3382 param2 in "[a-z][a-z0-9]{1,5}",
3383 ) {
3384 prop_assume!(param1 != param2);
3385
3386 async fn handler1() -> &'static str { "handler1" }
3387 async fn handler2() -> &'static str { "handler2" }
3388
3389 let prefix = format!("/{}", prefix);
3390 let existing_path = format!("{}/{}/{{{}}}", prefix, segment, param1);
3391 let nested_path = format!("/{}/{{{}}}", segment, param2);
3392
3393 let result = catch_unwind(AssertUnwindSafe(|| {
3394 let parent = Router::new().route(&existing_path, get(handler1));
3395 let nested = Router::new().route(&nested_path, get(handler2));
3396 let _app = parent.nest(&prefix, nested);
3397 }));
3398
3399 prop_assert!(result.is_err(), "Should have detected conflict");
3400
3401 if let Err(panic_info) = result {
3402 if let Some(msg) = panic_info.downcast_ref::<String>() {
3403 prop_assert!(
3404 msg.contains("How to resolve:"),
3405 "Error should contain 'How to resolve:' guidance, got: {}",
3406 msg
3407 );
3408 prop_assert!(
3409 msg.contains("Use different path patterns") ||
3410 msg.contains("different path patterns"),
3411 "Error should suggest using different path patterns, got: {}",
3412 msg
3413 );
3414 }
3415 }
3416 }
3417 }
3418}