1use super::extraction::ParameterExtractor;
7use super::matcher::{RouteDefinition, RouteMatchError, RouteMatcher};
8use super::pattern::{RoutePattern, RoutePatternError};
9use super::{HttpMethod, RouteInfo};
10use std::collections::{HashMap, HashSet};
11use thiserror::Error;
12
13#[derive(Error, Debug)]
15pub enum CompilationError {
16 #[error("Route pattern error: {0}")]
17 PatternError(#[from] RoutePatternError),
18 #[error("Route matching error: {0}")]
19 MatcherError(#[from] RouteMatchError),
20 #[error("Duplicate route ID: {0}")]
21 DuplicateRouteId(String),
22 #[error("Route conflict detected: {0} conflicts with {1}")]
23 RouteConflict(String, String),
24 #[error("Invalid route configuration: {0}")]
25 InvalidConfiguration(String),
26 #[error("Compilation failed: {0}")]
27 CompilationFailed(String),
28}
29
30#[derive(Debug, Clone)]
32pub struct CompilerConfig {
33 pub detect_conflicts: bool,
35 pub enable_optimization: bool,
37 pub max_routes_warning: usize,
39 pub performance_analysis: bool,
41}
42
43impl Default for CompilerConfig {
44 fn default() -> Self {
45 Self {
46 detect_conflicts: true,
47 enable_optimization: true,
48 max_routes_warning: 1000,
49 performance_analysis: true,
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
56pub struct CompilationStats {
57 pub total_routes: usize,
58 pub static_routes: usize,
59 pub dynamic_routes: usize,
60 pub parameter_routes: usize,
61 pub catch_all_routes: usize,
62 pub conflicts_detected: usize,
63 pub optimizations_applied: usize,
64 pub compilation_time_ms: u128,
65}
66
67#[derive(Debug, Clone)]
69pub struct CompilableRoute {
70 pub id: String,
71 pub method: HttpMethod,
72 pub path: String,
73 pub name: Option<String>,
74 pub metadata: HashMap<String, String>,
75}
76
77impl CompilableRoute {
78 pub fn new(id: String, method: HttpMethod, path: String) -> Self {
79 Self {
80 id,
81 method,
82 path,
83 name: None,
84 metadata: HashMap::new(),
85 }
86 }
87
88 pub fn with_name(mut self, name: String) -> Self {
89 self.name = Some(name);
90 self
91 }
92
93 pub fn with_metadata(mut self, key: String, value: String) -> Self {
94 self.metadata.insert(key, value);
95 self
96 }
97}
98
99#[derive(Debug)]
101pub struct CompilationResult {
102 pub matcher: RouteMatcher,
103 pub extractors: HashMap<String, ParameterExtractor>,
104 pub route_registry: HashMap<String, RouteInfo>,
105 pub stats: CompilationStats,
106 pub warnings: Vec<String>,
107}
108
109#[derive(Debug)]
111pub struct RouteCompiler {
112 config: CompilerConfig,
113 routes: Vec<CompilableRoute>,
114 route_ids: HashSet<String>,
115}
116
117impl RouteCompiler {
118 pub fn new() -> Self {
120 Self::with_config(CompilerConfig::default())
121 }
122
123 pub fn with_config(config: CompilerConfig) -> Self {
125 Self {
126 config,
127 routes: Vec::new(),
128 route_ids: HashSet::new(),
129 }
130 }
131
132 pub fn add_route(&mut self, route: CompilableRoute) -> Result<(), CompilationError> {
134 if self.route_ids.contains(&route.id) {
136 return Err(CompilationError::DuplicateRouteId(route.id));
137 }
138
139 self.route_ids.insert(route.id.clone());
140 self.routes.push(route);
141 Ok(())
142 }
143
144 pub fn add_routes(&mut self, routes: Vec<CompilableRoute>) -> Result<(), CompilationError> {
146 for route in routes {
147 self.add_route(route)?;
148 }
149 Ok(())
150 }
151
152 pub fn compile(self) -> Result<CompilationResult, CompilationError> {
154 let start_time = std::time::Instant::now();
155 let mut warnings = Vec::new();
156 let mut optimizations_applied = 0;
157
158 let total_route_count = self.routes.len();
160
161 if total_route_count > self.config.max_routes_warning {
163 warnings.push(format!(
164 "Large number of routes detected: {}. Consider route grouping or optimization.",
165 total_route_count
166 ));
167 }
168
169 let mut parsed_routes = Vec::new();
171 let mut static_count = 0;
172 let mut dynamic_count = 0;
173 let mut parameter_count = 0;
174 let mut catch_all_count = 0;
175
176 for route in self.routes {
177 let pattern = RoutePattern::parse(&route.path)?;
178
179 if pattern.is_static() {
181 static_count += 1;
182 } else {
183 dynamic_count += 1;
184 if pattern.has_catch_all {
185 catch_all_count += 1;
186 } else if !pattern.param_names.is_empty() {
187 parameter_count += 1;
188 }
189 }
190
191 parsed_routes.push((route, pattern));
192 }
193
194 let mut matcher = RouteMatcher::new();
196 let mut extractors = HashMap::new();
197 let mut route_registry = HashMap::new();
198 let mut conflicts_detected = 0;
199
200 if self.config.enable_optimization {
202 parsed_routes = Self::optimize_routes(parsed_routes);
203 optimizations_applied += 1;
204 }
205
206 for (route, pattern) in parsed_routes {
208 let route_id = route.id;
210 let route_method = route.method;
211 let route_path = route.path;
212 let route_name = route.name;
213 let route_group = route.metadata.get("group").cloned();
214 let is_pattern_static = pattern.is_static();
215 let pattern_param_names = pattern.param_names.clone();
216
217 let route_def = RouteDefinition {
219 id: route_id.clone(),
220 method: route_method.clone(),
221 path: route_path.clone(),
222 };
223
224 match matcher.add_route(route_def) {
226 Ok(()) => {
227 if !is_pattern_static {
229 let extractor = ParameterExtractor::new(pattern);
230 extractors.insert(route_id.clone(), extractor);
231 }
232
233 let route_info = RouteInfo {
235 name: route_name,
236 path: route_path,
237 method: route_method,
238 params: pattern_param_names,
239 group: route_group,
240 };
241 route_registry.insert(route_id, route_info);
242 }
243 Err(RouteMatchError::RouteConflict(source, target)) => {
244 if self.config.detect_conflicts {
245 return Err(CompilationError::RouteConflict(source, target));
246 } else {
247 conflicts_detected += 1;
248 warnings.push(format!(
249 "Route conflict detected: {} conflicts with {}",
250 source, target
251 ));
252 }
253 }
254 Err(e) => return Err(CompilationError::MatcherError(e)),
255 }
256 }
257
258 let compilation_time = start_time.elapsed().as_millis();
259
260 if self.config.performance_analysis && compilation_time > 100 {
262 warnings.push(format!(
263 "Route compilation took {}ms. Consider optimizing route patterns or reducing route count.",
264 compilation_time
265 ));
266 }
267
268 let stats = CompilationStats {
269 total_routes: total_route_count,
270 static_routes: static_count,
271 dynamic_routes: dynamic_count,
272 parameter_routes: parameter_count,
273 catch_all_routes: catch_all_count,
274 conflicts_detected,
275 optimizations_applied,
276 compilation_time_ms: compilation_time,
277 };
278
279 Ok(CompilationResult {
280 matcher,
281 extractors,
282 route_registry,
283 stats,
284 warnings,
285 })
286 }
287
288 fn optimize_routes(
290 mut routes: Vec<(CompilableRoute, RoutePattern)>,
291 ) -> Vec<(CompilableRoute, RoutePattern)> {
292 routes.sort_by(|(_, pattern_a), (_, pattern_b)| {
295 let static_a = pattern_a.is_static();
297 let static_b = pattern_b.is_static();
298
299 match (static_a, static_b) {
300 (true, false) => std::cmp::Ordering::Less, (false, true) => std::cmp::Ordering::Greater, _ => {
303 pattern_a.priority().cmp(&pattern_b.priority())
305 }
306 }
307 });
308
309 routes
310 }
311}
312
313impl Default for RouteCompiler {
314 fn default() -> Self {
315 Self::new()
316 }
317}
318
319#[derive(Debug)]
321pub struct RouteCompilerBuilder {
322 config: CompilerConfig,
323 routes: Vec<CompilableRoute>,
324}
325
326impl RouteCompilerBuilder {
327 pub fn new() -> Self {
329 Self {
330 config: CompilerConfig::default(),
331 routes: Vec::new(),
332 }
333 }
334
335 pub fn config(mut self, config: CompilerConfig) -> Self {
337 self.config = config;
338 self
339 }
340
341 pub fn detect_conflicts(mut self, enabled: bool) -> Self {
343 self.config.detect_conflicts = enabled;
344 self
345 }
346
347 pub fn optimize(mut self, enabled: bool) -> Self {
349 self.config.enable_optimization = enabled;
350 self
351 }
352
353 pub fn max_routes_warning(mut self, max: usize) -> Self {
355 self.config.max_routes_warning = max;
356 self
357 }
358
359 pub fn route(mut self, route: CompilableRoute) -> Self {
361 self.routes.push(route);
362 self
363 }
364
365 pub fn get(mut self, id: String, path: String) -> Self {
367 self.routes
368 .push(CompilableRoute::new(id, HttpMethod::GET, path));
369 self
370 }
371
372 pub fn post(mut self, id: String, path: String) -> Self {
374 self.routes
375 .push(CompilableRoute::new(id, HttpMethod::POST, path));
376 self
377 }
378
379 pub fn put(mut self, id: String, path: String) -> Self {
381 self.routes
382 .push(CompilableRoute::new(id, HttpMethod::PUT, path));
383 self
384 }
385
386 pub fn delete(mut self, id: String, path: String) -> Self {
388 self.routes
389 .push(CompilableRoute::new(id, HttpMethod::DELETE, path));
390 self
391 }
392
393 pub fn patch(mut self, id: String, path: String) -> Self {
395 self.routes
396 .push(CompilableRoute::new(id, HttpMethod::PATCH, path));
397 self
398 }
399
400 pub fn build(self) -> Result<CompilationResult, CompilationError> {
402 let mut compiler = RouteCompiler::with_config(self.config);
403
404 for route in self.routes {
405 compiler.add_route(route)?;
406 }
407
408 compiler.compile()
409 }
410}
411
412impl Default for RouteCompilerBuilder {
413 fn default() -> Self {
414 Self::new()
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 #[test]
423 fn test_basic_compilation() {
424 let result = RouteCompilerBuilder::new()
425 .get("home".to_string(), "/".to_string())
426 .get("users_index".to_string(), "/users".to_string())
427 .get("users_show".to_string(), "/users/{id}".to_string())
428 .build()
429 .unwrap();
430
431 assert_eq!(result.stats.total_routes, 3);
432 assert_eq!(result.stats.static_routes, 2);
433 assert_eq!(result.stats.dynamic_routes, 1);
434 assert_eq!(result.stats.parameter_routes, 1);
435 }
436
437 #[test]
438 fn test_route_optimization() {
439 let result = RouteCompilerBuilder::new()
440 .optimize(true)
441 .get("catch_all".to_string(), "/files/*path".to_string())
442 .get("specific".to_string(), "/files/config.json".to_string())
443 .get("param".to_string(), "/files/{name}".to_string())
444 .build()
445 .unwrap();
446
447 assert_eq!(result.stats.optimizations_applied, 1);
448
449 let matcher = result.matcher;
451
452 let route_match = matcher
454 .resolve(&HttpMethod::GET, "/files/config.json")
455 .unwrap();
456 assert_eq!(route_match.route_id, "specific");
457
458 let route_match = matcher
460 .resolve(&HttpMethod::GET, "/files/readme.txt")
461 .unwrap();
462 assert_eq!(route_match.route_id, "param");
463
464 let route_match = matcher
466 .resolve(&HttpMethod::GET, "/files/docs/api.md")
467 .unwrap();
468 assert_eq!(route_match.route_id, "catch_all");
469 }
470
471 #[test]
472 fn test_constraint_based_priority_ordering() {
473 let result = RouteCompilerBuilder::new()
475 .optimize(true)
476 .get("catch_all".to_string(), "/users/*path".to_string())
478 .get("unconstrained".to_string(), "/users/{name}".to_string())
479 .get("alpha_slug".to_string(), "/users/{slug:alpha}".to_string())
480 .get("custom_regex".to_string(), "/users/{id:[0-9]+}".to_string())
481 .get("int_id".to_string(), "/users/{id:int}".to_string())
482 .get("uuid_id".to_string(), "/users/{id:uuid}".to_string())
483 .get("static_me".to_string(), "/users/me".to_string())
484 .build()
485 .unwrap();
486
487 let matcher = result.matcher;
488
489 let route_match = matcher.resolve(&HttpMethod::GET, "/users/me").unwrap();
493 assert_eq!(route_match.route_id, "static_me");
494
495 let route_match = matcher
497 .resolve(
498 &HttpMethod::GET,
499 "/users/550e8400-e29b-41d4-a716-446655440000",
500 )
501 .unwrap();
502 assert_eq!(route_match.route_id, "uuid_id");
503
504 let route_match = matcher.resolve(&HttpMethod::GET, "/users/123").unwrap();
506 assert_eq!(route_match.route_id, "int_id");
507
508 let route_match = matcher.resolve(&HttpMethod::GET, "/users/456").unwrap();
510 assert_eq!(route_match.route_id, "int_id");
512
513 let route_match = matcher
515 .resolve(&HttpMethod::GET, "/users/johnsmith")
516 .unwrap();
517 assert_eq!(route_match.route_id, "alpha_slug");
518
519 let route_match = matcher
521 .resolve(&HttpMethod::GET, "/users/user_with_underscores")
522 .unwrap();
523 assert_eq!(route_match.route_id, "unconstrained");
524
525 let route_match = matcher
527 .resolve(&HttpMethod::GET, "/users/path/to/resource")
528 .unwrap();
529 assert_eq!(route_match.route_id, "catch_all");
530 }
531
532 #[test]
533 fn test_conflict_detection() {
534 let result = RouteCompilerBuilder::new()
535 .detect_conflicts(true)
536 .get("route1".to_string(), "/users".to_string())
537 .get("route2".to_string(), "/users".to_string())
538 .build();
539
540 assert!(result.is_err());
541 assert!(matches!(
542 result.unwrap_err(),
543 CompilationError::RouteConflict(_, _)
544 ));
545 }
546
547 #[test]
548 fn test_conflict_warnings() {
549 let result = RouteCompilerBuilder::new()
550 .detect_conflicts(false) .get("route1".to_string(), "/users".to_string())
552 .get("route2".to_string(), "/users".to_string())
553 .build()
554 .unwrap();
555
556 assert!(!result.warnings.is_empty());
557 assert!(result.warnings[0].contains("conflict"));
558 assert_eq!(result.stats.conflicts_detected, 1);
559 }
560
561 #[test]
562 fn test_parameter_extractors() {
563 let result = RouteCompilerBuilder::new()
564 .get("users_show".to_string(), "/users/{id:int}".to_string())
565 .get(
566 "posts_show".to_string(),
567 "/posts/{slug}/comments/{id:uuid}".to_string(),
568 )
569 .build()
570 .unwrap();
571
572 assert!(result.extractors.contains_key("users_show"));
574 assert!(result.extractors.contains_key("posts_show"));
575 assert_eq!(result.extractors.len(), 2);
576
577 let users_extractor = result.extractors.get("users_show").unwrap();
579 let extracted = users_extractor.extract("/users/123").unwrap();
580 assert_eq!(extracted.get_int("id").unwrap(), 123);
581 }
582
583 #[test]
584 fn test_route_registry() {
585 let result = RouteCompilerBuilder::new()
586 .route(
587 CompilableRoute::new(
588 "users_show".to_string(),
589 HttpMethod::GET,
590 "/users/{id}".to_string(),
591 )
592 .with_name("users.show".to_string())
593 .with_metadata("group".to_string(), "users".to_string()),
594 )
595 .build()
596 .unwrap();
597
598 let route_info = result.route_registry.get("users_show").unwrap();
599 assert_eq!(route_info.name, Some("users.show".to_string()));
600 assert_eq!(route_info.group, Some("users".to_string()));
601 assert_eq!(route_info.params, vec!["id"]);
602 }
603
604 #[test]
605 fn test_compilation_stats() {
606 let result = RouteCompilerBuilder::new()
607 .get("static1".to_string(), "/".to_string())
608 .get("static2".to_string(), "/about".to_string())
609 .get("param1".to_string(), "/users/{id}".to_string())
610 .get("param2".to_string(), "/posts/{slug}".to_string())
611 .get("catch_all".to_string(), "/files/*path".to_string())
612 .build()
613 .unwrap();
614
615 let stats = result.stats;
616 assert_eq!(stats.total_routes, 5);
617 assert_eq!(stats.static_routes, 2);
618 assert_eq!(stats.dynamic_routes, 3);
619 assert_eq!(stats.parameter_routes, 2);
620 assert_eq!(stats.catch_all_routes, 1);
621 }
622
623 #[test]
624 fn test_duplicate_route_id() {
625 let mut compiler = RouteCompiler::new();
626
627 let route1 = CompilableRoute::new(
628 "duplicate".to_string(),
629 HttpMethod::GET,
630 "/path1".to_string(),
631 );
632 let route2 = CompilableRoute::new(
633 "duplicate".to_string(),
634 HttpMethod::POST,
635 "/path2".to_string(),
636 );
637
638 compiler.add_route(route1).unwrap();
639 let result = compiler.add_route(route2);
640
641 assert!(result.is_err());
642 assert!(matches!(
643 result.unwrap_err(),
644 CompilationError::DuplicateRouteId(_)
645 ));
646 }
647
648 #[test]
649 fn test_performance_warnings() {
650 let mut builder = RouteCompilerBuilder::new().max_routes_warning(5);
652
653 for i in 0..10 {
654 builder = builder.get(format!("route_{}", i), format!("/route_{}", i));
655 }
656
657 let result = builder.build().unwrap();
658 assert!(!result.warnings.is_empty());
659 assert!(result
660 .warnings
661 .iter()
662 .any(|w| w.contains("Large number of routes")));
663 }
664
665 #[test]
666 fn test_move_semantics_performance() {
667 let start = std::time::Instant::now();
669
670 let mut builder = RouteCompilerBuilder::new().optimize(true);
671
672 for i in 0..100 {
674 let mut route = CompilableRoute::new(
675 format!("route_{}", i),
676 HttpMethod::GET,
677 format!("/api/v1/resources/{}/items", i),
678 );
679
680 route = route.with_metadata("group".to_string(), format!("group_{}", i));
682 route = route.with_metadata(
683 "description".to_string(),
684 format!("Route for resource {}", i),
685 );
686 route = route.with_metadata("version".to_string(), "v1".to_string());
687
688 builder = builder.route(route);
689 }
690
691 let result = builder.build().unwrap();
692 let compilation_time = start.elapsed();
693
694 assert_eq!(result.stats.total_routes, 100);
696 assert!(result.stats.optimizations_applied > 0);
697
698 assert!(
700 compilation_time.as_millis() < 100,
701 "Compilation took too long: {}ms",
702 compilation_time.as_millis()
703 );
704
705 println!(
706 "100 complex routes compiled in {}ms using move semantics",
707 compilation_time.as_millis()
708 );
709 }
710}