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 component_registrar: fn(&mut rustapi_openapi::OpenApiSpec),
194 ) {
195 if self.handlers.contains_key(&method) {
196 panic!(
197 "Duplicate handler for method {} on the same path",
198 method.as_str()
199 );
200 }
201
202 self.handlers.insert(method.clone(), handler);
203 self.operations.insert(method, operation);
204 self.component_registrars.push(component_registrar);
205 }
206
207 pub fn get<H, T>(self, handler: H) -> Self
209 where
210 H: Handler<T>,
211 T: 'static,
212 {
213 let mut op = Operation::new();
214 H::update_operation(&mut op);
215 self.on(
216 Method::GET,
217 into_boxed_handler(handler),
218 op,
219 <H as Handler<T>>::register_components,
220 )
221 }
222
223 pub fn post<H, T>(self, handler: H) -> Self
225 where
226 H: Handler<T>,
227 T: 'static,
228 {
229 let mut op = Operation::new();
230 H::update_operation(&mut op);
231 self.on(
232 Method::POST,
233 into_boxed_handler(handler),
234 op,
235 <H as Handler<T>>::register_components,
236 )
237 }
238
239 pub fn put<H, T>(self, handler: H) -> Self
241 where
242 H: Handler<T>,
243 T: 'static,
244 {
245 let mut op = Operation::new();
246 H::update_operation(&mut op);
247 self.on(
248 Method::PUT,
249 into_boxed_handler(handler),
250 op,
251 <H as Handler<T>>::register_components,
252 )
253 }
254
255 pub fn patch<H, T>(self, handler: H) -> Self
257 where
258 H: Handler<T>,
259 T: 'static,
260 {
261 let mut op = Operation::new();
262 H::update_operation(&mut op);
263 self.on(
264 Method::PATCH,
265 into_boxed_handler(handler),
266 op,
267 <H as Handler<T>>::register_components,
268 )
269 }
270
271 pub fn delete<H, T>(self, handler: H) -> Self
273 where
274 H: Handler<T>,
275 T: 'static,
276 {
277 let mut op = Operation::new();
278 H::update_operation(&mut op);
279 self.on(
280 Method::DELETE,
281 into_boxed_handler(handler),
282 op,
283 <H as Handler<T>>::register_components,
284 )
285 }
286}
287
288impl Default for MethodRouter {
289 fn default() -> Self {
290 Self::new()
291 }
292}
293
294pub fn get<H, T>(handler: H) -> MethodRouter
296where
297 H: Handler<T>,
298 T: 'static,
299{
300 let mut op = Operation::new();
301 H::update_operation(&mut op);
302 MethodRouter::new().on(
303 Method::GET,
304 into_boxed_handler(handler),
305 op,
306 <H as Handler<T>>::register_components,
307 )
308}
309
310pub fn post<H, T>(handler: H) -> MethodRouter
312where
313 H: Handler<T>,
314 T: 'static,
315{
316 let mut op = Operation::new();
317 H::update_operation(&mut op);
318 MethodRouter::new().on(
319 Method::POST,
320 into_boxed_handler(handler),
321 op,
322 <H as Handler<T>>::register_components,
323 )
324}
325
326pub fn put<H, T>(handler: H) -> MethodRouter
328where
329 H: Handler<T>,
330 T: 'static,
331{
332 let mut op = Operation::new();
333 H::update_operation(&mut op);
334 MethodRouter::new().on(
335 Method::PUT,
336 into_boxed_handler(handler),
337 op,
338 <H as Handler<T>>::register_components,
339 )
340}
341
342pub fn patch<H, T>(handler: H) -> MethodRouter
344where
345 H: Handler<T>,
346 T: 'static,
347{
348 let mut op = Operation::new();
349 H::update_operation(&mut op);
350 MethodRouter::new().on(
351 Method::PATCH,
352 into_boxed_handler(handler),
353 op,
354 <H as Handler<T>>::register_components,
355 )
356}
357
358pub fn delete<H, T>(handler: H) -> MethodRouter
360where
361 H: Handler<T>,
362 T: 'static,
363{
364 let mut op = Operation::new();
365 H::update_operation(&mut op);
366 MethodRouter::new().on(
367 Method::DELETE,
368 into_boxed_handler(handler),
369 op,
370 <H as Handler<T>>::register_components,
371 )
372}
373
374pub struct Router {
376 inner: MatchitRouter<MethodRouter>,
377 state: Arc<Extensions>,
378 registered_routes: HashMap<String, RouteInfo>,
380 method_routers: HashMap<String, MethodRouter>,
382 state_type_ids: Vec<std::any::TypeId>,
385}
386
387impl Router {
388 pub fn new() -> Self {
390 Self {
391 inner: MatchitRouter::new(),
392 state: Arc::new(Extensions::new()),
393 registered_routes: HashMap::new(),
394 method_routers: HashMap::new(),
395 state_type_ids: Vec::new(),
396 }
397 }
398
399 pub fn typed<P: TypedPath>(self, method_router: MethodRouter) -> Self {
401 self.route(P::PATH, method_router)
402 }
403
404 pub fn route(mut self, path: &str, method_router: MethodRouter) -> Self {
406 let matchit_path = convert_path_params(path);
408
409 let methods: Vec<Method> = method_router.handlers.keys().cloned().collect();
411
412 self.method_routers
414 .insert(matchit_path.clone(), method_router.clone());
415
416 match self.inner.insert(matchit_path.clone(), method_router) {
417 Ok(_) => {
418 self.registered_routes.insert(
420 matchit_path.clone(),
421 RouteInfo {
422 path: path.to_string(),
423 methods,
424 },
425 );
426 }
427 Err(e) => {
428 self.method_routers.remove(&matchit_path);
430
431 let existing_path = self
433 .find_conflicting_route(&matchit_path)
434 .map(|info| info.path.clone())
435 .unwrap_or_else(|| "<unknown>".to_string());
436
437 let conflict_error = RouteConflictError {
438 new_path: path.to_string(),
439 method: methods.first().cloned(),
440 existing_path,
441 details: e.to_string(),
442 };
443
444 panic!("{}", conflict_error);
445 }
446 }
447 self
448 }
449
450 fn find_conflicting_route(&self, matchit_path: &str) -> Option<&RouteInfo> {
452 if let Some(info) = self.registered_routes.get(matchit_path) {
454 return Some(info);
455 }
456
457 let normalized_new = normalize_path_for_comparison(matchit_path);
459
460 for (registered_path, info) in &self.registered_routes {
461 let normalized_existing = normalize_path_for_comparison(registered_path);
462 if normalized_new == normalized_existing {
463 return Some(info);
464 }
465 }
466
467 None
468 }
469
470 pub fn state<S: Clone + Send + Sync + 'static>(mut self, state: S) -> Self {
472 let type_id = std::any::TypeId::of::<S>();
473 let extensions = Arc::make_mut(&mut self.state);
474 extensions.insert(state);
475 if !self.state_type_ids.contains(&type_id) {
476 self.state_type_ids.push(type_id);
477 }
478 self
479 }
480
481 pub fn has_state<S: 'static>(&self) -> bool {
483 self.state_type_ids.contains(&std::any::TypeId::of::<S>())
484 }
485
486 pub fn state_type_ids(&self) -> &[std::any::TypeId] {
488 &self.state_type_ids
489 }
490
491 pub fn nest(mut self, prefix: &str, router: Router) -> Self {
559 let normalized_prefix = normalize_prefix(prefix);
561
562 for type_id in &router.state_type_ids {
566 if !self.state_type_ids.contains(type_id) {
567 self.state_type_ids.push(*type_id);
568 }
569 }
570
571 let nested_routes: Vec<(String, RouteInfo, MethodRouter)> = router
574 .registered_routes
575 .into_iter()
576 .filter_map(|(matchit_path, route_info)| {
577 router
578 .method_routers
579 .get(&matchit_path)
580 .map(|mr| (matchit_path, route_info, mr.clone()))
581 })
582 .collect();
583
584 for (matchit_path, route_info, method_router) in nested_routes {
586 let prefixed_matchit_path = if matchit_path == "/" {
590 normalized_prefix.clone()
591 } else {
592 format!("{}{}", normalized_prefix, matchit_path)
593 };
594
595 let prefixed_display_path = if route_info.path == "/" {
596 normalized_prefix.clone()
597 } else {
598 format!("{}{}", normalized_prefix, route_info.path)
599 };
600
601 self.method_routers
603 .insert(prefixed_matchit_path.clone(), method_router.clone());
604
605 match self
607 .inner
608 .insert(prefixed_matchit_path.clone(), method_router)
609 {
610 Ok(_) => {
611 self.registered_routes.insert(
613 prefixed_matchit_path,
614 RouteInfo {
615 path: prefixed_display_path,
616 methods: route_info.methods,
617 },
618 );
619 }
620 Err(e) => {
621 self.method_routers.remove(&prefixed_matchit_path);
623
624 let existing_path = self
626 .find_conflicting_route(&prefixed_matchit_path)
627 .map(|info| info.path.clone())
628 .unwrap_or_else(|| "<unknown>".to_string());
629
630 let conflict_error = RouteConflictError {
631 new_path: prefixed_display_path,
632 method: route_info.methods.first().cloned(),
633 existing_path,
634 details: e.to_string(),
635 };
636
637 panic!("{}", conflict_error);
638 }
639 }
640 }
641
642 self
643 }
644
645 pub fn merge_state<S: Clone + Send + Sync + 'static>(mut self, other: &Router) -> Self {
662 let type_id = std::any::TypeId::of::<S>();
663
664 if !self.state_type_ids.contains(&type_id) {
666 if let Some(state) = other.state.get::<S>() {
668 let extensions = Arc::make_mut(&mut self.state);
669 extensions.insert(state.clone());
670 self.state_type_ids.push(type_id);
671 }
672 }
673
674 self
675 }
676
677 pub fn match_route(&self, path: &str, method: &Method) -> RouteMatch<'_> {
679 match self.inner.at(path) {
680 Ok(matched) => {
681 let method_router = matched.value;
682
683 if let Some(handler) = method_router.get_handler(method) {
684 let params: PathParams = matched
686 .params
687 .iter()
688 .map(|(k, v)| (k.to_string(), v.to_string()))
689 .collect();
690
691 RouteMatch::Found { handler, params }
692 } else {
693 RouteMatch::MethodNotAllowed {
694 allowed: method_router.allowed_methods(),
695 }
696 }
697 }
698 Err(_) => RouteMatch::NotFound,
699 }
700 }
701
702 pub fn state_ref(&self) -> Arc<Extensions> {
704 self.state.clone()
705 }
706
707 pub fn registered_routes(&self) -> &HashMap<String, RouteInfo> {
709 &self.registered_routes
710 }
711
712 pub fn method_routers(&self) -> &HashMap<String, MethodRouter> {
714 &self.method_routers
715 }
716}
717
718impl Default for Router {
719 fn default() -> Self {
720 Self::new()
721 }
722}
723
724pub enum RouteMatch<'a> {
726 Found {
727 handler: &'a BoxedHandler,
728 params: PathParams,
729 },
730 NotFound,
731 MethodNotAllowed {
732 allowed: Vec<Method>,
733 },
734}
735
736fn convert_path_params(path: &str) -> String {
738 let mut result = String::with_capacity(path.len());
739
740 for ch in path.chars() {
741 match ch {
742 '{' => {
743 result.push(':');
744 }
745 '}' => {
746 }
748 _ => {
749 result.push(ch);
750 }
751 }
752 }
753
754 result
755}
756
757fn normalize_path_for_comparison(path: &str) -> String {
759 let mut result = String::with_capacity(path.len());
760 let mut in_param = false;
761
762 for ch in path.chars() {
763 match ch {
764 ':' => {
765 in_param = true;
766 result.push_str(":_");
767 }
768 '/' => {
769 in_param = false;
770 result.push('/');
771 }
772 _ if in_param => {
773 }
775 _ => {
776 result.push(ch);
777 }
778 }
779 }
780
781 result
782}
783
784pub(crate) fn normalize_prefix(prefix: &str) -> String {
801 if prefix.is_empty() {
803 return "/".to_string();
804 }
805
806 let segments: Vec<&str> = prefix.split('/').filter(|s| !s.is_empty()).collect();
808
809 if segments.is_empty() {
811 return "/".to_string();
812 }
813
814 let mut result = String::with_capacity(prefix.len() + 1);
816 for segment in segments {
817 result.push('/');
818 result.push_str(segment);
819 }
820
821 result
822}
823
824#[cfg(test)]
825mod tests {
826 use super::*;
827
828 #[test]
829 fn test_convert_path_params() {
830 assert_eq!(convert_path_params("/users/{id}"), "/users/:id");
831 assert_eq!(
832 convert_path_params("/users/{user_id}/posts/{post_id}"),
833 "/users/:user_id/posts/:post_id"
834 );
835 assert_eq!(convert_path_params("/static/path"), "/static/path");
836 }
837
838 #[test]
839 fn test_normalize_path_for_comparison() {
840 assert_eq!(normalize_path_for_comparison("/users/:id"), "/users/:_");
841 assert_eq!(
842 normalize_path_for_comparison("/users/:user_id"),
843 "/users/:_"
844 );
845 assert_eq!(
846 normalize_path_for_comparison("/users/:id/posts/:post_id"),
847 "/users/:_/posts/:_"
848 );
849 assert_eq!(
850 normalize_path_for_comparison("/static/path"),
851 "/static/path"
852 );
853 }
854
855 #[test]
856 fn test_normalize_prefix() {
857 assert_eq!(normalize_prefix("api"), "/api");
859 assert_eq!(normalize_prefix("/api"), "/api");
860 assert_eq!(normalize_prefix("/api/"), "/api");
861 assert_eq!(normalize_prefix("api/"), "/api");
862
863 assert_eq!(normalize_prefix("api/v1"), "/api/v1");
865 assert_eq!(normalize_prefix("/api/v1"), "/api/v1");
866 assert_eq!(normalize_prefix("/api/v1/"), "/api/v1");
867
868 assert_eq!(normalize_prefix(""), "/");
870 assert_eq!(normalize_prefix("/"), "/");
871
872 assert_eq!(normalize_prefix("//api"), "/api");
874 assert_eq!(normalize_prefix("api//v1"), "/api/v1");
875 assert_eq!(normalize_prefix("//api//v1//"), "/api/v1");
876 assert_eq!(normalize_prefix("///"), "/");
877 }
878
879 #[test]
880 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
881 fn test_route_conflict_detection() {
882 async fn handler1() -> &'static str {
883 "handler1"
884 }
885 async fn handler2() -> &'static str {
886 "handler2"
887 }
888
889 let _router = Router::new()
890 .route("/users/{id}", get(handler1))
891 .route("/users/{user_id}", get(handler2)); }
893
894 #[test]
895 fn test_no_conflict_different_paths() {
896 async fn handler1() -> &'static str {
897 "handler1"
898 }
899 async fn handler2() -> &'static str {
900 "handler2"
901 }
902
903 let router = Router::new()
904 .route("/users/{id}", get(handler1))
905 .route("/users/{id}/profile", get(handler2));
906
907 assert_eq!(router.registered_routes().len(), 2);
908 }
909
910 #[test]
911 fn test_route_info_tracking() {
912 async fn handler() -> &'static str {
913 "handler"
914 }
915
916 let router = Router::new().route("/users/{id}", get(handler));
917
918 let routes = router.registered_routes();
919 assert_eq!(routes.len(), 1);
920
921 let info = routes.get("/users/:id").unwrap();
922 assert_eq!(info.path, "/users/{id}");
923 assert_eq!(info.methods.len(), 1);
924 assert_eq!(info.methods[0], Method::GET);
925 }
926
927 #[test]
928 fn test_basic_router_nesting() {
929 async fn list_users() -> &'static str {
930 "list users"
931 }
932 async fn get_user() -> &'static str {
933 "get user"
934 }
935
936 let users_router = Router::new()
937 .route("/", get(list_users))
938 .route("/{id}", get(get_user));
939
940 let app = Router::new().nest("/api/users", users_router);
941
942 let routes = app.registered_routes();
943 assert_eq!(routes.len(), 2);
944
945 assert!(routes.contains_key("/api/users"));
947 assert!(routes.contains_key("/api/users/:id"));
948
949 let list_info = routes.get("/api/users").unwrap();
951 assert_eq!(list_info.path, "/api/users");
952
953 let get_info = routes.get("/api/users/:id").unwrap();
954 assert_eq!(get_info.path, "/api/users/{id}");
955 }
956
957 #[test]
958 fn test_nested_route_matching() {
959 async fn handler() -> &'static str {
960 "handler"
961 }
962
963 let users_router = Router::new().route("/{id}", get(handler));
964
965 let app = Router::new().nest("/api/users", users_router);
966
967 match app.match_route("/api/users/123", &Method::GET) {
969 RouteMatch::Found { params, .. } => {
970 assert_eq!(params.get("id"), Some(&"123".to_string()));
971 }
972 _ => panic!("Route should be found"),
973 }
974 }
975
976 #[test]
977 fn test_nested_route_matching_multiple_params() {
978 async fn handler() -> &'static str {
979 "handler"
980 }
981
982 let posts_router = Router::new().route("/{user_id}/posts/{post_id}", get(handler));
983
984 let app = Router::new().nest("/api", posts_router);
985
986 match app.match_route("/api/42/posts/100", &Method::GET) {
988 RouteMatch::Found { params, .. } => {
989 assert_eq!(params.get("user_id"), Some(&"42".to_string()));
990 assert_eq!(params.get("post_id"), Some(&"100".to_string()));
991 }
992 _ => panic!("Route should be found"),
993 }
994 }
995
996 #[test]
997 fn test_nested_route_matching_static_path() {
998 async fn handler() -> &'static str {
999 "handler"
1000 }
1001
1002 let health_router = Router::new().route("/health", get(handler));
1003
1004 let app = Router::new().nest("/api/v1", health_router);
1005
1006 match app.match_route("/api/v1/health", &Method::GET) {
1008 RouteMatch::Found { params, .. } => {
1009 assert!(params.is_empty(), "Static path should have no params");
1010 }
1011 _ => panic!("Route should be found"),
1012 }
1013 }
1014
1015 #[test]
1016 fn test_nested_route_not_found() {
1017 async fn handler() -> &'static str {
1018 "handler"
1019 }
1020
1021 let users_router = Router::new().route("/users", get(handler));
1022
1023 let app = Router::new().nest("/api", users_router);
1024
1025 match app.match_route("/api/posts", &Method::GET) {
1027 RouteMatch::NotFound => {
1028 }
1030 _ => panic!("Route should not be found"),
1031 }
1032
1033 match app.match_route("/v2/users", &Method::GET) {
1035 RouteMatch::NotFound => {
1036 }
1038 _ => panic!("Route with wrong prefix should not be found"),
1039 }
1040 }
1041
1042 #[test]
1043 fn test_nested_route_method_not_allowed() {
1044 async fn handler() -> &'static str {
1045 "handler"
1046 }
1047
1048 let users_router = Router::new().route("/users", get(handler));
1049
1050 let app = Router::new().nest("/api", users_router);
1051
1052 match app.match_route("/api/users", &Method::POST) {
1054 RouteMatch::MethodNotAllowed { allowed } => {
1055 assert!(allowed.contains(&Method::GET));
1056 assert!(!allowed.contains(&Method::POST));
1057 }
1058 _ => panic!("Should return MethodNotAllowed"),
1059 }
1060 }
1061
1062 #[test]
1063 fn test_nested_route_multiple_methods() {
1064 async fn get_handler() -> &'static str {
1065 "get"
1066 }
1067 async fn post_handler() -> &'static str {
1068 "post"
1069 }
1070
1071 let get_router = get(get_handler);
1073 let post_router = post(post_handler);
1074 let mut combined = MethodRouter::new();
1075 for (method, handler) in get_router.handlers {
1076 combined.handlers.insert(method, handler);
1077 }
1078 for (method, handler) in post_router.handlers {
1079 combined.handlers.insert(method, handler);
1080 }
1081
1082 let users_router = Router::new().route("/users", combined);
1083 let app = Router::new().nest("/api", users_router);
1084
1085 match app.match_route("/api/users", &Method::GET) {
1087 RouteMatch::Found { .. } => {}
1088 _ => panic!("GET should be found"),
1089 }
1090
1091 match app.match_route("/api/users", &Method::POST) {
1092 RouteMatch::Found { .. } => {}
1093 _ => panic!("POST should be found"),
1094 }
1095
1096 match app.match_route("/api/users", &Method::DELETE) {
1098 RouteMatch::MethodNotAllowed { allowed } => {
1099 assert!(allowed.contains(&Method::GET));
1100 assert!(allowed.contains(&Method::POST));
1101 }
1102 _ => panic!("DELETE should return MethodNotAllowed"),
1103 }
1104 }
1105
1106 #[test]
1107 fn test_nested_router_prefix_normalization() {
1108 async fn handler() -> &'static str {
1109 "handler"
1110 }
1111
1112 let router1 = Router::new().route("/test", get(handler));
1114 let app1 = Router::new().nest("api", router1);
1115 assert!(app1.registered_routes().contains_key("/api/test"));
1116
1117 let router2 = Router::new().route("/test", get(handler));
1118 let app2 = Router::new().nest("/api/", router2);
1119 assert!(app2.registered_routes().contains_key("/api/test"));
1120
1121 let router3 = Router::new().route("/test", get(handler));
1122 let app3 = Router::new().nest("//api//", router3);
1123 assert!(app3.registered_routes().contains_key("/api/test"));
1124 }
1125
1126 #[test]
1127 fn test_state_tracking() {
1128 #[derive(Clone)]
1129 struct MyState(#[allow(dead_code)] String);
1130
1131 let router = Router::new().state(MyState("test".to_string()));
1132
1133 assert!(router.has_state::<MyState>());
1134 assert!(!router.has_state::<String>());
1135 }
1136
1137 #[test]
1138 fn test_state_merge_nested_only() {
1139 #[derive(Clone, PartialEq, Debug)]
1140 struct NestedState(String);
1141
1142 async fn handler() -> &'static str {
1143 "handler"
1144 }
1145
1146 let state_source = Router::new().state(NestedState("nested".to_string()));
1148
1149 let nested = Router::new().route("/test", get(handler));
1150
1151 let parent = Router::new()
1152 .nest("/api", nested)
1153 .merge_state::<NestedState>(&state_source);
1154
1155 assert!(parent.has_state::<NestedState>());
1157
1158 let state = parent.state.get::<NestedState>().unwrap();
1160 assert_eq!(state.0, "nested");
1161 }
1162
1163 #[test]
1164 fn test_state_merge_parent_wins() {
1165 #[derive(Clone, PartialEq, Debug)]
1166 struct SharedState(String);
1167
1168 async fn handler() -> &'static str {
1169 "handler"
1170 }
1171
1172 let state_source = Router::new().state(SharedState("nested".to_string()));
1174
1175 let nested = Router::new().route("/test", get(handler));
1176
1177 let parent = Router::new()
1178 .state(SharedState("parent".to_string()))
1179 .nest("/api", nested)
1180 .merge_state::<SharedState>(&state_source);
1181
1182 assert!(parent.has_state::<SharedState>());
1184
1185 let state = parent.state.get::<SharedState>().unwrap();
1187 assert_eq!(state.0, "parent");
1188 }
1189
1190 #[test]
1191 fn test_state_type_ids_merged_on_nest() {
1192 #[derive(Clone)]
1193 struct NestedState(#[allow(dead_code)] String);
1194
1195 async fn handler() -> &'static str {
1196 "handler"
1197 }
1198
1199 let nested = Router::new()
1200 .route("/test", get(handler))
1201 .state(NestedState("nested".to_string()));
1202
1203 let parent = Router::new().nest("/api", nested);
1204
1205 assert!(parent
1207 .state_type_ids()
1208 .contains(&std::any::TypeId::of::<NestedState>()));
1209 }
1210
1211 #[test]
1212 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
1213 fn test_nested_route_conflict_with_existing_route() {
1214 async fn handler1() -> &'static str {
1215 "handler1"
1216 }
1217 async fn handler2() -> &'static str {
1218 "handler2"
1219 }
1220
1221 let parent = Router::new().route("/api/users/{id}", get(handler1));
1223
1224 let nested = Router::new().route("/{user_id}", get(handler2));
1226
1227 let _app = parent.nest("/api/users", nested);
1229 }
1230
1231 #[test]
1232 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
1233 fn test_nested_route_conflict_same_path_different_param_names() {
1234 async fn handler1() -> &'static str {
1235 "handler1"
1236 }
1237 async fn handler2() -> &'static str {
1238 "handler2"
1239 }
1240
1241 let nested1 = Router::new().route("/{id}", get(handler1));
1243 let nested2 = Router::new().route("/{user_id}", get(handler2));
1244
1245 let _app = Router::new()
1247 .nest("/api/users", nested1)
1248 .nest("/api/users", nested2);
1249 }
1250
1251 #[test]
1252 fn test_nested_route_conflict_error_contains_both_paths() {
1253 use std::panic::{catch_unwind, AssertUnwindSafe};
1254
1255 async fn handler1() -> &'static str {
1256 "handler1"
1257 }
1258 async fn handler2() -> &'static str {
1259 "handler2"
1260 }
1261
1262 let result = catch_unwind(AssertUnwindSafe(|| {
1263 let parent = Router::new().route("/api/users/{id}", get(handler1));
1264 let nested = Router::new().route("/{user_id}", get(handler2));
1265 let _app = parent.nest("/api/users", nested);
1266 }));
1267
1268 assert!(result.is_err(), "Should have panicked due to conflict");
1269
1270 if let Err(panic_info) = result {
1271 if let Some(msg) = panic_info.downcast_ref::<String>() {
1272 assert!(
1273 msg.contains("ROUTE CONFLICT DETECTED"),
1274 "Error should contain 'ROUTE CONFLICT DETECTED'"
1275 );
1276 assert!(
1277 msg.contains("Existing:") && msg.contains("New:"),
1278 "Error should contain both 'Existing:' and 'New:' labels"
1279 );
1280 assert!(
1281 msg.contains("How to resolve:"),
1282 "Error should contain resolution guidance"
1283 );
1284 }
1285 }
1286 }
1287
1288 #[test]
1289 fn test_nested_routes_no_conflict_different_prefixes() {
1290 async fn handler1() -> &'static str {
1291 "handler1"
1292 }
1293 async fn handler2() -> &'static str {
1294 "handler2"
1295 }
1296
1297 let nested1 = Router::new().route("/{id}", get(handler1));
1299 let nested2 = Router::new().route("/{id}", get(handler2));
1300
1301 let app = Router::new()
1303 .nest("/api/users", nested1)
1304 .nest("/api/posts", nested2);
1305
1306 assert_eq!(app.registered_routes().len(), 2);
1307 assert!(app.registered_routes().contains_key("/api/users/:id"));
1308 assert!(app.registered_routes().contains_key("/api/posts/:id"));
1309 }
1310
1311 #[test]
1316 fn test_multiple_router_composition_all_routes_registered() {
1317 async fn users_list() -> &'static str {
1318 "users list"
1319 }
1320 async fn users_get() -> &'static str {
1321 "users get"
1322 }
1323 async fn posts_list() -> &'static str {
1324 "posts list"
1325 }
1326 async fn posts_get() -> &'static str {
1327 "posts get"
1328 }
1329 async fn comments_list() -> &'static str {
1330 "comments list"
1331 }
1332
1333 let users_router = Router::new()
1335 .route("/", get(users_list))
1336 .route("/{id}", get(users_get));
1337
1338 let posts_router = Router::new()
1339 .route("/", get(posts_list))
1340 .route("/{id}", get(posts_get));
1341
1342 let comments_router = Router::new().route("/", get(comments_list));
1343
1344 let app = Router::new()
1346 .nest("/api/users", users_router)
1347 .nest("/api/posts", posts_router)
1348 .nest("/api/comments", comments_router);
1349
1350 let routes = app.registered_routes();
1352 assert_eq!(routes.len(), 5, "Should have 5 routes registered");
1353
1354 assert!(
1356 routes.contains_key("/api/users"),
1357 "Should have /api/users route"
1358 );
1359 assert!(
1360 routes.contains_key("/api/users/:id"),
1361 "Should have /api/users/:id route"
1362 );
1363
1364 assert!(
1366 routes.contains_key("/api/posts"),
1367 "Should have /api/posts route"
1368 );
1369 assert!(
1370 routes.contains_key("/api/posts/:id"),
1371 "Should have /api/posts/:id route"
1372 );
1373
1374 assert!(
1376 routes.contains_key("/api/comments"),
1377 "Should have /api/comments route"
1378 );
1379 }
1380
1381 #[test]
1382 fn test_multiple_router_composition_no_interference() {
1383 async fn users_handler() -> &'static str {
1384 "users"
1385 }
1386 async fn posts_handler() -> &'static str {
1387 "posts"
1388 }
1389 async fn admin_handler() -> &'static str {
1390 "admin"
1391 }
1392
1393 let users_router = Router::new()
1395 .route("/list", get(users_handler))
1396 .route("/{id}", get(users_handler));
1397
1398 let posts_router = Router::new()
1399 .route("/list", get(posts_handler))
1400 .route("/{id}", get(posts_handler));
1401
1402 let admin_router = Router::new()
1403 .route("/list", get(admin_handler))
1404 .route("/{id}", get(admin_handler));
1405
1406 let app = Router::new()
1408 .nest("/api/v1/users", users_router)
1409 .nest("/api/v1/posts", posts_router)
1410 .nest("/admin", admin_router);
1411
1412 let routes = app.registered_routes();
1414 assert_eq!(routes.len(), 6, "Should have 6 routes registered");
1415
1416 assert!(routes.contains_key("/api/v1/users/list"));
1418 assert!(routes.contains_key("/api/v1/users/:id"));
1419 assert!(routes.contains_key("/api/v1/posts/list"));
1420 assert!(routes.contains_key("/api/v1/posts/:id"));
1421 assert!(routes.contains_key("/admin/list"));
1422 assert!(routes.contains_key("/admin/:id"));
1423
1424 match app.match_route("/api/v1/users/list", &Method::GET) {
1426 RouteMatch::Found { params, .. } => {
1427 assert!(params.is_empty(), "Static path should have no params");
1428 }
1429 _ => panic!("Should find /api/v1/users/list"),
1430 }
1431
1432 match app.match_route("/api/v1/posts/123", &Method::GET) {
1433 RouteMatch::Found { params, .. } => {
1434 assert_eq!(params.get("id"), Some(&"123".to_string()));
1435 }
1436 _ => panic!("Should find /api/v1/posts/123"),
1437 }
1438
1439 match app.match_route("/admin/456", &Method::GET) {
1440 RouteMatch::Found { params, .. } => {
1441 assert_eq!(params.get("id"), Some(&"456".to_string()));
1442 }
1443 _ => panic!("Should find /admin/456"),
1444 }
1445 }
1446
1447 #[test]
1448 fn test_multiple_router_composition_with_multiple_methods() {
1449 async fn get_handler() -> &'static str {
1450 "get"
1451 }
1452 async fn post_handler() -> &'static str {
1453 "post"
1454 }
1455 async fn put_handler() -> &'static str {
1456 "put"
1457 }
1458
1459 let get_router = get(get_handler);
1462 let post_router = post(post_handler);
1463 let mut users_root_combined = MethodRouter::new();
1464 for (method, handler) in get_router.handlers {
1465 users_root_combined.handlers.insert(method, handler);
1466 }
1467 for (method, handler) in post_router.handlers {
1468 users_root_combined.handlers.insert(method, handler);
1469 }
1470
1471 let get_router2 = get(get_handler);
1473 let put_router = put(put_handler);
1474 let mut users_id_combined = MethodRouter::new();
1475 for (method, handler) in get_router2.handlers {
1476 users_id_combined.handlers.insert(method, handler);
1477 }
1478 for (method, handler) in put_router.handlers {
1479 users_id_combined.handlers.insert(method, handler);
1480 }
1481
1482 let users_router = Router::new()
1483 .route("/", users_root_combined)
1484 .route("/{id}", users_id_combined);
1485
1486 let get_router3 = get(get_handler);
1488 let post_router2 = post(post_handler);
1489 let mut posts_root_combined = MethodRouter::new();
1490 for (method, handler) in get_router3.handlers {
1491 posts_root_combined.handlers.insert(method, handler);
1492 }
1493 for (method, handler) in post_router2.handlers {
1494 posts_root_combined.handlers.insert(method, handler);
1495 }
1496
1497 let posts_router = Router::new().route("/", posts_root_combined);
1498
1499 let app = Router::new()
1501 .nest("/users", users_router)
1502 .nest("/posts", posts_router);
1503
1504 let routes = app.registered_routes();
1506 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1507
1508 let users_root = routes.get("/users").unwrap();
1510 assert!(users_root.methods.contains(&Method::GET));
1511 assert!(users_root.methods.contains(&Method::POST));
1512
1513 let users_id = routes.get("/users/:id").unwrap();
1514 assert!(users_id.methods.contains(&Method::GET));
1515 assert!(users_id.methods.contains(&Method::PUT));
1516
1517 let posts_root = routes.get("/posts").unwrap();
1519 assert!(posts_root.methods.contains(&Method::GET));
1520 assert!(posts_root.methods.contains(&Method::POST));
1521
1522 match app.match_route("/users", &Method::GET) {
1524 RouteMatch::Found { .. } => {}
1525 _ => panic!("GET /users should be found"),
1526 }
1527 match app.match_route("/users", &Method::POST) {
1528 RouteMatch::Found { .. } => {}
1529 _ => panic!("POST /users should be found"),
1530 }
1531 match app.match_route("/users/123", &Method::PUT) {
1532 RouteMatch::Found { .. } => {}
1533 _ => panic!("PUT /users/123 should be found"),
1534 }
1535 }
1536
1537 #[test]
1538 fn test_multiple_router_composition_deep_nesting() {
1539 async fn handler() -> &'static str {
1540 "handler"
1541 }
1542
1543 let deep_router = Router::new().route("/action", get(handler));
1545
1546 let mid_router = Router::new().route("/info", get(handler));
1547
1548 let shallow_router = Router::new().route("/status", get(handler));
1549
1550 let app = Router::new()
1552 .nest("/api/v1/resources/items", deep_router)
1553 .nest("/api/v1/resources", mid_router)
1554 .nest("/api", shallow_router);
1555
1556 let routes = app.registered_routes();
1558 assert_eq!(routes.len(), 3, "Should have 3 routes registered");
1559
1560 assert!(routes.contains_key("/api/v1/resources/items/action"));
1561 assert!(routes.contains_key("/api/v1/resources/info"));
1562 assert!(routes.contains_key("/api/status"));
1563
1564 match app.match_route("/api/v1/resources/items/action", &Method::GET) {
1566 RouteMatch::Found { .. } => {}
1567 _ => panic!("Should find deep route"),
1568 }
1569 match app.match_route("/api/v1/resources/info", &Method::GET) {
1570 RouteMatch::Found { .. } => {}
1571 _ => panic!("Should find mid route"),
1572 }
1573 match app.match_route("/api/status", &Method::GET) {
1574 RouteMatch::Found { .. } => {}
1575 _ => panic!("Should find shallow route"),
1576 }
1577 }
1578}
1579
1580#[cfg(test)]
1581mod property_tests {
1582 use super::*;
1583 use proptest::prelude::*;
1584 use std::panic::{catch_unwind, AssertUnwindSafe};
1585
1586 proptest! {
1594 #![proptest_config(ProptestConfig::with_cases(100))]
1595
1596 #[test]
1601 fn prop_normalized_prefix_starts_with_single_slash(
1602 leading_slashes in prop::collection::vec(Just('/'), 0..5),
1604 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 0..4),
1605 trailing_slashes in prop::collection::vec(Just('/'), 0..5),
1606 ) {
1607 let mut prefix = String::new();
1609 for _ in &leading_slashes {
1610 prefix.push('/');
1611 }
1612 for (i, segment) in segments.iter().enumerate() {
1613 if i > 0 {
1614 prefix.push('/');
1615 }
1616 prefix.push_str(segment);
1617 }
1618 for _ in &trailing_slashes {
1619 prefix.push('/');
1620 }
1621
1622 let normalized = normalize_prefix(&prefix);
1623
1624 prop_assert!(
1626 normalized.starts_with('/'),
1627 "Normalized prefix '{}' should start with '/', input was '{}'",
1628 normalized, prefix
1629 );
1630
1631 prop_assert!(
1633 !normalized.starts_with("//"),
1634 "Normalized prefix '{}' should not start with '//', input was '{}'",
1635 normalized, prefix
1636 );
1637 }
1638
1639 #[test]
1644 fn prop_normalized_prefix_no_trailing_slash(
1645 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1646 trailing_slashes in prop::collection::vec(Just('/'), 0..5),
1647 ) {
1648 let mut prefix = String::from("/");
1650 for (i, segment) in segments.iter().enumerate() {
1651 if i > 0 {
1652 prefix.push('/');
1653 }
1654 prefix.push_str(segment);
1655 }
1656 for _ in &trailing_slashes {
1657 prefix.push('/');
1658 }
1659
1660 let normalized = normalize_prefix(&prefix);
1661
1662 prop_assert!(
1664 !normalized.ends_with('/'),
1665 "Normalized prefix '{}' should not end with '/', input was '{}'",
1666 normalized, prefix
1667 );
1668 }
1669
1670 #[test]
1675 fn prop_normalized_prefix_no_double_slashes(
1676 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
1678 extra_slashes in prop::collection::vec(0..4usize, 1..4),
1679 ) {
1680 let mut prefix = String::from("/");
1682 for (i, segment) in segments.iter().enumerate() {
1683 if i > 0 {
1684 let num_slashes = extra_slashes.get(i).copied().unwrap_or(1);
1686 for _ in 0..=num_slashes {
1687 prefix.push('/');
1688 }
1689 }
1690 prefix.push_str(segment);
1691 }
1692
1693 let normalized = normalize_prefix(&prefix);
1694
1695 prop_assert!(
1697 !normalized.contains("//"),
1698 "Normalized prefix '{}' should not contain '//', input was '{}'",
1699 normalized, prefix
1700 );
1701 }
1702
1703 #[test]
1708 fn prop_normalized_prefix_preserves_segments(
1709 segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..4),
1710 ) {
1711 let prefix = format!("/{}", segments.join("/"));
1713
1714 let normalized = normalize_prefix(&prefix);
1715
1716 let normalized_segments: Vec<&str> = normalized
1718 .split('/')
1719 .filter(|s| !s.is_empty())
1720 .collect();
1721
1722 prop_assert_eq!(
1723 segments.len(),
1724 normalized_segments.len(),
1725 "Segment count should be preserved"
1726 );
1727
1728 for (original, normalized_seg) in segments.iter().zip(normalized_segments.iter()) {
1729 prop_assert_eq!(
1730 original, normalized_seg,
1731 "Segment content should be preserved"
1732 );
1733 }
1734 }
1735
1736 #[test]
1741 fn prop_empty_or_slashes_normalize_to_root(
1742 num_slashes in 0..10usize,
1743 ) {
1744 let prefix = "/".repeat(num_slashes);
1745
1746 let normalized = normalize_prefix(&prefix);
1747
1748 prop_assert_eq!(
1749 normalized, "/",
1750 "Empty or slash-only prefix '{}' should normalize to '/'",
1751 prefix
1752 );
1753 }
1754 }
1755
1756 proptest! {
1763 #![proptest_config(ProptestConfig::with_cases(100))]
1764
1765 #[test]
1770 fn prop_method_router_clone_preserves_methods(
1771 use_get in any::<bool>(),
1773 use_post in any::<bool>(),
1774 use_put in any::<bool>(),
1775 use_patch in any::<bool>(),
1776 use_delete in any::<bool>(),
1777 ) {
1778 prop_assume!(use_get || use_post || use_put || use_patch || use_delete);
1780
1781 let mut method_router = MethodRouter::new();
1783 let mut expected_methods: Vec<Method> = Vec::new();
1784
1785 async fn handler() -> &'static str { "handler" }
1786
1787 if use_get {
1788 method_router = get(handler);
1789 expected_methods.push(Method::GET);
1790 }
1791
1792 if use_post {
1793 let post_router = post(handler);
1794 for (method, handler) in post_router.handlers {
1795 method_router.handlers.insert(method.clone(), handler);
1796 if !expected_methods.contains(&method) {
1797 expected_methods.push(method);
1798 }
1799 }
1800 }
1801
1802 if use_put {
1803 let put_router = put(handler);
1804 for (method, handler) in put_router.handlers {
1805 method_router.handlers.insert(method.clone(), handler);
1806 if !expected_methods.contains(&method) {
1807 expected_methods.push(method);
1808 }
1809 }
1810 }
1811
1812 if use_patch {
1813 let patch_router = patch(handler);
1814 for (method, handler) in patch_router.handlers {
1815 method_router.handlers.insert(method.clone(), handler);
1816 if !expected_methods.contains(&method) {
1817 expected_methods.push(method);
1818 }
1819 }
1820 }
1821
1822 if use_delete {
1823 let delete_router = delete(handler);
1824 for (method, handler) in delete_router.handlers {
1825 method_router.handlers.insert(method.clone(), handler);
1826 if !expected_methods.contains(&method) {
1827 expected_methods.push(method);
1828 }
1829 }
1830 }
1831
1832 let cloned_router = method_router.clone();
1834
1835 let original_methods = method_router.allowed_methods();
1837 let cloned_methods = cloned_router.allowed_methods();
1838
1839 prop_assert_eq!(
1840 original_methods.len(),
1841 cloned_methods.len(),
1842 "Cloned router should have same number of methods"
1843 );
1844
1845 for method in &expected_methods {
1846 prop_assert!(
1847 cloned_router.get_handler(method).is_some(),
1848 "Cloned router should have handler for method {:?}",
1849 method
1850 );
1851 }
1852
1853 for method in &cloned_methods {
1855 prop_assert!(
1856 cloned_router.get_handler(method).is_some(),
1857 "Handler for {:?} should be accessible after clone",
1858 method
1859 );
1860 }
1861 }
1862 }
1863
1864 proptest! {
1872 #![proptest_config(ProptestConfig::with_cases(100))]
1873
1874 #[test]
1879 fn prop_nested_routes_have_prefix(
1880 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1882 route_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1884 has_param in any::<bool>(),
1885 ) {
1886 async fn handler() -> &'static str { "handler" }
1887
1888 let prefix = format!("/{}", prefix_segments.join("/"));
1890
1891 let mut route_path = format!("/{}", route_segments.join("/"));
1893 if has_param {
1894 route_path.push_str("/{id}");
1895 }
1896
1897 let nested_router = Router::new().route(&route_path, get(handler));
1899 let app = Router::new().nest(&prefix, nested_router);
1900
1901 let expected_matchit_path = if has_param {
1903 format!("{}/{}/:id", prefix, route_segments.join("/"))
1904 } else {
1905 format!("{}/{}", prefix, route_segments.join("/"))
1906 };
1907
1908 let routes = app.registered_routes();
1909
1910 prop_assert!(
1912 routes.contains_key(&expected_matchit_path),
1913 "Expected route '{}' not found. Available routes: {:?}",
1914 expected_matchit_path,
1915 routes.keys().collect::<Vec<_>>()
1916 );
1917
1918 let route_info = routes.get(&expected_matchit_path).unwrap();
1920 let expected_display_path = format!("{}{}", prefix, route_path);
1921 prop_assert_eq!(
1922 &route_info.path, &expected_display_path,
1923 "Display path should be prefix + original path"
1924 );
1925 }
1926
1927 #[test]
1932 fn prop_route_count_preserved_after_nesting(
1933 num_routes in 1..4usize,
1935 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
1936 ) {
1937 async fn handler() -> &'static str { "handler" }
1938
1939 let prefix = format!("/{}", prefix_segments.join("/"));
1940
1941 let mut nested_router = Router::new();
1943 for i in 0..num_routes {
1944 let path = format!("/route{}", i);
1945 nested_router = nested_router.route(&path, get(handler));
1946 }
1947
1948 let app = Router::new().nest(&prefix, nested_router);
1949
1950 prop_assert_eq!(
1951 app.registered_routes().len(),
1952 num_routes,
1953 "Number of routes should be preserved after nesting"
1954 );
1955 }
1956
1957 #[test]
1961 fn prop_nested_routes_are_matchable(
1962 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1963 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
1964 ) {
1965 async fn handler() -> &'static str { "handler" }
1966
1967 let prefix = format!("/{}", prefix_segments.join("/"));
1968 let route_path = format!("/{}", route_segments.join("/"));
1969
1970 let nested_router = Router::new().route(&route_path, get(handler));
1971 let app = Router::new().nest(&prefix, nested_router);
1972
1973 let full_path = format!("{}{}", prefix, route_path);
1975
1976 match app.match_route(&full_path, &Method::GET) {
1978 RouteMatch::Found { .. } => {
1979 }
1981 RouteMatch::NotFound => {
1982 prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
1983 }
1984 RouteMatch::MethodNotAllowed { .. } => {
1985 prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
1986 }
1987 }
1988 }
1989 }
1990
1991 proptest! {
1998 #![proptest_config(ProptestConfig::with_cases(100))]
1999
2000 #[test]
2005 fn prop_state_type_ids_merged(
2006 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2007 has_nested_state in any::<bool>(),
2008 ) {
2009 #[derive(Clone)]
2010 struct TestState(#[allow(dead_code)] i32);
2011
2012 async fn handler() -> &'static str { "handler" }
2013
2014 let prefix = format!("/{}", prefix_segments.join("/"));
2015
2016 let mut nested = Router::new().route("/test", get(handler));
2017 if has_nested_state {
2018 nested = nested.state(TestState(42));
2019 }
2020
2021 let parent = Router::new().nest(&prefix, nested);
2022
2023 if has_nested_state {
2025 prop_assert!(
2026 parent.state_type_ids().contains(&std::any::TypeId::of::<TestState>()),
2027 "Parent should track nested state type ID"
2028 );
2029 }
2030 }
2031
2032 #[test]
2037 fn prop_merge_state_adds_nested_state(
2038 state_value in any::<i32>(),
2039 ) {
2040 #[derive(Clone, PartialEq, Debug)]
2041 struct UniqueState(i32);
2042
2043 let source = Router::new().state(UniqueState(state_value));
2045
2046 let parent = Router::new().merge_state::<UniqueState>(&source);
2048
2049 prop_assert!(
2051 parent.has_state::<UniqueState>(),
2052 "Parent should have state after merge"
2053 );
2054
2055 let merged_state = parent.state.get::<UniqueState>().unwrap();
2057 prop_assert_eq!(
2058 merged_state.0, state_value,
2059 "Merged state value should match source"
2060 );
2061 }
2062 }
2063
2064 proptest! {
2071 #![proptest_config(ProptestConfig::with_cases(100))]
2072
2073 #[test]
2078 fn prop_parent_state_takes_precedence(
2079 parent_value in any::<i32>(),
2080 nested_value in any::<i32>(),
2081 ) {
2082 prop_assume!(parent_value != nested_value);
2084
2085 #[derive(Clone, PartialEq, Debug)]
2086 struct SharedState(i32);
2087
2088 let source = Router::new().state(SharedState(nested_value));
2090
2091 let parent = Router::new()
2093 .state(SharedState(parent_value))
2094 .merge_state::<SharedState>(&source);
2095
2096 prop_assert!(
2098 parent.has_state::<SharedState>(),
2099 "Parent should have state"
2100 );
2101
2102 let final_state = parent.state.get::<SharedState>().unwrap();
2104 prop_assert_eq!(
2105 final_state.0, parent_value,
2106 "Parent state value should be preserved, not overwritten by nested"
2107 );
2108 }
2109
2110 #[test]
2115 fn prop_state_precedence_consistent(
2116 parent_value in any::<i32>(),
2117 source1_value in any::<i32>(),
2118 source2_value in any::<i32>(),
2119 ) {
2120 #[derive(Clone, PartialEq, Debug)]
2121 struct ConsistentState(i32);
2122
2123 let source1 = Router::new().state(ConsistentState(source1_value));
2125 let source2 = Router::new().state(ConsistentState(source2_value));
2126
2127 let parent = Router::new()
2129 .state(ConsistentState(parent_value))
2130 .merge_state::<ConsistentState>(&source1)
2131 .merge_state::<ConsistentState>(&source2);
2132
2133 let final_state = parent.state.get::<ConsistentState>().unwrap();
2135 prop_assert_eq!(
2136 final_state.0, parent_value,
2137 "Parent state should be preserved after multiple merges"
2138 );
2139 }
2140 }
2141
2142 proptest! {
2150 #![proptest_config(ProptestConfig::with_cases(100))]
2151
2152 #[test]
2157 fn prop_same_structure_different_param_names_conflict(
2158 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
2160 param1 in "[a-z][a-z0-9]{0,5}",
2162 param2 in "[a-z][a-z0-9]{0,5}",
2163 ) {
2164 prop_assume!(param1 != param2);
2166
2167 let mut path1 = String::from("/");
2169 let mut path2 = String::from("/");
2170
2171 for segment in &segments {
2172 path1.push_str(segment);
2173 path1.push('/');
2174 path2.push_str(segment);
2175 path2.push('/');
2176 }
2177
2178 path1.push('{');
2179 path1.push_str(¶m1);
2180 path1.push('}');
2181
2182 path2.push('{');
2183 path2.push_str(¶m2);
2184 path2.push('}');
2185
2186 let result = catch_unwind(AssertUnwindSafe(|| {
2188 async fn handler1() -> &'static str { "handler1" }
2189 async fn handler2() -> &'static str { "handler2" }
2190
2191 let _router = Router::new()
2192 .route(&path1, get(handler1))
2193 .route(&path2, get(handler2));
2194 }));
2195
2196 prop_assert!(
2197 result.is_err(),
2198 "Routes '{}' and '{}' should conflict but didn't",
2199 path1, path2
2200 );
2201 }
2202
2203 #[test]
2208 fn prop_different_structures_no_conflict(
2209 segments1 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2211 segments2 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
2212 has_param1 in any::<bool>(),
2214 has_param2 in any::<bool>(),
2215 ) {
2216 let mut path1 = String::from("/");
2218 let mut path2 = String::from("/");
2219
2220 for segment in &segments1 {
2221 path1.push_str(segment);
2222 path1.push('/');
2223 }
2224 path1.pop(); for segment in &segments2 {
2227 path2.push_str(segment);
2228 path2.push('/');
2229 }
2230 path2.pop(); if has_param1 {
2233 path1.push_str("/{id}");
2234 }
2235
2236 if has_param2 {
2237 path2.push_str("/{id}");
2238 }
2239
2240 let norm1 = normalize_path_for_comparison(&convert_path_params(&path1));
2242 let norm2 = normalize_path_for_comparison(&convert_path_params(&path2));
2243
2244 prop_assume!(norm1 != norm2);
2246
2247 let result = catch_unwind(AssertUnwindSafe(|| {
2249 async fn handler1() -> &'static str { "handler1" }
2250 async fn handler2() -> &'static str { "handler2" }
2251
2252 let router = Router::new()
2253 .route(&path1, get(handler1))
2254 .route(&path2, get(handler2));
2255
2256 router.registered_routes().len()
2257 }));
2258
2259 prop_assert!(
2260 result.is_ok(),
2261 "Routes '{}' and '{}' should not conflict but did",
2262 path1, path2
2263 );
2264
2265 if let Ok(count) = result {
2266 prop_assert_eq!(count, 2, "Should have registered 2 routes");
2267 }
2268 }
2269
2270 #[test]
2275 fn prop_conflict_error_contains_both_paths(
2276 segment in "[a-z][a-z0-9]{1,5}",
2278 param1 in "[a-z][a-z0-9]{1,5}",
2279 param2 in "[a-z][a-z0-9]{1,5}",
2280 ) {
2281 prop_assume!(param1 != param2);
2282
2283 let path1 = format!("/{}/{{{}}}", segment, param1);
2284 let path2 = format!("/{}/{{{}}}", segment, param2);
2285
2286 let result = catch_unwind(AssertUnwindSafe(|| {
2287 async fn handler1() -> &'static str { "handler1" }
2288 async fn handler2() -> &'static str { "handler2" }
2289
2290 let _router = Router::new()
2291 .route(&path1, get(handler1))
2292 .route(&path2, get(handler2));
2293 }));
2294
2295 prop_assert!(result.is_err(), "Should have panicked due to conflict");
2296
2297 if let Err(panic_info) = result {
2299 if let Some(msg) = panic_info.downcast_ref::<String>() {
2300 prop_assert!(
2301 msg.contains("ROUTE CONFLICT DETECTED"),
2302 "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
2303 msg
2304 );
2305 prop_assert!(
2306 msg.contains("Existing:") && msg.contains("New:"),
2307 "Error should contain both 'Existing:' and 'New:' labels, got: {}",
2308 msg
2309 );
2310 prop_assert!(
2311 msg.contains("How to resolve:"),
2312 "Error should contain resolution guidance, got: {}",
2313 msg
2314 );
2315 }
2316 }
2317 }
2318
2319 #[test]
2323 fn prop_exact_duplicate_paths_conflict(
2324 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
2326 has_param in any::<bool>(),
2327 ) {
2328 let mut path = String::from("/");
2330
2331 for segment in &segments {
2332 path.push_str(segment);
2333 path.push('/');
2334 }
2335 path.pop(); if has_param {
2338 path.push_str("/{id}");
2339 }
2340
2341 let result = catch_unwind(AssertUnwindSafe(|| {
2343 async fn handler1() -> &'static str { "handler1" }
2344 async fn handler2() -> &'static str { "handler2" }
2345
2346 let _router = Router::new()
2347 .route(&path, get(handler1))
2348 .route(&path, get(handler2));
2349 }));
2350
2351 prop_assert!(
2352 result.is_err(),
2353 "Registering path '{}' twice should conflict but didn't",
2354 path
2355 );
2356 }
2357 }
2358
2359 proptest! {
2366 #![proptest_config(ProptestConfig::with_cases(100))]
2367
2368 #[test]
2373 fn prop_nested_route_with_params_matches(
2374 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2375 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
2376 param_value in "[a-z0-9]{1,10}",
2377 ) {
2378 async fn handler() -> &'static str { "handler" }
2379
2380 let prefix = format!("/{}", prefix_segments.join("/"));
2381 let route_path = if route_segments.is_empty() {
2382 "/{id}".to_string()
2383 } else {
2384 format!("/{}/{{id}}", route_segments.join("/"))
2385 };
2386
2387 let nested_router = Router::new().route(&route_path, get(handler));
2388 let app = Router::new().nest(&prefix, nested_router);
2389
2390 let full_path = if route_segments.is_empty() {
2392 format!("{}/{}", prefix, param_value)
2393 } else {
2394 format!("{}/{}/{}", prefix, route_segments.join("/"), param_value)
2395 };
2396
2397 match app.match_route(&full_path, &Method::GET) {
2399 RouteMatch::Found { params, .. } => {
2400 prop_assert!(
2402 params.contains_key("id"),
2403 "Should have 'id' parameter, got: {:?}",
2404 params
2405 );
2406 prop_assert_eq!(
2407 params.get("id").unwrap(),
2408 ¶m_value,
2409 "Parameter value should match"
2410 );
2411 }
2412 RouteMatch::NotFound => {
2413 prop_assert!(false, "Route '{}' should be found but got NotFound", full_path);
2414 }
2415 RouteMatch::MethodNotAllowed { .. } => {
2416 prop_assert!(false, "Route '{}' should be found but got MethodNotAllowed", full_path);
2417 }
2418 }
2419 }
2420
2421 #[test]
2426 fn prop_nested_route_matches_correct_method(
2427 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2428 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..2),
2429 use_get in any::<bool>(),
2430 ) {
2431 async fn handler() -> &'static str { "handler" }
2432
2433 let prefix = format!("/{}", prefix_segments.join("/"));
2434 let route_path = format!("/{}", route_segments.join("/"));
2435
2436 let method_router = if use_get { get(handler) } else { post(handler) };
2438 let nested_router = Router::new().route(&route_path, method_router);
2439 let app = Router::new().nest(&prefix, nested_router);
2440
2441 let full_path = format!("{}{}", prefix, route_path);
2442 let registered_method = if use_get { Method::GET } else { Method::POST };
2443 let other_method = if use_get { Method::POST } else { Method::GET };
2444
2445 match app.match_route(&full_path, ®istered_method) {
2447 RouteMatch::Found { .. } => {
2448 }
2450 other => {
2451 prop_assert!(false, "Route should be found for registered method, got: {:?}",
2452 match other {
2453 RouteMatch::NotFound => "NotFound",
2454 RouteMatch::MethodNotAllowed { .. } => "MethodNotAllowed",
2455 _ => "Found",
2456 }
2457 );
2458 }
2459 }
2460
2461 match app.match_route(&full_path, &other_method) {
2463 RouteMatch::MethodNotAllowed { allowed } => {
2464 prop_assert!(
2465 allowed.contains(®istered_method),
2466 "Allowed methods should contain {:?}",
2467 registered_method
2468 );
2469 }
2470 other => {
2471 prop_assert!(false, "Route should return MethodNotAllowed for other method, got: {:?}",
2472 match other {
2473 RouteMatch::NotFound => "NotFound",
2474 RouteMatch::Found { .. } => "Found",
2475 _ => "MethodNotAllowed",
2476 }
2477 );
2478 }
2479 }
2480 }
2481 }
2482
2483 proptest! {
2490 #![proptest_config(ProptestConfig::with_cases(100))]
2491
2492 #[test]
2497 fn prop_single_param_extraction(
2498 prefix in "[a-z][a-z0-9]{1,5}",
2499 param_name in "[a-z][a-z0-9]{1,5}",
2500 param_value in "[a-z0-9]{1,10}",
2501 ) {
2502 async fn handler() -> &'static str { "handler" }
2503
2504 let prefix = format!("/{}", prefix);
2505 let route_path = format!("/{{{}}}", param_name);
2506
2507 let nested_router = Router::new().route(&route_path, get(handler));
2508 let app = Router::new().nest(&prefix, nested_router);
2509
2510 let full_path = format!("{}/{}", prefix, param_value);
2511
2512 match app.match_route(&full_path, &Method::GET) {
2513 RouteMatch::Found { params, .. } => {
2514 prop_assert!(
2515 params.contains_key(¶m_name),
2516 "Should have '{}' parameter, got: {:?}",
2517 param_name, params
2518 );
2519 prop_assert_eq!(
2520 params.get(¶m_name).unwrap(),
2521 ¶m_value,
2522 "Parameter '{}' value should be '{}'",
2523 param_name, param_value
2524 );
2525 }
2526 _ => {
2527 prop_assert!(false, "Route should be found");
2528 }
2529 }
2530 }
2531
2532 #[test]
2537 fn prop_multiple_params_extraction(
2538 prefix in "[a-z][a-z0-9]{1,5}",
2539 param1_name in "[a-z]{1,5}",
2540 param1_value in "[a-z0-9]{1,10}",
2541 param2_name in "[a-z]{1,5}",
2542 param2_value in "[a-z0-9]{1,10}",
2543 ) {
2544 prop_assume!(param1_name != param2_name);
2546
2547 async fn handler() -> &'static str { "handler" }
2548
2549 let prefix = format!("/{}", prefix);
2550 let route_path = format!("/{{{}}}/items/{{{}}}", param1_name, param2_name);
2551
2552 let nested_router = Router::new().route(&route_path, get(handler));
2553 let app = Router::new().nest(&prefix, nested_router);
2554
2555 let full_path = format!("{}/{}/items/{}", prefix, param1_value, param2_value);
2556
2557 match app.match_route(&full_path, &Method::GET) {
2558 RouteMatch::Found { params, .. } => {
2559 prop_assert!(
2561 params.contains_key(¶m1_name),
2562 "Should have '{}' parameter, got: {:?}",
2563 param1_name, params
2564 );
2565 prop_assert_eq!(
2566 params.get(¶m1_name).unwrap(),
2567 ¶m1_value,
2568 "Parameter '{}' value should be '{}'",
2569 param1_name, param1_value
2570 );
2571
2572 prop_assert!(
2574 params.contains_key(¶m2_name),
2575 "Should have '{}' parameter, got: {:?}",
2576 param2_name, params
2577 );
2578 prop_assert_eq!(
2579 params.get(¶m2_name).unwrap(),
2580 ¶m2_value,
2581 "Parameter '{}' value should be '{}'",
2582 param2_name, param2_value
2583 );
2584 }
2585 _ => {
2586 prop_assert!(false, "Route should be found");
2587 }
2588 }
2589 }
2590
2591 #[test]
2596 fn prop_param_value_preservation(
2597 prefix in "[a-z]{1,5}",
2598 param_value in "[a-zA-Z0-9_-]{1,15}",
2600 ) {
2601 async fn handler() -> &'static str { "handler" }
2602
2603 let prefix = format!("/{}", prefix);
2604 let route_path = "/{id}".to_string();
2605
2606 let nested_router = Router::new().route(&route_path, get(handler));
2607 let app = Router::new().nest(&prefix, nested_router);
2608
2609 let full_path = format!("{}/{}", prefix, param_value);
2610
2611 match app.match_route(&full_path, &Method::GET) {
2612 RouteMatch::Found { params, .. } => {
2613 prop_assert_eq!(
2614 params.get("id").unwrap(),
2615 ¶m_value,
2616 "Parameter value should be preserved exactly"
2617 );
2618 }
2619 _ => {
2620 prop_assert!(false, "Route should be found");
2621 }
2622 }
2623 }
2624 }
2625
2626 proptest! {
2633 #![proptest_config(ProptestConfig::with_cases(100))]
2634
2635 #[test]
2640 fn prop_unregistered_path_returns_not_found(
2641 prefix in "[a-z][a-z0-9]{1,5}",
2642 route_segment in "[a-z][a-z0-9]{1,5}",
2643 unregistered_segment in "[a-z][a-z0-9]{6,10}",
2644 ) {
2645 prop_assume!(route_segment != unregistered_segment);
2647
2648 async fn handler() -> &'static str { "handler" }
2649
2650 let prefix = format!("/{}", prefix);
2651 let route_path = format!("/{}", route_segment);
2652
2653 let nested_router = Router::new().route(&route_path, get(handler));
2654 let app = Router::new().nest(&prefix, nested_router);
2655
2656 let unregistered_path = format!("{}/{}", prefix, unregistered_segment);
2658
2659 match app.match_route(&unregistered_path, &Method::GET) {
2660 RouteMatch::NotFound => {
2661 }
2663 RouteMatch::Found { .. } => {
2664 prop_assert!(false, "Path '{}' should not be found", unregistered_path);
2665 }
2666 RouteMatch::MethodNotAllowed { .. } => {
2667 prop_assert!(false, "Path '{}' should return NotFound, not MethodNotAllowed", unregistered_path);
2668 }
2669 }
2670 }
2671
2672 #[test]
2676 fn prop_wrong_prefix_returns_not_found(
2677 prefix1 in "[a-z][a-z0-9]{1,5}",
2678 prefix2 in "[a-z][a-z0-9]{6,10}",
2679 route_segment in "[a-z][a-z0-9]{1,5}",
2680 ) {
2681 prop_assume!(prefix1 != prefix2);
2683
2684 async fn handler() -> &'static str { "handler" }
2685
2686 let prefix = format!("/{}", prefix1);
2687 let route_path = format!("/{}", route_segment);
2688
2689 let nested_router = Router::new().route(&route_path, get(handler));
2690 let app = Router::new().nest(&prefix, nested_router);
2691
2692 let wrong_prefix_path = format!("/{}/{}", prefix2, route_segment);
2694
2695 match app.match_route(&wrong_prefix_path, &Method::GET) {
2696 RouteMatch::NotFound => {
2697 }
2699 _ => {
2700 prop_assert!(false, "Path '{}' with wrong prefix should return NotFound", wrong_prefix_path);
2701 }
2702 }
2703 }
2704
2705 #[test]
2710 fn prop_partial_path_returns_not_found(
2711 prefix in "[a-z][a-z0-9]{1,5}",
2712 segment1 in "[a-z][a-z0-9]{1,5}",
2713 segment2 in "[a-z][a-z0-9]{1,5}",
2714 ) {
2715 async fn handler() -> &'static str { "handler" }
2716
2717 let prefix = format!("/{}", prefix);
2718 let route_path = format!("/{}/{}", segment1, segment2);
2719
2720 let nested_router = Router::new().route(&route_path, get(handler));
2721 let app = Router::new().nest(&prefix, nested_router);
2722
2723 let partial_path = format!("{}/{}", prefix, segment1);
2725
2726 match app.match_route(&partial_path, &Method::GET) {
2727 RouteMatch::NotFound => {
2728 }
2730 _ => {
2731 prop_assert!(false, "Partial path '{}' should return NotFound", partial_path);
2732 }
2733 }
2734 }
2735 }
2736
2737 proptest! {
2744 #![proptest_config(ProptestConfig::with_cases(100))]
2745
2746 #[test]
2752 fn prop_unregistered_method_returns_method_not_allowed(
2753 prefix in "[a-z][a-z0-9]{1,5}",
2754 route_segment in "[a-z][a-z0-9]{1,5}",
2755 ) {
2756 async fn handler() -> &'static str { "handler" }
2757
2758 let prefix = format!("/{}", prefix);
2759 let route_path = format!("/{}", route_segment);
2760
2761 let nested_router = Router::new().route(&route_path, get(handler));
2763 let app = Router::new().nest(&prefix, nested_router);
2764
2765 let full_path = format!("{}{}", prefix, route_path);
2766
2767 match app.match_route(&full_path, &Method::POST) {
2769 RouteMatch::MethodNotAllowed { allowed } => {
2770 prop_assert!(
2771 allowed.contains(&Method::GET),
2772 "Allowed methods should contain GET, got: {:?}",
2773 allowed
2774 );
2775 prop_assert!(
2776 !allowed.contains(&Method::POST),
2777 "Allowed methods should not contain POST"
2778 );
2779 }
2780 RouteMatch::Found { .. } => {
2781 prop_assert!(false, "POST should not be found on GET-only route");
2782 }
2783 RouteMatch::NotFound => {
2784 prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
2785 }
2786 }
2787 }
2788
2789 #[test]
2794 fn prop_multiple_methods_in_allowed_list(
2795 prefix in "[a-z][a-z0-9]{1,5}",
2796 route_segment in "[a-z][a-z0-9]{1,5}",
2797 use_get in any::<bool>(),
2798 use_post in any::<bool>(),
2799 use_put in any::<bool>(),
2800 ) {
2801 prop_assume!(use_get || use_post || use_put);
2803
2804 async fn handler() -> &'static str { "handler" }
2805
2806 let prefix = format!("/{}", prefix);
2807 let route_path = format!("/{}", route_segment);
2808
2809 let mut method_router = MethodRouter::new();
2811 let mut expected_methods: Vec<Method> = Vec::new();
2812
2813 if use_get {
2814 let get_router = get(handler);
2815 for (method, h) in get_router.handlers {
2816 method_router.handlers.insert(method.clone(), h);
2817 expected_methods.push(method);
2818 }
2819 }
2820 if use_post {
2821 let post_router = post(handler);
2822 for (method, h) in post_router.handlers {
2823 method_router.handlers.insert(method.clone(), h);
2824 expected_methods.push(method);
2825 }
2826 }
2827 if use_put {
2828 let put_router = put(handler);
2829 for (method, h) in put_router.handlers {
2830 method_router.handlers.insert(method.clone(), h);
2831 expected_methods.push(method);
2832 }
2833 }
2834
2835 let nested_router = Router::new().route(&route_path, method_router);
2836 let app = Router::new().nest(&prefix, nested_router);
2837
2838 let full_path = format!("{}{}", prefix, route_path);
2839
2840 match app.match_route(&full_path, &Method::DELETE) {
2842 RouteMatch::MethodNotAllowed { allowed } => {
2843 for method in &expected_methods {
2845 prop_assert!(
2846 allowed.contains(method),
2847 "Allowed methods should contain {:?}, got: {:?}",
2848 method, allowed
2849 );
2850 }
2851 prop_assert!(
2853 !allowed.contains(&Method::DELETE),
2854 "Allowed methods should not contain DELETE"
2855 );
2856 }
2857 RouteMatch::Found { .. } => {
2858 prop_assert!(false, "DELETE should not be found");
2859 }
2860 RouteMatch::NotFound => {
2861 prop_assert!(false, "Path exists, should return MethodNotAllowed not NotFound");
2862 }
2863 }
2864 }
2865 }
2866
2867 proptest! {
2881 #![proptest_config(ProptestConfig::with_cases(100))]
2882
2883 #[test]
2889 fn prop_multiple_routers_all_routes_registered(
2890 prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2892 prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
2893 num_routes1 in 1..4usize,
2895 num_routes2 in 1..4usize,
2896 ) {
2897 let prefix1 = format!("/{}", prefix1_segments.join("/"));
2899 let prefix2 = format!("/{}", prefix2_segments.join("/"));
2900
2901 prop_assume!(prefix1 != prefix2);
2903
2904 async fn handler() -> &'static str { "handler" }
2905
2906 let mut router1 = Router::new();
2908 for i in 0..num_routes1 {
2909 let path = format!("/route1_{}", i);
2910 router1 = router1.route(&path, get(handler));
2911 }
2912
2913 let mut router2 = Router::new();
2915 for i in 0..num_routes2 {
2916 let path = format!("/route2_{}", i);
2917 router2 = router2.route(&path, get(handler));
2918 }
2919
2920 let app = Router::new()
2922 .nest(&prefix1, router1)
2923 .nest(&prefix2, router2);
2924
2925 let routes = app.registered_routes();
2926
2927 let expected_count = num_routes1 + num_routes2;
2929 prop_assert_eq!(
2930 routes.len(),
2931 expected_count,
2932 "Should have {} routes ({}+{}), got {}",
2933 expected_count, num_routes1, num_routes2, routes.len()
2934 );
2935
2936 for i in 0..num_routes1 {
2938 let expected_path = format!("{}/route1_{}", prefix1, i);
2939 let matchit_path = convert_path_params(&expected_path);
2940 prop_assert!(
2941 routes.contains_key(&matchit_path),
2942 "Route '{}' should be registered",
2943 expected_path
2944 );
2945 }
2946
2947 for i in 0..num_routes2 {
2949 let expected_path = format!("{}/route2_{}", prefix2, i);
2950 let matchit_path = convert_path_params(&expected_path);
2951 prop_assert!(
2952 routes.contains_key(&matchit_path),
2953 "Route '{}' should be registered",
2954 expected_path
2955 );
2956 }
2957 }
2958
2959 #[test]
2964 fn prop_multiple_routers_no_interference(
2965 prefix1 in "[a-z][a-z0-9]{1,5}",
2966 prefix2 in "[a-z][a-z0-9]{1,5}",
2967 route_segment in "[a-z][a-z0-9]{1,5}",
2968 param_value1 in "[a-z0-9]{1,10}",
2969 param_value2 in "[a-z0-9]{1,10}",
2970 ) {
2971 prop_assume!(prefix1 != prefix2);
2973
2974 let prefix1 = format!("/{}", prefix1);
2975 let prefix2 = format!("/{}", prefix2);
2976
2977 async fn handler() -> &'static str { "handler" }
2978
2979 let router1 = Router::new()
2981 .route(&format!("/{}", route_segment), get(handler))
2982 .route("/{id}", get(handler));
2983
2984 let router2 = Router::new()
2985 .route(&format!("/{}", route_segment), get(handler))
2986 .route("/{id}", get(handler));
2987
2988 let app = Router::new()
2990 .nest(&prefix1, router1)
2991 .nest(&prefix2, router2);
2992
2993 let path1_static = format!("{}/{}", prefix1, route_segment);
2995 match app.match_route(&path1_static, &Method::GET) {
2996 RouteMatch::Found { params, .. } => {
2997 prop_assert!(params.is_empty(), "Static path should have no params");
2998 }
2999 _ => {
3000 prop_assert!(false, "Route '{}' should be found", path1_static);
3001 }
3002 }
3003
3004 let path1_param = format!("{}/{}", prefix1, param_value1);
3005 match app.match_route(&path1_param, &Method::GET) {
3006 RouteMatch::Found { params, .. } => {
3007 prop_assert_eq!(
3008 params.get("id"),
3009 Some(¶m_value1.to_string()),
3010 "Parameter should be extracted correctly"
3011 );
3012 }
3013 _ => {
3014 prop_assert!(false, "Route '{}' should be found", path1_param);
3015 }
3016 }
3017
3018 let path2_static = format!("{}/{}", prefix2, route_segment);
3020 match app.match_route(&path2_static, &Method::GET) {
3021 RouteMatch::Found { params, .. } => {
3022 prop_assert!(params.is_empty(), "Static path should have no params");
3023 }
3024 _ => {
3025 prop_assert!(false, "Route '{}' should be found", path2_static);
3026 }
3027 }
3028
3029 let path2_param = format!("{}/{}", prefix2, param_value2);
3030 match app.match_route(&path2_param, &Method::GET) {
3031 RouteMatch::Found { params, .. } => {
3032 prop_assert_eq!(
3033 params.get("id"),
3034 Some(¶m_value2.to_string()),
3035 "Parameter should be extracted correctly"
3036 );
3037 }
3038 _ => {
3039 prop_assert!(false, "Route '{}' should be found", path2_param);
3040 }
3041 }
3042 }
3043
3044 #[test]
3049 fn prop_multiple_routers_preserve_methods(
3050 prefix1 in "[a-z][a-z0-9]{1,5}",
3051 prefix2 in "[a-z][a-z0-9]{1,5}",
3052 route_segment in "[a-z][a-z0-9]{1,5}",
3053 router1_use_get in any::<bool>(),
3054 router1_use_post in any::<bool>(),
3055 router2_use_get in any::<bool>(),
3056 router2_use_put in any::<bool>(),
3057 ) {
3058 prop_assume!(router1_use_get || router1_use_post);
3060 prop_assume!(router2_use_get || router2_use_put);
3061 prop_assume!(prefix1 != prefix2);
3063
3064 let prefix1 = format!("/{}", prefix1);
3065 let prefix2 = format!("/{}", prefix2);
3066 let route_path = format!("/{}", route_segment);
3067
3068 async fn handler() -> &'static str { "handler" }
3069
3070 let mut method_router1 = MethodRouter::new();
3072 let mut expected_methods1: Vec<Method> = Vec::new();
3073 if router1_use_get {
3074 let get_router = get(handler);
3075 for (method, h) in get_router.handlers {
3076 method_router1.handlers.insert(method.clone(), h);
3077 expected_methods1.push(method);
3078 }
3079 }
3080 if router1_use_post {
3081 let post_router = post(handler);
3082 for (method, h) in post_router.handlers {
3083 method_router1.handlers.insert(method.clone(), h);
3084 expected_methods1.push(method);
3085 }
3086 }
3087
3088 let mut method_router2 = MethodRouter::new();
3090 let mut expected_methods2: Vec<Method> = Vec::new();
3091 if router2_use_get {
3092 let get_router = get(handler);
3093 for (method, h) in get_router.handlers {
3094 method_router2.handlers.insert(method.clone(), h);
3095 expected_methods2.push(method);
3096 }
3097 }
3098 if router2_use_put {
3099 let put_router = put(handler);
3100 for (method, h) in put_router.handlers {
3101 method_router2.handlers.insert(method.clone(), h);
3102 expected_methods2.push(method);
3103 }
3104 }
3105
3106 let router1 = Router::new().route(&route_path, method_router1);
3107 let router2 = Router::new().route(&route_path, method_router2);
3108
3109 let app = Router::new()
3110 .nest(&prefix1, router1)
3111 .nest(&prefix2, router2);
3112
3113 let full_path1 = format!("{}{}", prefix1, route_path);
3114 let full_path2 = format!("{}{}", prefix2, route_path);
3115
3116 for method in &expected_methods1 {
3118 match app.match_route(&full_path1, method) {
3119 RouteMatch::Found { .. } => {}
3120 _ => {
3121 prop_assert!(false, "Method {:?} should be found for {}", method, full_path1);
3122 }
3123 }
3124 }
3125
3126 for method in &expected_methods2 {
3128 match app.match_route(&full_path2, method) {
3129 RouteMatch::Found { .. } => {}
3130 _ => {
3131 prop_assert!(false, "Method {:?} should be found for {}", method, full_path2);
3132 }
3133 }
3134 }
3135
3136 if !expected_methods1.contains(&Method::DELETE) {
3138 match app.match_route(&full_path1, &Method::DELETE) {
3139 RouteMatch::MethodNotAllowed { allowed } => {
3140 for method in &expected_methods1 {
3141 prop_assert!(
3142 allowed.contains(method),
3143 "Allowed methods for {} should contain {:?}",
3144 full_path1, method
3145 );
3146 }
3147 }
3148 _ => {
3149 prop_assert!(false, "DELETE should return MethodNotAllowed for {}", full_path1);
3150 }
3151 }
3152 }
3153 }
3154
3155 #[test]
3160 fn prop_three_routers_composition(
3161 prefix1 in "[a-z]{1,3}",
3162 prefix2 in "[a-z]{4,6}",
3163 prefix3 in "[a-z]{7,9}",
3164 num_routes in 1..3usize,
3165 ) {
3166 let prefix1 = format!("/{}", prefix1);
3167 let prefix2 = format!("/{}", prefix2);
3168 let prefix3 = format!("/{}", prefix3);
3169
3170 async fn handler() -> &'static str { "handler" }
3171
3172 let mut router1 = Router::new();
3174 let mut router2 = Router::new();
3175 let mut router3 = Router::new();
3176
3177 for i in 0..num_routes {
3178 let path = format!("/item{}", i);
3179 router1 = router1.route(&path, get(handler));
3180 router2 = router2.route(&path, get(handler));
3181 router3 = router3.route(&path, get(handler));
3182 }
3183
3184 let app = Router::new()
3186 .nest(&prefix1, router1)
3187 .nest(&prefix2, router2)
3188 .nest(&prefix3, router3);
3189
3190 let routes = app.registered_routes();
3191
3192 let expected_count = 3 * num_routes;
3194 prop_assert_eq!(
3195 routes.len(),
3196 expected_count,
3197 "Should have {} routes, got {}",
3198 expected_count, routes.len()
3199 );
3200
3201 for i in 0..num_routes {
3203 let path1 = format!("{}/item{}", prefix1, i);
3204 let path2 = format!("{}/item{}", prefix2, i);
3205 let path3 = format!("{}/item{}", prefix3, i);
3206
3207 match app.match_route(&path1, &Method::GET) {
3208 RouteMatch::Found { .. } => {}
3209 _ => prop_assert!(false, "Route '{}' should be found", path1),
3210 }
3211 match app.match_route(&path2, &Method::GET) {
3212 RouteMatch::Found { .. } => {}
3213 _ => prop_assert!(false, "Route '{}' should be found", path2),
3214 }
3215 match app.match_route(&path3, &Method::GET) {
3216 RouteMatch::Found { .. } => {}
3217 _ => prop_assert!(false, "Route '{}' should be found", path3),
3218 }
3219 }
3220 }
3221 }
3222 proptest! {
3223 #![proptest_config(ProptestConfig::with_cases(100))]
3224
3225 #[test]
3231 fn prop_nested_route_conflict_different_param_names(
3232 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3233 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 0..2),
3234 param1 in "[a-z][a-z0-9]{1,5}",
3235 param2 in "[a-z][a-z0-9]{1,5}",
3236 ) {
3237 prop_assume!(param1 != param2);
3239
3240 async fn handler1() -> &'static str { "handler1" }
3241 async fn handler2() -> &'static str { "handler2" }
3242
3243 let prefix = format!("/{}", prefix_segments.join("/"));
3244
3245 let existing_path = if route_segments.is_empty() {
3247 format!("{}/{{{}}}", prefix, param1)
3248 } else {
3249 format!("{}/{}/{{{}}}", prefix, route_segments.join("/"), param1)
3250 };
3251
3252 let nested_path = if route_segments.is_empty() {
3254 format!("/{{{}}}", param2)
3255 } else {
3256 format!("/{}/{{{}}}", route_segments.join("/"), param2)
3257 };
3258
3259 let result = catch_unwind(AssertUnwindSafe(|| {
3261 let parent = Router::new().route(&existing_path, get(handler1));
3262 let nested = Router::new().route(&nested_path, get(handler2));
3263 let _app = parent.nest(&prefix, nested);
3264 }));
3265
3266 prop_assert!(
3268 result.is_err(),
3269 "Nested route '{}{}' should conflict with existing route '{}' but didn't",
3270 prefix, nested_path, existing_path
3271 );
3272
3273 if let Err(panic_info) = result {
3275 if let Some(msg) = panic_info.downcast_ref::<String>() {
3276 prop_assert!(
3277 msg.contains("ROUTE CONFLICT DETECTED"),
3278 "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
3279 msg
3280 );
3281 prop_assert!(
3282 msg.contains("Existing:") && msg.contains("New:"),
3283 "Error should contain both 'Existing:' and 'New:' labels, got: {}",
3284 msg
3285 );
3286 }
3287 }
3288 }
3289
3290 #[test]
3295 fn prop_nested_route_conflict_exact_same_path(
3296 prefix_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3297 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3298 ) {
3299 async fn handler1() -> &'static str { "handler1" }
3300 async fn handler2() -> &'static str { "handler2" }
3301
3302 let prefix = format!("/{}", prefix_segments.join("/"));
3303 let route_path = format!("/{}", route_segments.join("/"));
3304
3305 let existing_path = format!("{}{}", prefix, route_path);
3307
3308 let result = catch_unwind(AssertUnwindSafe(|| {
3310 let parent = Router::new().route(&existing_path, get(handler1));
3311 let nested = Router::new().route(&route_path, get(handler2));
3312 let _app = parent.nest(&prefix, nested);
3313 }));
3314
3315 prop_assert!(
3317 result.is_err(),
3318 "Nested route '{}{}' should conflict with existing route '{}' but didn't",
3319 prefix, route_path, existing_path
3320 );
3321 }
3322
3323 #[test]
3328 fn prop_nested_routes_different_prefixes_no_conflict(
3329 prefix1_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3330 prefix2_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3331 route_segments in prop::collection::vec("[a-z][a-z0-9]{1,5}", 1..3),
3332 has_param in any::<bool>(),
3333 ) {
3334 let prefix1 = format!("/{}", prefix1_segments.join("/"));
3336 let prefix2 = format!("/{}", prefix2_segments.join("/"));
3337
3338 prop_assume!(prefix1 != prefix2);
3340
3341 async fn handler1() -> &'static str { "handler1" }
3342 async fn handler2() -> &'static str { "handler2" }
3343
3344 let route_path = if has_param {
3346 format!("/{}/{{id}}", route_segments.join("/"))
3347 } else {
3348 format!("/{}", route_segments.join("/"))
3349 };
3350
3351 let result = catch_unwind(AssertUnwindSafe(|| {
3353 let nested1 = Router::new().route(&route_path, get(handler1));
3354 let nested2 = Router::new().route(&route_path, get(handler2));
3355
3356 let app = Router::new()
3357 .nest(&prefix1, nested1)
3358 .nest(&prefix2, nested2);
3359
3360 app.registered_routes().len()
3361 }));
3362
3363 prop_assert!(
3365 result.is_ok(),
3366 "Routes under different prefixes '{}' and '{}' should not conflict",
3367 prefix1, prefix2
3368 );
3369
3370 if let Ok(count) = result {
3371 prop_assert_eq!(count, 2, "Should have registered 2 routes");
3372 }
3373 }
3374
3375 #[test]
3380 fn prop_nested_conflict_error_contains_guidance(
3381 prefix in "[a-z][a-z0-9]{1,5}",
3382 segment in "[a-z][a-z0-9]{1,5}",
3383 param1 in "[a-z][a-z0-9]{1,5}",
3384 param2 in "[a-z][a-z0-9]{1,5}",
3385 ) {
3386 prop_assume!(param1 != param2);
3387
3388 async fn handler1() -> &'static str { "handler1" }
3389 async fn handler2() -> &'static str { "handler2" }
3390
3391 let prefix = format!("/{}", prefix);
3392 let existing_path = format!("{}/{}/{{{}}}", prefix, segment, param1);
3393 let nested_path = format!("/{}/{{{}}}", segment, param2);
3394
3395 let result = catch_unwind(AssertUnwindSafe(|| {
3396 let parent = Router::new().route(&existing_path, get(handler1));
3397 let nested = Router::new().route(&nested_path, get(handler2));
3398 let _app = parent.nest(&prefix, nested);
3399 }));
3400
3401 prop_assert!(result.is_err(), "Should have detected conflict");
3402
3403 if let Err(panic_info) = result {
3404 if let Some(msg) = panic_info.downcast_ref::<String>() {
3405 prop_assert!(
3406 msg.contains("How to resolve:"),
3407 "Error should contain 'How to resolve:' guidance, got: {}",
3408 msg
3409 );
3410 prop_assert!(
3411 msg.contains("Use different path patterns") ||
3412 msg.contains("different path patterns"),
3413 "Error should suggest using different path patterns, got: {}",
3414 msg
3415 );
3416 }
3417 }
3418 }
3419 }
3420}