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
164impl Default for MethodRouter {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170pub fn get<H, T>(handler: H) -> MethodRouter
172where
173 H: Handler<T>,
174 T: 'static,
175{
176 let mut op = Operation::new();
177 H::update_operation(&mut op);
178 MethodRouter::new().on(Method::GET, into_boxed_handler(handler), op)
179}
180
181pub fn post<H, T>(handler: H) -> MethodRouter
183where
184 H: Handler<T>,
185 T: 'static,
186{
187 let mut op = Operation::new();
188 H::update_operation(&mut op);
189 MethodRouter::new().on(Method::POST, into_boxed_handler(handler), op)
190}
191
192pub fn put<H, T>(handler: H) -> MethodRouter
194where
195 H: Handler<T>,
196 T: 'static,
197{
198 let mut op = Operation::new();
199 H::update_operation(&mut op);
200 MethodRouter::new().on(Method::PUT, into_boxed_handler(handler), op)
201}
202
203pub fn patch<H, T>(handler: H) -> MethodRouter
205where
206 H: Handler<T>,
207 T: 'static,
208{
209 let mut op = Operation::new();
210 H::update_operation(&mut op);
211 MethodRouter::new().on(Method::PATCH, into_boxed_handler(handler), op)
212}
213
214pub fn delete<H, T>(handler: H) -> MethodRouter
216where
217 H: Handler<T>,
218 T: 'static,
219{
220 let mut op = Operation::new();
221 H::update_operation(&mut op);
222 MethodRouter::new().on(Method::DELETE, into_boxed_handler(handler), op)
223}
224
225pub struct Router {
227 inner: MatchitRouter<MethodRouter>,
228 state: Arc<Extensions>,
229 registered_routes: HashMap<String, RouteInfo>,
231}
232
233impl Router {
234 pub fn new() -> Self {
236 Self {
237 inner: MatchitRouter::new(),
238 state: Arc::new(Extensions::new()),
239 registered_routes: HashMap::new(),
240 }
241 }
242
243 pub fn route(mut self, path: &str, method_router: MethodRouter) -> Self {
245 let matchit_path = convert_path_params(path);
247
248 let methods: Vec<Method> = method_router.handlers.keys().cloned().collect();
250
251 match self.inner.insert(matchit_path.clone(), method_router) {
252 Ok(_) => {
253 self.registered_routes.insert(
255 matchit_path.clone(),
256 RouteInfo {
257 path: path.to_string(),
258 methods,
259 },
260 );
261 }
262 Err(e) => {
263 let existing_path = self
265 .find_conflicting_route(&matchit_path)
266 .map(|info| info.path.clone())
267 .unwrap_or_else(|| "<unknown>".to_string());
268
269 let conflict_error = RouteConflictError {
270 new_path: path.to_string(),
271 method: methods.first().cloned(),
272 existing_path,
273 details: e.to_string(),
274 };
275
276 panic!("{}", conflict_error);
277 }
278 }
279 self
280 }
281
282 fn find_conflicting_route(&self, matchit_path: &str) -> Option<&RouteInfo> {
284 if let Some(info) = self.registered_routes.get(matchit_path) {
286 return Some(info);
287 }
288
289 let normalized_new = normalize_path_for_comparison(matchit_path);
291
292 for (registered_path, info) in &self.registered_routes {
293 let normalized_existing = normalize_path_for_comparison(registered_path);
294 if normalized_new == normalized_existing {
295 return Some(info);
296 }
297 }
298
299 None
300 }
301
302 pub fn state<S: Clone + Send + Sync + 'static>(mut self, state: S) -> Self {
304 let extensions = Arc::make_mut(&mut self.state);
305 extensions.insert(state);
306 self
307 }
308
309 pub fn nest(self, _prefix: &str, _router: Router) -> Self {
311 self
313 }
314
315 pub(crate) fn match_route(&self, path: &str, method: &Method) -> RouteMatch<'_> {
317 match self.inner.at(path) {
318 Ok(matched) => {
319 let method_router = matched.value;
320
321 if let Some(handler) = method_router.get_handler(method) {
322 let params: HashMap<String, String> = matched
324 .params
325 .iter()
326 .map(|(k, v)| (k.to_string(), v.to_string()))
327 .collect();
328
329 RouteMatch::Found { handler, params }
330 } else {
331 RouteMatch::MethodNotAllowed {
332 allowed: method_router.allowed_methods(),
333 }
334 }
335 }
336 Err(_) => RouteMatch::NotFound,
337 }
338 }
339
340 pub(crate) fn state_ref(&self) -> Arc<Extensions> {
342 self.state.clone()
343 }
344
345 pub fn registered_routes(&self) -> &HashMap<String, RouteInfo> {
347 &self.registered_routes
348 }
349}
350
351impl Default for Router {
352 fn default() -> Self {
353 Self::new()
354 }
355}
356
357pub(crate) enum RouteMatch<'a> {
359 Found {
360 handler: &'a BoxedHandler,
361 params: HashMap<String, String>,
362 },
363 NotFound,
364 MethodNotAllowed {
365 allowed: Vec<Method>,
366 },
367}
368
369fn convert_path_params(path: &str) -> String {
371 let mut result = String::with_capacity(path.len());
372
373 for ch in path.chars() {
374 match ch {
375 '{' => {
376 result.push(':');
377 }
378 '}' => {
379 }
381 _ => {
382 result.push(ch);
383 }
384 }
385 }
386
387 result
388}
389
390fn normalize_path_for_comparison(path: &str) -> String {
392 let mut result = String::with_capacity(path.len());
393 let mut in_param = false;
394
395 for ch in path.chars() {
396 match ch {
397 ':' => {
398 in_param = true;
399 result.push_str(":_");
400 }
401 '/' => {
402 in_param = false;
403 result.push('/');
404 }
405 _ if in_param => {
406 }
408 _ => {
409 result.push(ch);
410 }
411 }
412 }
413
414 result
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
422 fn test_convert_path_params() {
423 assert_eq!(convert_path_params("/users/{id}"), "/users/:id");
424 assert_eq!(
425 convert_path_params("/users/{user_id}/posts/{post_id}"),
426 "/users/:user_id/posts/:post_id"
427 );
428 assert_eq!(convert_path_params("/static/path"), "/static/path");
429 }
430
431 #[test]
432 fn test_normalize_path_for_comparison() {
433 assert_eq!(normalize_path_for_comparison("/users/:id"), "/users/:_");
434 assert_eq!(
435 normalize_path_for_comparison("/users/:user_id"),
436 "/users/:_"
437 );
438 assert_eq!(
439 normalize_path_for_comparison("/users/:id/posts/:post_id"),
440 "/users/:_/posts/:_"
441 );
442 assert_eq!(
443 normalize_path_for_comparison("/static/path"),
444 "/static/path"
445 );
446 }
447
448 #[test]
449 #[should_panic(expected = "ROUTE CONFLICT DETECTED")]
450 fn test_route_conflict_detection() {
451 async fn handler1() -> &'static str {
452 "handler1"
453 }
454 async fn handler2() -> &'static str {
455 "handler2"
456 }
457
458 let _router = Router::new()
459 .route("/users/{id}", get(handler1))
460 .route("/users/{user_id}", get(handler2)); }
462
463 #[test]
464 fn test_no_conflict_different_paths() {
465 async fn handler1() -> &'static str {
466 "handler1"
467 }
468 async fn handler2() -> &'static str {
469 "handler2"
470 }
471
472 let router = Router::new()
473 .route("/users/{id}", get(handler1))
474 .route("/users/{id}/profile", get(handler2));
475
476 assert_eq!(router.registered_routes().len(), 2);
477 }
478
479 #[test]
480 fn test_route_info_tracking() {
481 async fn handler() -> &'static str {
482 "handler"
483 }
484
485 let router = Router::new().route("/users/{id}", get(handler));
486
487 let routes = router.registered_routes();
488 assert_eq!(routes.len(), 1);
489
490 let info = routes.get("/users/:id").unwrap();
491 assert_eq!(info.path, "/users/{id}");
492 assert_eq!(info.methods.len(), 1);
493 assert_eq!(info.methods[0], Method::GET);
494 }
495}
496
497#[cfg(test)]
498mod property_tests {
499 use super::*;
500 use proptest::prelude::*;
501 use std::panic::{catch_unwind, AssertUnwindSafe};
502
503 proptest! {
511 #![proptest_config(ProptestConfig::with_cases(100))]
512
513 #[test]
518 fn prop_same_structure_different_param_names_conflict(
519 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
521 param1 in "[a-z][a-z0-9]{0,5}",
523 param2 in "[a-z][a-z0-9]{0,5}",
524 ) {
525 prop_assume!(param1 != param2);
527
528 let mut path1 = String::from("/");
530 let mut path2 = String::from("/");
531
532 for segment in &segments {
533 path1.push_str(segment);
534 path1.push('/');
535 path2.push_str(segment);
536 path2.push('/');
537 }
538
539 path1.push('{');
540 path1.push_str(¶m1);
541 path1.push('}');
542
543 path2.push('{');
544 path2.push_str(¶m2);
545 path2.push('}');
546
547 let result = catch_unwind(AssertUnwindSafe(|| {
549 async fn handler1() -> &'static str { "handler1" }
550 async fn handler2() -> &'static str { "handler2" }
551
552 let _router = Router::new()
553 .route(&path1, get(handler1))
554 .route(&path2, get(handler2));
555 }));
556
557 prop_assert!(
558 result.is_err(),
559 "Routes '{}' and '{}' should conflict but didn't",
560 path1, path2
561 );
562 }
563
564 #[test]
569 fn prop_different_structures_no_conflict(
570 segments1 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
572 segments2 in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..3),
573 has_param1 in any::<bool>(),
575 has_param2 in any::<bool>(),
576 ) {
577 let mut path1 = String::from("/");
579 let mut path2 = String::from("/");
580
581 for segment in &segments1 {
582 path1.push_str(segment);
583 path1.push('/');
584 }
585 path1.pop(); for segment in &segments2 {
588 path2.push_str(segment);
589 path2.push('/');
590 }
591 path2.pop(); if has_param1 {
594 path1.push_str("/{id}");
595 }
596
597 if has_param2 {
598 path2.push_str("/{id}");
599 }
600
601 let norm1 = normalize_path_for_comparison(&convert_path_params(&path1));
603 let norm2 = normalize_path_for_comparison(&convert_path_params(&path2));
604
605 prop_assume!(norm1 != norm2);
607
608 let result = catch_unwind(AssertUnwindSafe(|| {
610 async fn handler1() -> &'static str { "handler1" }
611 async fn handler2() -> &'static str { "handler2" }
612
613 let router = Router::new()
614 .route(&path1, get(handler1))
615 .route(&path2, get(handler2));
616
617 router.registered_routes().len()
618 }));
619
620 prop_assert!(
621 result.is_ok(),
622 "Routes '{}' and '{}' should not conflict but did",
623 path1, path2
624 );
625
626 if let Ok(count) = result {
627 prop_assert_eq!(count, 2, "Should have registered 2 routes");
628 }
629 }
630
631 #[test]
636 fn prop_conflict_error_contains_both_paths(
637 segment in "[a-z][a-z0-9]{1,5}",
639 param1 in "[a-z][a-z0-9]{1,5}",
640 param2 in "[a-z][a-z0-9]{1,5}",
641 ) {
642 prop_assume!(param1 != param2);
643
644 let path1 = format!("/{}/{{{}}}", segment, param1);
645 let path2 = format!("/{}/{{{}}}", segment, param2);
646
647 let result = catch_unwind(AssertUnwindSafe(|| {
648 async fn handler1() -> &'static str { "handler1" }
649 async fn handler2() -> &'static str { "handler2" }
650
651 let _router = Router::new()
652 .route(&path1, get(handler1))
653 .route(&path2, get(handler2));
654 }));
655
656 prop_assert!(result.is_err(), "Should have panicked due to conflict");
657
658 if let Err(panic_info) = result {
660 if let Some(msg) = panic_info.downcast_ref::<String>() {
661 prop_assert!(
662 msg.contains("ROUTE CONFLICT DETECTED"),
663 "Error should contain 'ROUTE CONFLICT DETECTED', got: {}",
664 msg
665 );
666 prop_assert!(
667 msg.contains("Existing:") && msg.contains("New:"),
668 "Error should contain both 'Existing:' and 'New:' labels, got: {}",
669 msg
670 );
671 prop_assert!(
672 msg.contains("How to resolve:"),
673 "Error should contain resolution guidance, got: {}",
674 msg
675 );
676 }
677 }
678 }
679
680 #[test]
684 fn prop_exact_duplicate_paths_conflict(
685 segments in prop::collection::vec("[a-z][a-z0-9]{0,5}", 1..4),
687 has_param in any::<bool>(),
688 ) {
689 let mut path = String::from("/");
691
692 for segment in &segments {
693 path.push_str(segment);
694 path.push('/');
695 }
696 path.pop(); if has_param {
699 path.push_str("/{id}");
700 }
701
702 let result = catch_unwind(AssertUnwindSafe(|| {
704 async fn handler1() -> &'static str { "handler1" }
705 async fn handler2() -> &'static str { "handler2" }
706
707 let _router = Router::new()
708 .route(&path, get(handler1))
709 .route(&path, get(handler2));
710 }));
711
712 prop_assert!(
713 result.is_err(),
714 "Registering path '{}' twice should conflict but didn't",
715 path
716 );
717 }
718 }
719}