1use super::pattern::{CompiledRoute, RouteId, RouteMatch, RoutePattern, RoutePatternError};
7use super::HttpMethod;
8use std::collections::HashMap;
9use thiserror::Error;
10
11#[derive(Error, Debug)]
13pub enum RouteMatchError {
14 #[error("No matching route found")]
15 NoMatch,
16 #[error("Route pattern error: {0}")]
17 PatternError(#[from] RoutePatternError),
18 #[error("Conflicting routes: {0} conflicts with {1}")]
19 RouteConflict(String, String),
20}
21
22#[derive(Debug, Clone)]
24pub struct RouteDefinition {
25 pub id: RouteId,
26 pub method: HttpMethod,
27 pub path: String,
28}
29
30#[derive(Debug)]
32pub struct RouteMatcher {
33 static_routes: HashMap<HttpMethod, HashMap<String, RouteId>>,
36 dynamic_routes: Vec<CompiledRoute>,
38 route_definitions: HashMap<RouteId, RouteDefinition>,
40}
41
42impl RouteMatcher {
43 pub fn new() -> Self {
45 Self {
46 static_routes: HashMap::new(),
47 dynamic_routes: Vec::new(),
48 route_definitions: HashMap::new(),
49 }
50 }
51
52 pub fn add_route(&mut self, definition: RouteDefinition) -> Result<(), RouteMatchError> {
54 let pattern = RoutePattern::parse(&definition.path)?;
55
56 self.check_conflicts(&definition, &pattern)?;
58
59 self.route_definitions
61 .insert(definition.id.clone(), definition.clone());
62
63 if pattern.is_static() {
64 self.static_routes
66 .entry(definition.method.clone())
67 .or_default()
68 .insert(definition.path.clone(), definition.id);
69 } else {
70 let compiled_route = CompiledRoute::new(definition.id, definition.method, pattern);
72
73 let insert_pos = self
75 .dynamic_routes
76 .binary_search_by_key(&compiled_route.priority, |r| r.priority)
77 .unwrap_or_else(|pos| pos);
78
79 self.dynamic_routes.insert(insert_pos, compiled_route);
80 }
81
82 Ok(())
83 }
84
85 pub fn resolve(&self, method: &HttpMethod, path: &str) -> Option<RouteMatch> {
87 if let Some(method_routes) = self.static_routes.get(method) {
89 if let Some(route_id) = method_routes.get(path) {
90 return Some(RouteMatch {
91 route_id: route_id.clone(),
92 params: HashMap::new(),
93 });
94 }
95 }
96
97 for compiled_route in &self.dynamic_routes {
99 if compiled_route.matches(method, path) {
100 let params = compiled_route.extract_params(path);
101 return Some(RouteMatch {
102 route_id: compiled_route.id.clone(),
103 params,
104 });
105 }
106 }
107
108 None
109 }
110
111 fn check_conflicts(
113 &self,
114 new_route: &RouteDefinition,
115 new_pattern: &RoutePattern,
116 ) -> Result<(), RouteMatchError> {
117 if new_pattern.is_static() {
119 if let Some(method_routes) = self.static_routes.get(&new_route.method) {
120 if let Some(existing_id) = method_routes.get(&new_route.path) {
121 return Err(RouteMatchError::RouteConflict(
122 new_route.id.clone(),
123 existing_id.clone(),
124 ));
125 }
126 }
127 }
128
129 for existing_route in &self.dynamic_routes {
131 if existing_route.method == new_route.method
132 && self.patterns_conflict(new_pattern, &existing_route.pattern)
133 {
134 return Err(RouteMatchError::RouteConflict(
135 new_route.id.clone(),
136 existing_route.id.clone(),
137 ));
138 }
139 }
140
141 Ok(())
142 }
143
144 fn patterns_conflict(&self, pattern1: &RoutePattern, pattern2: &RoutePattern) -> bool {
146 if pattern1.segments.len() != pattern2.segments.len() {
150 return false;
151 }
152
153 for (seg1, seg2) in pattern1.segments.iter().zip(pattern2.segments.iter()) {
154 match (seg1, seg2) {
155 (
156 super::pattern::PathSegment::Static(s1),
157 super::pattern::PathSegment::Static(s2),
158 ) if s1 == s2 => continue,
159 (
160 super::pattern::PathSegment::Parameter { constraint: c1, .. },
161 super::pattern::PathSegment::Parameter { constraint: c2, .. },
162 ) if c1 == c2 => continue,
163 (
164 super::pattern::PathSegment::CatchAll { .. },
165 super::pattern::PathSegment::CatchAll { .. },
166 ) => continue,
167 _ => return false, }
169 }
170
171 true }
173
174 pub fn all_routes(&self) -> &HashMap<RouteId, RouteDefinition> {
176 &self.route_definitions
177 }
178
179 pub fn get_route(&self, route_id: &RouteId) -> Option<&RouteDefinition> {
181 self.route_definitions.get(route_id)
182 }
183
184 pub fn stats(&self) -> MatcherStats {
186 let static_routes_count = self
187 .static_routes
188 .values()
189 .map(|method_routes| method_routes.len())
190 .sum();
191
192 MatcherStats {
193 static_routes: static_routes_count,
194 dynamic_routes: self.dynamic_routes.len(),
195 total_routes: self.route_definitions.len(),
196 }
197 }
198
199 pub fn clear(&mut self) {
201 self.static_routes.clear();
202 self.dynamic_routes.clear();
203 self.route_definitions.clear();
204 }
205}
206
207impl Default for RouteMatcher {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213#[derive(Debug, Clone)]
215pub struct MatcherStats {
216 pub static_routes: usize,
217 pub dynamic_routes: usize,
218 pub total_routes: usize,
219}
220
221pub struct RouteMatcherBuilder {
223 routes: Vec<RouteDefinition>,
224}
225
226impl RouteMatcherBuilder {
227 pub fn new() -> Self {
229 Self { routes: Vec::new() }
230 }
231
232 pub fn route(mut self, id: String, method: HttpMethod, path: String) -> Self {
234 self.routes.push(RouteDefinition { id, method, path });
235 self
236 }
237
238 pub fn get(self, id: String, path: String) -> Self {
240 self.route(id, HttpMethod::GET, path)
241 }
242
243 pub fn post(self, id: String, path: String) -> Self {
245 self.route(id, HttpMethod::POST, path)
246 }
247
248 pub fn put(self, id: String, path: String) -> Self {
250 self.route(id, HttpMethod::PUT, path)
251 }
252
253 pub fn delete(self, id: String, path: String) -> Self {
255 self.route(id, HttpMethod::DELETE, path)
256 }
257
258 pub fn patch(self, id: String, path: String) -> Self {
260 self.route(id, HttpMethod::PATCH, path)
261 }
262
263 pub fn build(self) -> Result<RouteMatcher, RouteMatchError> {
265 let mut matcher = RouteMatcher::new();
266
267 for route_def in self.routes {
268 matcher.add_route(route_def)?;
269 }
270
271 Ok(matcher)
272 }
273}
274
275impl Default for RouteMatcherBuilder {
276 fn default() -> Self {
277 Self::new()
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn test_static_route_matching() {
287 let mut matcher = RouteMatcher::new();
288
289 let route_def = RouteDefinition {
290 id: "home".to_string(),
291 method: HttpMethod::GET,
292 path: "/".to_string(),
293 };
294
295 matcher.add_route(route_def).unwrap();
296
297 let result = matcher.resolve(&HttpMethod::GET, "/");
298 assert!(result.is_some());
299
300 let route_match = result.unwrap();
301 assert_eq!(route_match.route_id, "home");
302 assert!(route_match.params.is_empty());
303
304 assert!(matcher.resolve(&HttpMethod::POST, "/").is_none());
306
307 assert!(matcher.resolve(&HttpMethod::GET, "/users").is_none());
309 }
310
311 #[test]
312 fn test_dynamic_route_matching() {
313 let mut matcher = RouteMatcher::new();
314
315 let route_def = RouteDefinition {
316 id: "user_show".to_string(),
317 method: HttpMethod::GET,
318 path: "/users/{id}".to_string(),
319 };
320
321 matcher.add_route(route_def).unwrap();
322
323 let result = matcher.resolve(&HttpMethod::GET, "/users/123");
324 assert!(result.is_some());
325
326 let route_match = result.unwrap();
327 assert_eq!(route_match.route_id, "user_show");
328 assert_eq!(route_match.params.get("id"), Some(&"123".to_string()));
329 }
330
331 #[test]
332 fn test_route_priority() {
333 let mut matcher = RouteMatcher::new();
334
335 matcher
337 .add_route(RouteDefinition {
338 id: "catch_all".to_string(),
339 method: HttpMethod::GET,
340 path: "/files/*path".to_string(),
341 })
342 .unwrap();
343
344 matcher
345 .add_route(RouteDefinition {
346 id: "specific".to_string(),
347 method: HttpMethod::GET,
348 path: "/files/config.json".to_string(),
349 })
350 .unwrap();
351
352 matcher
353 .add_route(RouteDefinition {
354 id: "param".to_string(),
355 method: HttpMethod::GET,
356 path: "/files/{name}".to_string(),
357 })
358 .unwrap();
359
360 let result = matcher.resolve(&HttpMethod::GET, "/files/config.json");
362 assert_eq!(result.unwrap().route_id, "specific");
363
364 let result = matcher.resolve(&HttpMethod::GET, "/files/other.txt");
366 assert_eq!(result.unwrap().route_id, "param");
367
368 let result = matcher.resolve(&HttpMethod::GET, "/files/docs/readme.md");
370 assert_eq!(result.unwrap().route_id, "catch_all");
371 }
372
373 #[test]
374 fn test_route_conflict_detection() {
375 let mut matcher = RouteMatcher::new();
376
377 matcher
379 .add_route(RouteDefinition {
380 id: "route1".to_string(),
381 method: HttpMethod::GET,
382 path: "/users".to_string(),
383 })
384 .unwrap();
385
386 let result = matcher.add_route(RouteDefinition {
388 id: "route2".to_string(),
389 method: HttpMethod::GET,
390 path: "/users".to_string(),
391 });
392
393 assert!(result.is_err());
394 assert!(matches!(result, Err(RouteMatchError::RouteConflict(_, _))));
395 }
396
397 #[test]
398 fn test_advanced_conflict_detection() {
399 let mut matcher = RouteMatcher::new();
400
401 matcher
403 .add_route(RouteDefinition {
404 id: "users_by_id".to_string(),
405 method: HttpMethod::GET,
406 path: "/users/{id}".to_string(),
407 })
408 .unwrap();
409
410 let result = matcher.add_route(RouteDefinition {
411 id: "users_by_name".to_string(),
412 method: HttpMethod::GET,
413 path: "/users/{name}".to_string(),
414 });
415 assert!(
416 result.is_err(),
417 "Parameters with different names should conflict"
418 );
419
420 let result = matcher.add_route(RouteDefinition {
422 id: "users_post".to_string(),
423 method: HttpMethod::POST,
424 path: "/users/{id}".to_string(),
425 });
426 assert!(result.is_ok(), "Different methods should not conflict");
427
428 let result = matcher.add_route(RouteDefinition {
430 id: "posts_by_id".to_string(),
431 method: HttpMethod::GET,
432 path: "/posts/{id}".to_string(),
433 });
434 assert!(
435 result.is_ok(),
436 "Different static segments should not conflict"
437 );
438
439 matcher
441 .add_route(RouteDefinition {
442 id: "files_serve".to_string(),
443 method: HttpMethod::GET,
444 path: "/files/*path".to_string(),
445 })
446 .unwrap();
447
448 let result = matcher.add_route(RouteDefinition {
449 id: "files_download".to_string(),
450 method: HttpMethod::GET,
451 path: "/files/*file_path".to_string(),
452 });
453 assert!(
454 result.is_err(),
455 "Catch-all routes with same structure should conflict"
456 );
457
458 let result = matcher.add_route(RouteDefinition {
460 id: "admin_static".to_string(),
461 method: HttpMethod::GET,
462 path: "/admin/dashboard".to_string(),
463 });
464 assert!(
465 result.is_ok(),
466 "Static vs parameter segments should not conflict"
467 );
468 }
469
470 #[test]
471 fn test_constraint_based_conflicts() {
472 let mut matcher = RouteMatcher::new();
473
474 matcher
476 .add_route(RouteDefinition {
477 id: "user_by_int_id".to_string(),
478 method: HttpMethod::GET,
479 path: "/users/{id:int}".to_string(),
480 })
481 .unwrap();
482
483 let result = matcher.add_route(RouteDefinition {
484 id: "user_by_int_uid".to_string(),
485 method: HttpMethod::GET,
486 path: "/users/{uid:int}".to_string(),
487 });
488 assert!(result.is_err(), "Same constraints should conflict");
489
490 let result = matcher.add_route(RouteDefinition {
492 id: "user_by_uuid".to_string(),
493 method: HttpMethod::GET,
494 path: "/users/{id:uuid}".to_string(),
495 });
496 assert!(result.is_ok(), "Different constraints should not conflict");
497
498 let result = matcher.add_route(RouteDefinition {
500 id: "user_by_string".to_string(),
501 method: HttpMethod::GET,
502 path: "/users/{name}".to_string(),
503 });
504 assert!(
505 result.is_ok(),
506 "Constrained vs unconstrained should not conflict"
507 );
508 }
509
510 #[test]
511 fn test_complex_pattern_conflicts() {
512 let mut matcher = RouteMatcher::new();
513
514 matcher
516 .add_route(RouteDefinition {
517 id: "api_user_posts".to_string(),
518 method: HttpMethod::GET,
519 path: "/api/v1/users/{user_id}/posts/{post_id}".to_string(),
520 })
521 .unwrap();
522
523 let result = matcher.add_route(RouteDefinition {
525 id: "api_member_articles".to_string(),
526 method: HttpMethod::GET,
527 path: "/api/v1/users/{member_id}/posts/{article_id}".to_string(),
528 });
529 assert!(
530 result.is_err(),
531 "Structurally identical complex patterns should conflict"
532 );
533
534 let result = matcher.add_route(RouteDefinition {
536 id: "api_user_comments".to_string(),
537 method: HttpMethod::GET,
538 path: "/api/v1/users/{user_id}/comments/{comment_id}".to_string(),
539 });
540 assert!(
541 result.is_ok(),
542 "Different static segments should not conflict"
543 );
544
545 let result = matcher.add_route(RouteDefinition {
547 id: "api_user_profile".to_string(),
548 method: HttpMethod::GET,
549 path: "/api/v1/users/{user_id}/profile".to_string(),
550 });
551 assert!(
552 result.is_ok(),
553 "Different segment count should not conflict"
554 );
555 }
556
557 #[test]
558 fn test_matcher_builder() {
559 let matcher = RouteMatcherBuilder::new()
560 .get("home".to_string(), "/".to_string())
561 .post("users_create".to_string(), "/users".to_string())
562 .get("users_show".to_string(), "/users/{id}".to_string())
563 .build()
564 .unwrap();
565
566 let stats = matcher.stats();
567 assert_eq!(stats.total_routes, 3);
568 assert_eq!(stats.static_routes, 2); assert_eq!(stats.dynamic_routes, 1); assert!(matcher.resolve(&HttpMethod::GET, "/").is_some());
573 assert!(matcher.resolve(&HttpMethod::POST, "/users").is_some());
574 assert!(matcher.resolve(&HttpMethod::GET, "/users/123").is_some());
575 }
576
577 #[test]
578 fn test_constraint_validation_in_matching() {
579 let mut matcher = RouteMatcher::new();
580
581 matcher
582 .add_route(RouteDefinition {
583 id: "user_by_id".to_string(),
584 method: HttpMethod::GET,
585 path: "/users/{id:int}".to_string(),
586 })
587 .unwrap();
588
589 let result = matcher.resolve(&HttpMethod::GET, "/users/123");
591 assert!(result.is_some());
592 assert_eq!(result.unwrap().route_id, "user_by_id");
593
594 let result = matcher.resolve(&HttpMethod::GET, "/users/abc");
596 assert!(result.is_none());
597 }
598
599 #[test]
600 fn test_mixed_static_and_dynamic_routes() {
601 let mut matcher = RouteMatcher::new();
602
603 matcher
605 .add_route(RouteDefinition {
606 id: "api_status".to_string(),
607 method: HttpMethod::GET,
608 path: "/api/status".to_string(),
609 })
610 .unwrap();
611
612 matcher
613 .add_route(RouteDefinition {
614 id: "api_user".to_string(),
615 method: HttpMethod::GET,
616 path: "/api/users/{id}".to_string(),
617 })
618 .unwrap();
619
620 matcher
621 .add_route(RouteDefinition {
622 id: "root".to_string(),
623 method: HttpMethod::GET,
624 path: "/".to_string(),
625 })
626 .unwrap();
627
628 let result = matcher.resolve(&HttpMethod::GET, "/api/status");
630 assert_eq!(result.unwrap().route_id, "api_status");
631
632 let result = matcher.resolve(&HttpMethod::GET, "/");
634 assert_eq!(result.unwrap().route_id, "root");
635
636 let result = matcher.resolve(&HttpMethod::GET, "/api/users/456");
638 let route_match = result.unwrap();
639 assert_eq!(route_match.route_id, "api_user");
640 assert_eq!(route_match.params.get("id"), Some(&"456".to_string()));
641 }
642
643 #[test]
644 fn test_no_match() {
645 let matcher = RouteMatcherBuilder::new()
646 .get("home".to_string(), "/".to_string())
647 .build()
648 .unwrap();
649
650 assert!(matcher.resolve(&HttpMethod::GET, "/nonexistent").is_none());
651 assert!(matcher.resolve(&HttpMethod::POST, "/").is_none());
652 }
653
654 #[test]
655 fn test_static_route_lookup_performance() {
656 let mut builder = RouteMatcherBuilder::new();
658
659 for i in 0..100 {
660 builder = builder
661 .get(format!("get_{}", i), format!("/static/path/{}", i))
662 .post(format!("post_{}", i), format!("/api/v1/{}", i))
663 .put(format!("put_{}", i), format!("/resource/{}", i));
664 }
665
666 let matcher = builder.build().unwrap();
667 let stats = matcher.stats();
668
669 assert_eq!(stats.static_routes, 300); assert_eq!(stats.dynamic_routes, 0);
672
673 let start = std::time::Instant::now();
675
676 for i in 0..1000 {
678 let test_index = i % 100;
679
680 let result = matcher.resolve(&HttpMethod::GET, &format!("/static/path/{}", test_index));
682 assert!(result.is_some());
683
684 let result = matcher.resolve(&HttpMethod::POST, &format!("/api/v1/{}", test_index));
685 assert!(result.is_some());
686
687 let result = matcher.resolve(&HttpMethod::PUT, &format!("/resource/{}", test_index));
688 assert!(result.is_some());
689
690 let result = matcher.resolve(&HttpMethod::GET, "/nonexistent/path");
692 assert!(result.is_none());
693 }
694
695 let elapsed = start.elapsed();
696
697 assert!(
703 elapsed.as_millis() < 100,
704 "Static route lookups took too long: {}ms",
705 elapsed.as_millis()
706 );
707
708 println!(
709 "3000 static route lookups completed in {}μs",
710 elapsed.as_micros()
711 );
712 }
713}