1use crate::handler::{into_boxed_handler, BoxedHandler, Handler};
45use http::{Extensions, Method};
46use matchit::Router as MatchitRouter;
47use rustapi_openapi::Operation;
48use std::collections::HashMap;
49use std::sync::Arc;
50
51#[derive(Debug, Clone)]
53pub struct RouteInfo {
54 pub path: String,
56 pub methods: Vec<Method>,
58}
59
60#[derive(Debug, Clone)]
62pub struct RouteConflictError {
63 pub new_path: String,
65 pub method: Option<Method>,
67 pub existing_path: String,
69 pub details: String,
71}
72
73impl std::fmt::Display for RouteConflictError {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 writeln!(
76 f,
77 "\n╭─────────────────────────────────────────────────────────────╮"
78 )?;
79 writeln!(
80 f,
81 "│ ROUTE CONFLICT DETECTED │"
82 )?;
83 writeln!(
84 f,
85 "╰─────────────────────────────────────────────────────────────╯"
86 )?;
87 writeln!(f)?;
88 writeln!(f, " Conflicting routes:")?;
89 writeln!(f, " → Existing: {}", self.existing_path)?;
90 writeln!(f, " → New: {}", self.new_path)?;
91 writeln!(f)?;
92 if let Some(ref method) = self.method {
93 writeln!(f, " HTTP Method: {}", method)?;
94 writeln!(f)?;
95 }
96 writeln!(f, " Details: {}", self.details)?;
97 writeln!(f)?;
98 writeln!(f, " How to resolve:")?;
99 writeln!(f, " 1. Use different path patterns for each route")?;
100 writeln!(
101 f,
102 " 2. If paths must be similar, ensure parameter names differ"
103 )?;
104 writeln!(
105 f,
106 " 3. Consider using different HTTP methods if appropriate"
107 )?;
108 writeln!(f)?;
109 writeln!(f, " Example:")?;
110 writeln!(f, " Instead of:")?;
111 writeln!(f, " .route(\"/users/{{id}}\", get(handler1))")?;
112 writeln!(f, " .route(\"/users/{{user_id}}\", get(handler2))")?;
113 writeln!(f)?;
114 writeln!(f, " Use:")?;
115 writeln!(f, " .route(\"/users/{{id}}\", get(handler1))")?;
116 writeln!(f, " .route(\"/users/{{id}}/profile\", get(handler2))")?;
117 Ok(())
118 }
119}
120
121impl std::error::Error for RouteConflictError {}
122
123pub struct MethodRouter {
125 handlers: HashMap<Method, BoxedHandler>,
126 pub(crate) operations: HashMap<Method, Operation>,
127}
128
129impl MethodRouter {
130 pub fn new() -> Self {
132 Self {
133 handlers: HashMap::new(),
134 operations: HashMap::new(),
135 }
136 }
137
138 fn on(mut self, method: Method, handler: BoxedHandler, operation: Operation) -> Self {
140 self.handlers.insert(method.clone(), handler);
141 self.operations.insert(method, operation);
142 self
143 }
144
145 pub(crate) fn get_handler(&self, method: &Method) -> Option<&BoxedHandler> {
147 self.handlers.get(method)
148 }
149
150 pub(crate) fn allowed_methods(&self) -> Vec<Method> {
152 self.handlers.keys().cloned().collect()
153 }
154
155 pub(crate) fn from_boxed(handlers: HashMap<Method, BoxedHandler>) -> Self {
157 Self {
158 handlers,
159 operations: HashMap::new(), }
161 }
162
163 pub(crate) fn insert_boxed_with_operation(
167 &mut self,
168 method: Method,
169 handler: BoxedHandler,
170 operation: Operation,
171 ) {
172 if self.handlers.contains_key(&method) {
173 panic!(
174 "Duplicate handler for method {} on the same path",
175 method.as_str()
176 );
177 }
178
179 self.handlers.insert(method.clone(), handler);
180 self.operations.insert(method, operation);
181 }
182}
183
184impl Default for MethodRouter {
185 fn default() -> Self {
186 Self::new()
187 }
188}
189
190pub fn get<H, T>(handler: H) -> MethodRouter
192where
193 H: Handler<T>,
194 T: 'static,
195{
196 let mut op = Operation::new();
197 H::update_operation(&mut op);
198 MethodRouter::new().on(Method::GET, into_boxed_handler(handler), op)
199}
200
201pub fn post<H, T>(handler: H) -> MethodRouter
203where
204 H: Handler<T>,
205 T: 'static,
206{
207 let mut op = Operation::new();
208 H::update_operation(&mut op);
209 MethodRouter::new().on(Method::POST, into_boxed_handler(handler), op)
210}
211
212pub fn put<H, T>(handler: H) -> MethodRouter
214where
215 H: Handler<T>,
216 T: 'static,
217{
218 let mut op = Operation::new();
219 H::update_operation(&mut op);
220 MethodRouter::new().on(Method::PUT, into_boxed_handler(handler), op)
221}
222
223pub fn patch<H, T>(handler: H) -> MethodRouter
225where
226 H: Handler<T>,
227 T: 'static,
228{
229 let mut op = Operation::new();
230 H::update_operation(&mut op);
231 MethodRouter::new().on(Method::PATCH, into_boxed_handler(handler), op)
232}
233
234pub fn delete<H, T>(handler: H) -> MethodRouter
236where
237 H: Handler<T>,
238 T: 'static,
239{
240 let mut op = Operation::new();
241 H::update_operation(&mut op);
242 MethodRouter::new().on(Method::DELETE, into_boxed_handler(handler), op)
243}
244
245pub struct Router {
247 inner: MatchitRouter<MethodRouter>,
248 state: Arc<Extensions>,
249 registered_routes: HashMap<String, RouteInfo>,
251}
252
253impl Router {
254 pub fn new() -> Self {
256 Self {
257 inner: MatchitRouter::new(),
258 state: Arc::new(Extensions::new()),
259 registered_routes: HashMap::new(),
260 }
261 }
262
263 pub fn route(mut self, path: &str, method_router: MethodRouter) -> Self {
265 let matchit_path = convert_path_params(path);
267
268 let methods: Vec<Method> = method_router.handlers.keys().cloned().collect();
270
271 match self.inner.insert(matchit_path.clone(), method_router) {
272 Ok(_) => {
273 self.registered_routes.insert(
275 matchit_path.clone(),
276 RouteInfo {
277 path: path.to_string(),
278 methods,
279 },
280 );
281 }
282 Err(e) => {
283 let existing_path = self
285 .find_conflicting_route(&matchit_path)
286 .map(|info| info.path.clone())
287 .unwrap_or_else(|| "<unknown>".to_string());
288
289 let conflict_error = RouteConflictError {
290 new_path: path.to_string(),
291 method: methods.first().cloned(),
292 existing_path,
293 details: e.to_string(),
294 };
295
296 panic!("{}", conflict_error);
297 }
298 }
299 self
300 }
301
302 fn find_conflicting_route(&self, matchit_path: &str) -> Option<&RouteInfo> {
304 if let Some(info) = self.registered_routes.get(matchit_path) {
306 return Some(info);
307 }
308
309 let normalized_new = normalize_path_for_comparison(matchit_path);
311
312 for (registered_path, info) in &self.registered_routes {
313 let normalized_existing = normalize_path_for_comparison(registered_path);
314 if normalized_new == normalized_existing {
315 return Some(info);
316 }
317 }
318
319 None
320 }
321
322 pub fn state<S: Clone + Send + Sync + 'static>(mut self, state: S) -> Self {
324 let extensions = Arc::make_mut(&mut self.state);
325 extensions.insert(state);
326 self
327 }
328
329 pub fn nest(self, _prefix: &str, _router: Router) -> Self {
331 self
333 }
334
335 pub(crate) fn match_route(&self, path: &str, method: &Method) -> RouteMatch<'_> {
337 match self.inner.at(path) {
338 Ok(matched) => {
339 let method_router = matched.value;
340
341 if let Some(handler) = method_router.get_handler(method) {
342 let params: HashMap<String, String> = matched
344 .params
345 .iter()
346 .map(|(k, v)| (k.to_string(), v.to_string()))
347 .collect();
348
349 RouteMatch::Found { handler, params }
350 } else {
351 RouteMatch::MethodNotAllowed {
352 allowed: method_router.allowed_methods(),
353 }
354 }
355 }
356 Err(_) => RouteMatch::NotFound,
357 }
358 }
359
360 pub(crate) fn state_ref(&self) -> Arc<Extensions> {
362 self.state.clone()
363 }
364
365 pub fn registered_routes(&self) -> &HashMap<String, RouteInfo> {
367 &self.registered_routes
368 }
369}
370
371impl Default for Router {
372 fn default() -> Self {
373 Self::new()
374 }
375}
376
377pub(crate) enum RouteMatch<'a> {
379 Found {
380 handler: &'a BoxedHandler,
381 params: HashMap<String, String>,
382 },
383 NotFound,
384 MethodNotAllowed {
385 allowed: Vec<Method>,
386 },
387}
388
389fn convert_path_params(path: &str) -> String {
391 let mut result = String::with_capacity(path.len());
392
393 for ch in path.chars() {
394 match ch {
395 '{' => {
396 result.push(':');
397 }
398 '}' => {
399 }
401 _ => {
402 result.push(ch);
403 }
404 }
405 }
406
407 result
408}
409
410fn normalize_path_for_comparison(path: &str) -> String {
412 let mut result = String::with_capacity(path.len());
413 let mut in_param = false;
414
415 for ch in path.chars() {
416 match ch {
417 ':' => {
418 in_param = true;
419 result.push_str(":_");
420 }
421 '/' => {
422 in_param = false;
423 result.push('/');
424 }
425 _ if in_param => {
426 }
428 _ => {
429 result.push(ch);
430 }
431 }
432 }
433
434 result
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 #[test]
442 fn test_convert_path_params() {
443 assert_eq!(convert_path_params("/users/{id}"), "/users/:id");
444 assert_eq!(
445 convert_path_params("/users/{user_id}/posts/{post_id}"),
446 "/users/:user_id/posts/:post_id"
447 );
448 assert_eq!(convert_path_params("/static/path"), "/static/path");
449 }
450
451 #[test]
452 fn test_normalize_path_for_comparison() {
453 assert_eq!(normalize_path_for_comparison("/users/:id"), "/users/:_");
454 assert_eq!(
455 normalize_path_for_comparison("/users/:user_id"),
456 "/users/:_"
457 );
458 assert_eq!(
459 normalize_path_for_comparison("/users/:id/posts/:post_id"),
460 "/users/:_/posts/:_"
461 );
462 assert_eq!(
463 normalize_path_for_comparison("/static/path"),
464 "/static/path"
465 );
466 }
467
468 #[test]
469 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
470 fn test_route_conflict_detection() {
471 async fn handler1() -> &'static str {
472 "handler1"
473 }
474 async fn handler2() -> &'static str {
475 "handler2"
476 }
477
478 let _router = Router::new()
479 .route("/users/{id}", get(handler1))
480 .route("/users/{user_id}", get(handler2)); }
482
483 #[test]
484 fn test_no_conflict_different_paths() {
485 async fn handler1() -> &'static str {
486 "handler1"
487 }
488 async fn handler2() -> &'static str {
489 "handler2"
490 }
491
492 let router = Router::new()
493 .route("/users/{id}", get(handler1))
494 .route("/users/{id}/profile", get(handler2));
495
496 assert_eq!(router.registered_routes().len(), 2);
497 }
498
499 #[test]
500 fn test_route_info_tracking() {
501 async fn handler() -> &'static str {
502 "handler"
503 }
504
505 let router = Router::new().route("/users/{id}", get(handler));
506
507 let routes = router.registered_routes();
508 assert_eq!(routes.len(), 1);
509
510 let info = routes.get("/users/:id").unwrap();
511 assert_eq!(info.path, "/users/{id}");
512 assert_eq!(info.methods.len(), 1);
513 assert_eq!(info.methods[0], Method::GET);
514 }
515}
516
517#[cfg(test)]
518mod property_tests {
519 use super::*;
520 use proptest::prelude::*;
521 use std::panic::{catch_unwind, AssertUnwindSafe};
522
523 proptest! {
531 #![proptest_config(ProptestConfig::with_cases(100))]
532
533 #[test]
538 fn prop_same_structure_different_param_names_conflict(
539 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
541 param1 in "[a-z][a-z0-9]{0,5}",
543 param2 in "[a-z][a-z0-9]{0,5}",
544 ) {
545 prop_assume!(param1 != param2);
547
548 let mut path1 = String::from("/");
550 let mut path2 = String::from("/");
551
552 for segment in &segments {
553 path1.push_str(segment);
554 path1.push('/');
555 path2.push_str(segment);
556 path2.push('/');
557 }
558
559 path1.push('{');
560 path1.push_str(¶m1);
561 path1.push('}');
562
563 path2.push('{');
564 path2.push_str(¶m2);
565 path2.push('}');
566
567 let result = catch_unwind(AssertUnwindSafe(|| {
569 async fn handler1() -> &'static str { "handler1" }
570 async fn handler2() -> &'static str { "handler2" }
571
572 let _router = Router::new()
573 .route(&path1, get(handler1))
574 .route(&path2, get(handler2));
575 }));
576
577 prop_assert!(
578 result.is_err(),
579 "Routes '{}' and '{}' should conflict but didn't",
580 path1, path2
581 );
582 }
583
584 #[test]
589 fn prop_different_structures_no_conflict(
590 segments1 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
592 segments2 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
593 has_param1 in any::<bool>(),
595 has_param2 in any::<bool>(),
596 ) {
597 let mut path1 = String::from("/");
599 let mut path2 = String::from("/");
600
601 for segment in &segments1 {
602 path1.push_str(segment);
603 path1.push('/');
604 }
605 path1.pop(); for segment in &segments2 {
608 path2.push_str(segment);
609 path2.push('/');
610 }
611 path2.pop(); if has_param1 {
614 path1.push_str("/{id}");
615 }
616
617 if has_param2 {
618 path2.push_str("/{id}");
619 }
620
621 let norm1 = normalize_path_for_comparison(&convert_path_params(&path1));
623 let norm2 = normalize_path_for_comparison(&convert_path_params(&path2));
624
625 prop_assume!(norm1 != norm2);
627
628 let result = catch_unwind(AssertUnwindSafe(|| {
630 async fn handler1() -> &'static str { "handler1" }
631 async fn handler2() -> &'static str { "handler2" }
632
633 let router = Router::new()
634 .route(&path1, get(handler1))
635 .route(&path2, get(handler2));
636
637 router.registered_routes().len()
638 }));
639
640 prop_assert!(
641 result.is_ok(),
642 "Routes '{}' and '{}' should not conflict but did",
643 path1, path2
644 );
645
646 if let Ok(count) = result {
647 prop_assert_eq!(count, 2, "Should have registered 2 routes");
648 }
649 }
650
651 #[test]
656 fn prop_conflict_error_contains_both_paths(
657 segment in "[a-z][a-z0-9]{1,5}",
659 param1 in "[a-z][a-z0-9]{1,5}",
660 param2 in "[a-z][a-z0-9]{1,5}",
661 ) {
662 prop_assume!(param1 != param2);
663
664 let path1 = format!("/{}/{{{}}}", segment, param1);
665 let path2 = format!("/{}/{{{}}}", segment, param2);
666
667 let result = catch_unwind(AssertUnwindSafe(|| {
668 async fn handler1() -> &'static str { "handler1" }
669 async fn handler2() -> &'static str { "handler2" }
670
671 let _router = Router::new()
672 .route(&path1, get(handler1))
673 .route(&path2, get(handler2));
674 }));
675
676 prop_assert!(result.is_err(), "Should have panicked due to conflict");
677
678 if let Err(panic_info) = result {
680 if let Some(msg) = panic_info.downcast_ref::<String>() {
681 prop_assert!(
682 msg.contains("ROUTE CONFLICT DETECTED"),
683 "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
684 msg
685 );
686 prop_assert!(
687 msg.contains("Existing:") && msg.contains("New:"),
688 "Error should contain both 'Existing:' and 'New:' labels, got: {}",
689 msg
690 );
691 prop_assert!(
692 msg.contains("How to resolve:"),
693 "Error should contain resolution guidance, got: {}",
694 msg
695 );
696 }
697 }
698 }
699
700 #[test]
704 fn prop_exact_duplicate_paths_conflict(
705 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
707 has_param in any::<bool>(),
708 ) {
709 let mut path = String::from("/");
711
712 for segment in &segments {
713 path.push_str(segment);
714 path.push('/');
715 }
716 path.pop(); if has_param {
719 path.push_str("/{id}");
720 }
721
722 let result = catch_unwind(AssertUnwindSafe(|| {
724 async fn handler1() -> &'static str { "handler1" }
725 async fn handler2() -> &'static str { "handler2" }
726
727 let _router = Router::new()
728 .route(&path, get(handler1))
729 .route(&path, get(handler2));
730 }));
731
732 prop_assert!(
733 result.is_err(),
734 "Registering path '{}' twice should conflict but didn't",
735 path
736 );
737 }
738 }
739}