elif_http/routing/
compiler.rs

1//! Route compilation and optimization for elif.rs
2//!
3//! This module provides compilation and optimization of route definitions
4//! into efficient runtime structures for high-performance route matching.
5
6use 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/// Errors that can occur during route compilation
14#[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/// Configuration for route compilation
31#[derive(Debug, Clone)]
32pub struct CompilerConfig {
33    /// Enable conflict detection
34    pub detect_conflicts: bool,
35    /// Enable route optimization
36    pub enable_optimization: bool,
37    /// Maximum number of routes before warning
38    pub max_routes_warning: usize,
39    /// Enable performance analysis
40    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/// Statistics about compiled routes
55#[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/// A single route definition for compilation
68#[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/// Result of route compilation
100#[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/// Route compiler with optimization and validation
110#[derive(Debug)]
111pub struct RouteCompiler {
112    config: CompilerConfig,
113    routes: Vec<CompilableRoute>,
114    route_ids: HashSet<String>,
115}
116
117impl RouteCompiler {
118    /// Create a new route compiler
119    pub fn new() -> Self {
120        Self::with_config(CompilerConfig::default())
121    }
122
123    /// Create a new route compiler with custom configuration
124    pub fn with_config(config: CompilerConfig) -> Self {
125        Self {
126            config,
127            routes: Vec::new(),
128            route_ids: HashSet::new(),
129        }
130    }
131
132    /// Add a route to be compiled
133    pub fn add_route(&mut self, route: CompilableRoute) -> Result<(), CompilationError> {
134        // Check for duplicate route IDs
135        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    /// Add multiple routes
145    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    /// Compile all routes into optimized structures
153    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        // Capture total route count before moving
159        let total_route_count = self.routes.len();
160
161        // Check route count warning
162        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        // Parse and validate all route patterns
170        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            // Collect statistics
180            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        // Create route matcher
195        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        // Apply optimizations if enabled
201        if self.config.enable_optimization {
202            parsed_routes = Self::optimize_routes(parsed_routes);
203            optimizations_applied += 1;
204        }
205
206        // Add routes to matcher and create extractors
207        for (route, pattern) in parsed_routes {
208            // Extract data we need to move/clone before consuming route
209            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            // Create route definition
218            let route_def = RouteDefinition {
219                id: route_id.clone(),
220                method: route_method.clone(),
221                path: route_path.clone(),
222            };
223
224            // Try to add route, handle conflicts
225            match matcher.add_route(route_def) {
226                Ok(()) => {
227                    // Create parameter extractor for dynamic routes
228                    if !is_pattern_static {
229                        let extractor = ParameterExtractor::new(pattern);
230                        extractors.insert(route_id.clone(), extractor);
231                    }
232
233                    // Create route info for registry
234                    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        // Performance analysis
261        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    /// Optimize route ordering for better performance
289    fn optimize_routes(
290        mut routes: Vec<(CompilableRoute, RoutePattern)>,
291    ) -> Vec<(CompilableRoute, RoutePattern)> {
292        // Sort routes by specificity (more specific routes first)
293        // This improves matching performance for common cases
294        routes.sort_by(|(_, pattern_a), (_, pattern_b)| {
295            // Primary sort: static routes first
296            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, // Static before dynamic
301                (false, true) => std::cmp::Ordering::Greater, // Dynamic after static
302                _ => {
303                    // Secondary sort: by priority (lower = more specific)
304                    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/// Builder for creating route compilers with fluent API
320#[derive(Debug)]
321pub struct RouteCompilerBuilder {
322    config: CompilerConfig,
323    routes: Vec<CompilableRoute>,
324}
325
326impl RouteCompilerBuilder {
327    /// Create a new builder
328    pub fn new() -> Self {
329        Self {
330            config: CompilerConfig::default(),
331            routes: Vec::new(),
332        }
333    }
334
335    /// Set compiler configuration
336    pub fn config(mut self, config: CompilerConfig) -> Self {
337        self.config = config;
338        self
339    }
340
341    /// Enable or disable conflict detection
342    pub fn detect_conflicts(mut self, enabled: bool) -> Self {
343        self.config.detect_conflicts = enabled;
344        self
345    }
346
347    /// Enable or disable route optimization
348    pub fn optimize(mut self, enabled: bool) -> Self {
349        self.config.enable_optimization = enabled;
350        self
351    }
352
353    /// Set maximum routes warning threshold
354    pub fn max_routes_warning(mut self, max: usize) -> Self {
355        self.config.max_routes_warning = max;
356        self
357    }
358
359    /// Add a route
360    pub fn route(mut self, route: CompilableRoute) -> Self {
361        self.routes.push(route);
362        self
363    }
364
365    /// Add a GET route
366    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    /// Add a POST route
373    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    /// Add a PUT route
380    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    /// Add a DELETE route
387    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    /// Add a PATCH route
394    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    /// Build and compile the routes
401    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        // Test that matching works correctly with optimized order
450        let matcher = result.matcher;
451
452        // Static route should match first
453        let route_match = matcher
454            .resolve(&HttpMethod::GET, "/files/config.json")
455            .unwrap();
456        assert_eq!(route_match.route_id, "specific");
457
458        // Parameter route should match next
459        let route_match = matcher
460            .resolve(&HttpMethod::GET, "/files/readme.txt")
461            .unwrap();
462        assert_eq!(route_match.route_id, "param");
463
464        // Catch-all should match complex paths
465        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        // Test that routes are properly ordered by constraint specificity
474        let result = RouteCompilerBuilder::new()
475            .optimize(true)
476            // Add routes in reverse priority order to test sorting
477            .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        // Test that most specific routes match first
490
491        // Static route (highest priority)
492        let route_match = matcher.resolve(&HttpMethod::GET, "/users/me").unwrap();
493        assert_eq!(route_match.route_id, "static_me");
494
495        // UUID constraint (specific)
496        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        // Integer constraint (specific)
505        let route_match = matcher.resolve(&HttpMethod::GET, "/users/123").unwrap();
506        assert_eq!(route_match.route_id, "int_id");
507
508        // Custom regex constraint (medium-high priority)
509        let route_match = matcher.resolve(&HttpMethod::GET, "/users/456").unwrap();
510        // Note: This should match int_id since it has higher priority than custom regex
511        assert_eq!(route_match.route_id, "int_id");
512
513        // Alpha constraint (general)
514        let route_match = matcher
515            .resolve(&HttpMethod::GET, "/users/johnsmith")
516            .unwrap();
517        assert_eq!(route_match.route_id, "alpha_slug");
518
519        // Unconstrained parameter (contains characters that don't match any specific constraint)
520        let route_match = matcher
521            .resolve(&HttpMethod::GET, "/users/user_with_underscores")
522            .unwrap();
523        assert_eq!(route_match.route_id, "unconstrained");
524
525        // Catch-all (lowest priority)
526        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) // Disable conflict errors, enable warnings
551            .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        // Should create extractors for dynamic routes
573        assert!(result.extractors.contains_key("users_show"));
574        assert!(result.extractors.contains_key("posts_show"));
575        assert_eq!(result.extractors.len(), 2);
576
577        // Test extractor functionality
578        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        // Create many routes to trigger performance warning
651        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        // Test that compilation uses move semantics efficiently
668        let start = std::time::Instant::now();
669
670        let mut builder = RouteCompilerBuilder::new().optimize(true);
671
672        // Create routes with complex metadata to test move optimization
673        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            // Add metadata to make cloning more expensive
681            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        // Verify compilation succeeded
695        assert_eq!(result.stats.total_routes, 100);
696        assert!(result.stats.optimizations_applied > 0);
697
698        // Should compile reasonably fast with move semantics
699        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}