elif_http/bootstrap/
route_validator.rs

1//! Route conflict detection and validation system for bootstrap integration
2//!
3//! This module provides comprehensive route conflict detection during application startup,
4//! building on the core route matching capabilities with enhanced diagnostics and
5//! conflict resolution suggestions.
6
7use crate::{
8    bootstrap::BootstrapError,
9    routing::{HttpMethod, RouteMatchError, RouteMatcher, RouteDefinition},
10};
11use std::collections::{HashMap, HashSet};
12use thiserror::Error;
13
14/// Errors that can occur during route validation
15#[derive(Error, Debug)]
16pub enum RouteValidationError {
17    #[error("Route conflict detected")]
18    ConflictDetected {
19        conflicts: Vec<RouteConflict>,
20    },
21    #[error("Parameter type conflict in route {route}: {details}")]
22    ParameterConflict {
23        route: String,
24        details: String,
25    },
26    #[error("Invalid route configuration: {message}")]
27    InvalidConfiguration {
28        message: String,
29    },
30    #[error("Route validation failed: {0}")]
31    ValidationFailed(#[from] RouteMatchError),
32}
33
34/// Detailed information about a route conflict
35#[derive(Debug, Clone)]
36pub struct RouteConflict {
37    pub route1: RouteInfo,
38    pub route2: RouteInfo,
39    pub conflict_type: ConflictType,
40    pub resolution_suggestions: Vec<ConflictResolution>,
41}
42
43/// Information about a conflicting route
44#[derive(Debug, Clone)]
45pub struct RouteInfo {
46    pub method: HttpMethod,
47    pub path: String,
48    pub controller: String,
49    pub handler: String,
50    pub middleware: Vec<String>,
51    pub parameters: Vec<ParamDef>,
52}
53
54/// Types of route conflicts
55#[derive(Debug, Clone)]
56pub enum ConflictType {
57    /// Exact path conflict (same method + path)
58    Exact,
59    /// Parameter type mismatch for same path pattern
60    ParameterMismatch,
61    /// Ambiguous route patterns that could match same request
62    Ambiguous,
63    /// Middleware incompatibility
64    MiddlewareIncompatible,
65}
66
67/// Parameter definition for validation
68#[derive(Debug, Clone)]
69pub struct ParamDef {
70    pub name: String,
71    pub param_type: String,
72    pub required: bool,
73    pub constraints: Vec<String>,
74}
75
76/// Suggested resolutions for route conflicts
77#[derive(Debug, Clone)]
78pub enum ConflictResolution {
79    MergePaths { suggestion: String },
80    RenameParameter { from: String, to: String },
81    DifferentControllerPaths { suggestion: String },
82    MiddlewareConsolidation { suggestion: String },
83    UseQueryParameters { suggestion: String },
84    ReorderRoutes { suggestion: String },
85}
86
87/// Route validator for bootstrap integration
88#[derive(Debug)]
89pub struct RouteValidator {
90    /// All registered routes for validation
91    routes: HashMap<RouteKey, RouteRegistration>,
92    /// Route matcher for conflict detection
93    matcher: RouteMatcher,
94    /// Enable detailed diagnostics
95    enable_diagnostics: bool,
96}
97
98/// Key for identifying unique routes
99#[derive(Hash, PartialEq, Eq, Debug, Clone)]
100pub struct RouteKey {
101    pub method: HttpMethod,
102    pub path_pattern: String,
103}
104
105/// Registration information for a route
106#[derive(Debug, Clone)]
107pub struct RouteRegistration {
108    pub controller: String,
109    pub handler: String,
110    pub middleware: Vec<String>,
111    pub parameters: Vec<ParamDef>,
112    pub definition: RouteDefinition,
113}
114
115impl RouteValidator {
116    /// Create a new route validator
117    pub fn new() -> Self {
118        Self {
119            routes: HashMap::new(),
120            matcher: RouteMatcher::new(),
121            enable_diagnostics: true,
122        }
123    }
124
125    /// Enable or disable detailed diagnostics
126    pub fn with_diagnostics(mut self, enable: bool) -> Self {
127        self.enable_diagnostics = enable;
128        self
129    }
130
131    /// Register a route for validation
132    pub fn register_route(&mut self, registration: RouteRegistration) -> Result<(), RouteValidationError> {
133        let route_key = RouteKey {
134            method: registration.definition.method.clone(),
135            path_pattern: registration.definition.path.clone(),
136        };
137
138        // Check for conflicts before adding
139        if let Some(existing) = self.routes.get(&route_key) {
140            let conflict = self.analyze_conflict(&registration, existing)?;
141            return Err(RouteValidationError::ConflictDetected {
142                conflicts: vec![conflict],
143            });
144        }
145
146        // Add to route matcher for pattern conflict detection
147        self.matcher.add_route(registration.definition.clone())
148            .map_err(RouteValidationError::ValidationFailed)?;
149
150        // Store registration
151        self.routes.insert(route_key, registration);
152        
153        Ok(())
154    }
155
156    /// Validate all registered routes for conflicts
157    pub fn validate_all_routes(&self) -> Result<ValidationReport, RouteValidationError> {
158        let mut conflicts = Vec::new();
159        let mut warnings = Vec::new();
160
161        // Check for parameter type conflicts
162        self.check_parameter_conflicts(&mut conflicts);
163
164        // Check for middleware incompatibilities
165        self.check_middleware_conflicts(&mut warnings);
166
167        // Check for performance issues
168        self.check_performance_issues(&mut warnings);
169
170        if !conflicts.is_empty() {
171            return Err(RouteValidationError::ConflictDetected { conflicts });
172        }
173
174        Ok(ValidationReport {
175            total_routes: self.routes.len(),
176            conflicts: conflicts.len(),
177            warnings: warnings.len(),
178            performance_score: self.calculate_performance_score(),
179            suggestions: self.generate_optimization_suggestions(),
180        })
181    }
182
183    /// Generate detailed conflict report for diagnostics
184    pub fn generate_conflict_report(&self, conflicts: &[RouteConflict]) -> String {
185        let mut report = String::new();
186        
187        for (i, conflict) in conflicts.iter().enumerate() {
188            if i > 0 {
189                report.push_str("\n\n");
190            }
191            
192            match conflict.conflict_type {
193                ConflictType::Exact => {
194                    report.push_str(&format!(
195                        "Error: Duplicate route definition detected\n\n\
196                         Route: {} {}\n\
197                         Defined in:\n\
198                         1. {}::{}\n\
199                         2. {}::{}\n\n\
200                         Resolution suggestions:",
201                        conflict.route1.method.as_str(),
202                        conflict.route1.path,
203                        conflict.route1.controller,
204                        conflict.route1.handler,
205                        conflict.route2.controller,
206                        conflict.route2.handler
207                    ));
208                }
209                ConflictType::ParameterMismatch => {
210                    report.push_str(&format!(
211                        "Error: Route parameter type conflict\n\n\
212                         Route pattern: {} {}\n\
213                         Parameter conflicts:\n\
214                         • {} expects different types\n\
215                         • {} expects different types\n\n\
216                         Resolution: Ensure all controllers use the same parameter types",
217                        conflict.route1.method.as_str(),
218                        conflict.route1.path,
219                        conflict.route1.controller,
220                        conflict.route2.controller
221                    ));
222                }
223                ConflictType::Ambiguous => {
224                    report.push_str(&format!(
225                        "Error: Ambiguous route patterns detected\n\n\
226                         Routes that could match the same request:\n\
227                         1. {} {} ({})\n\
228                         2. {} {} ({})\n\n\
229                         Problem: These patterns could match the same request\n\n\
230                         Resolution: Reorder routes or use more specific patterns",
231                        conflict.route1.method.as_str(),
232                        conflict.route1.path,
233                        conflict.route1.controller,
234                        conflict.route2.method.as_str(),
235                        conflict.route2.path,
236                        conflict.route2.controller
237                    ));
238                }
239                ConflictType::MiddlewareIncompatible => {
240                    report.push_str(&format!(
241                        "Warning: Middleware incompatibility detected\n\n\
242                         Route: {} {}\n\
243                         Controllers with different middleware:\n\
244                         • {}: {:?}\n\
245                         • {}: {:?}\n\n\
246                         Resolution: Consider consolidating middleware requirements",
247                        conflict.route1.method.as_str(),
248                        conflict.route1.path,
249                        conflict.route1.controller,
250                        conflict.route1.middleware,
251                        conflict.route2.controller,
252                        conflict.route2.middleware
253                    ));
254                }
255            }
256
257            // Add resolution suggestions
258            for (j, suggestion) in conflict.resolution_suggestions.iter().enumerate() {
259                report.push_str(&format!("\n  {}. {}", j + 1, self.format_suggestion(suggestion)));
260            }
261        }
262
263        report
264    }
265
266    /// Analyze conflict between two route registrations
267    fn analyze_conflict(&self, route1: &RouteRegistration, route2: &RouteRegistration) -> Result<RouteConflict, RouteValidationError> {
268        let route_info1 = RouteInfo {
269            method: route1.definition.method.clone(),
270            path: route1.definition.path.clone(),
271            controller: route1.controller.clone(),
272            handler: route1.handler.clone(),
273            middleware: route1.middleware.clone(),
274            parameters: route1.parameters.clone(),
275        };
276
277        let route_info2 = RouteInfo {
278            method: route2.definition.method.clone(),
279            path: route2.definition.path.clone(),
280            controller: route2.controller.clone(),
281            handler: route2.handler.clone(),
282            middleware: route2.middleware.clone(),
283            parameters: route2.parameters.clone(),
284        };
285
286        let conflict_type = if route1.definition.path == route2.definition.path {
287            if self.parameters_conflict(&route1.parameters, &route2.parameters) {
288                ConflictType::ParameterMismatch
289            } else {
290                ConflictType::Exact
291            }
292        } else {
293            ConflictType::Ambiguous
294        };
295
296        let resolution_suggestions = self.generate_resolution_suggestions(&route_info1, &route_info2, &conflict_type);
297
298        Ok(RouteConflict {
299            route1: route_info1,
300            route2: route_info2,
301            conflict_type,
302            resolution_suggestions,
303        })
304    }
305
306    /// Check if parameters conflict between routes
307    fn parameters_conflict(&self, params1: &[ParamDef], params2: &[ParamDef]) -> bool {
308        for param1 in params1 {
309            for param2 in params2 {
310                if param1.name == param2.name && param1.param_type != param2.param_type {
311                    return true;
312                }
313            }
314        }
315        false
316    }
317
318    /// Check for parameter conflicts across all routes
319    fn check_parameter_conflicts(&self, _conflicts: &mut Vec<RouteConflict>) {
320        let mut param_types: HashMap<String, (String, String)> = HashMap::new();
321        
322        for registration in self.routes.values() {
323            for param in &registration.parameters {
324                let key = format!("{}:{}", registration.definition.path, param.name);
325                if let Some((existing_type, _existing_controller)) = param_types.get(&key) {
326                    if existing_type != &param.param_type {
327                        // Found parameter conflict - would need to create RouteConflict
328                        // This is simplified for now
329                    }
330                } else {
331                    param_types.insert(key, (param.param_type.clone(), registration.controller.clone()));
332                }
333            }
334        }
335    }
336
337    /// Check for middleware conflicts
338    fn check_middleware_conflicts(&self, warnings: &mut Vec<String>) {
339        // Group routes by path pattern to check middleware consistency
340        let mut path_middleware: HashMap<String, Vec<(String, Vec<String>)>> = HashMap::new();
341        
342        for registration in self.routes.values() {
343            let path = &registration.definition.path;
344            path_middleware
345                .entry(path.clone())
346                .or_default()
347                .push((registration.controller.clone(), registration.middleware.clone()));
348        }
349
350        for (path, controllers) in path_middleware {
351            if controllers.len() > 1 {
352                let middleware_sets: HashSet<Vec<String>> = controllers.iter().map(|(_, mw)| mw.clone()).collect();
353                if middleware_sets.len() > 1 {
354                    warnings.push(format!(
355                        "Inconsistent middleware for path {}: controllers have different middleware requirements",
356                        path
357                    ));
358                }
359            }
360        }
361    }
362
363    /// Check for performance issues
364    fn check_performance_issues(&self, warnings: &mut Vec<String>) {
365        if self.routes.len() > 1000 {
366            warnings.push("Large number of routes (>1000) may impact performance".to_string());
367        }
368
369        // Check for overly complex patterns
370        for registration in self.routes.values() {
371            let param_count = registration.parameters.len();
372            if param_count > 5 {
373                warnings.push(format!(
374                    "Route {} has {} parameters, consider simplifying",
375                    registration.definition.path,
376                    param_count
377                ));
378            }
379        }
380    }
381
382    /// Calculate overall performance score
383    fn calculate_performance_score(&self) -> u32 {
384        let base_score: u32 = 100;
385        let route_penalty = (self.routes.len() / 100) as u32; // 1 point per 100 routes
386        
387        let complex_routes = self.routes.values()
388            .filter(|r| r.parameters.len() > 3)
389            .count() as u32;
390        
391        base_score.saturating_sub(route_penalty + complex_routes)
392    }
393
394    /// Generate optimization suggestions
395    fn generate_optimization_suggestions(&self) -> Vec<String> {
396        let mut suggestions = Vec::new();
397
398        if self.routes.len() > 500 {
399            suggestions.push("Consider grouping routes by modules for better organization".to_string());
400        }
401
402        suggestions
403    }
404
405    /// Generate resolution suggestions for conflicts
406    fn generate_resolution_suggestions(&self, _route1: &RouteInfo, _route2: &RouteInfo, conflict_type: &ConflictType) -> Vec<ConflictResolution> {
407        match conflict_type {
408            ConflictType::Exact => vec![
409                ConflictResolution::DifferentControllerPaths { 
410                    suggestion: "Use different base paths like /api/users vs /api/admin/users".to_string() 
411                },
412                ConflictResolution::MergePaths { 
413                    suggestion: "Merge functionality into a single controller".to_string() 
414                },
415                ConflictResolution::UseQueryParameters { 
416                    suggestion: "Use query parameters instead: GET /api/users/{id}?admin=true".to_string() 
417                },
418            ],
419            ConflictType::ParameterMismatch => vec![
420                ConflictResolution::RenameParameter { 
421                    from: "id".to_string(), 
422                    to: "user_id".to_string() 
423                },
424            ],
425            ConflictType::Ambiguous => vec![
426                ConflictResolution::ReorderRoutes { 
427                    suggestion: "Reorder routes to put more specific patterns first".to_string() 
428                },
429            ],
430            ConflictType::MiddlewareIncompatible => vec![
431                ConflictResolution::MiddlewareConsolidation { 
432                    suggestion: "Consolidate middleware requirements across controllers".to_string() 
433                },
434            ],
435        }
436    }
437
438    /// Format a resolution suggestion for display
439    fn format_suggestion(&self, suggestion: &ConflictResolution) -> String {
440        match suggestion {
441            ConflictResolution::MergePaths { suggestion } => suggestion.clone(),
442            ConflictResolution::RenameParameter { from, to } => {
443                format!("Rename parameter '{}' to '{}'", from, to)
444            },
445            ConflictResolution::DifferentControllerPaths { suggestion } => suggestion.clone(),
446            ConflictResolution::MiddlewareConsolidation { suggestion } => suggestion.clone(),
447            ConflictResolution::UseQueryParameters { suggestion } => suggestion.clone(),
448            ConflictResolution::ReorderRoutes { suggestion } => suggestion.clone(),
449        }
450    }
451}
452
453/// Report generated after route validation
454#[derive(Debug)]
455pub struct ValidationReport {
456    pub total_routes: usize,
457    pub conflicts: usize,
458    pub warnings: usize,
459    pub performance_score: u32,
460    pub suggestions: Vec<String>,
461}
462
463impl Default for RouteValidator {
464    fn default() -> Self {
465        Self::new()
466    }
467}
468
469/// Convert RouteValidationError to BootstrapError for integration
470impl From<RouteValidationError> for BootstrapError {
471    fn from(err: RouteValidationError) -> Self {
472        BootstrapError::RouteRegistrationFailed {
473            message: format!("Route validation failed: {}", err),
474        }
475    }
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    /// Helper to create test route registration
483    fn create_test_route(
484        controller: &str,
485        handler: &str,
486        method: HttpMethod,
487        path: &str,
488        params: Vec<ParamDef>,
489    ) -> RouteRegistration {
490        RouteRegistration {
491            controller: controller.to_string(),
492            handler: handler.to_string(),
493            middleware: Vec::new(),
494            parameters: params,
495            definition: RouteDefinition {
496                id: format!("{}::{}", controller, handler),
497                method,
498                path: path.to_string(),
499            },
500        }
501    }
502
503    #[test]
504    fn test_successful_route_registration() {
505        let mut validator = RouteValidator::new();
506        
507        let route = create_test_route(
508            "UserController",
509            "get_user",
510            HttpMethod::GET,
511            "/api/users/{id}",
512            vec![ParamDef {
513                name: "id".to_string(),
514                param_type: "u32".to_string(),
515                required: true,
516                constraints: vec!["int".to_string()],
517            }]
518        );
519
520        let result = validator.register_route(route);
521        assert!(result.is_ok(), "Route registration should succeed");
522        
523        let report = validator.validate_all_routes().unwrap();
524        assert_eq!(report.total_routes, 1);
525        assert_eq!(report.conflicts, 0);
526    }
527
528    #[test]
529    fn test_exact_route_conflict_detection() {
530        let mut validator = RouteValidator::new();
531        
532        // Register first route
533        let route1 = create_test_route(
534            "UserController",
535            "get_user",
536            HttpMethod::GET,
537            "/api/users/{id}",
538            vec![ParamDef {
539                name: "id".to_string(),
540                param_type: "u32".to_string(),
541                required: true,
542                constraints: vec!["int".to_string()],
543            }]
544        );
545        validator.register_route(route1).unwrap();
546
547        // Try to register conflicting route
548        let route2 = create_test_route(
549            "AdminController",
550            "get_admin_user",
551            HttpMethod::GET,
552            "/api/users/{id}",  // Same path!
553            vec![ParamDef {
554                name: "id".to_string(),
555                param_type: "u32".to_string(),
556                required: true,
557                constraints: vec!["int".to_string()],
558            }]
559        );
560
561        let result = validator.register_route(route2);
562        assert!(result.is_err(), "Conflicting route should be rejected");
563        
564        match result.unwrap_err() {
565            RouteValidationError::ConflictDetected { conflicts } => {
566                assert_eq!(conflicts.len(), 1);
567                assert!(matches!(conflicts[0].conflict_type, ConflictType::Exact));
568            },
569            _ => panic!("Expected ConflictDetected error"),
570        }
571    }
572
573    #[test]
574    fn test_parameter_conflict_detection() {
575        let validator = RouteValidator::new();
576        
577        let route1 = create_test_route(
578            "UserController",
579            "get_user",
580            HttpMethod::GET,
581            "/api/users/{id}",
582            vec![ParamDef {
583                name: "id".to_string(),
584                param_type: "u32".to_string(),
585                required: true,
586                constraints: vec!["int".to_string()],
587            }]
588        );
589
590        let route2 = create_test_route(
591            "AdminController", 
592            "get_admin_user",
593            HttpMethod::GET,
594            "/api/users/{id}",
595            vec![ParamDef {
596                name: "id".to_string(),
597                param_type: "String".to_string(), // Different type!
598                required: true,
599                constraints: vec!["string".to_string()],
600            }]
601        );
602
603        // Check if parameters conflict
604        let conflicts = validator.parameters_conflict(&route1.parameters, &route2.parameters);
605        assert!(conflicts, "Parameters with same name but different types should conflict");
606    }
607
608    #[test]
609    fn test_conflict_report_generation() {
610        let validator = RouteValidator::new();
611        
612        let route_info1 = RouteInfo {
613            method: HttpMethod::GET,
614            path: "/api/users/{id}".to_string(),
615            controller: "UserController".to_string(),
616            handler: "get_user".to_string(),
617            middleware: Vec::new(),
618            parameters: Vec::new(),
619        };
620
621        let route_info2 = RouteInfo {
622            method: HttpMethod::GET,
623            path: "/api/users/{id}".to_string(),
624            controller: "AdminController".to_string(),
625            handler: "get_admin_user".to_string(),
626            middleware: Vec::new(),
627            parameters: Vec::new(),
628        };
629
630        let conflict = RouteConflict {
631            route1: route_info1,
632            route2: route_info2,
633            conflict_type: ConflictType::Exact,
634            resolution_suggestions: vec![
635                ConflictResolution::DifferentControllerPaths {
636                    suggestion: "Use different paths".to_string()
637                }
638            ],
639        };
640
641        let report = validator.generate_conflict_report(&[conflict]);
642        
643        assert!(report.contains("Duplicate route definition detected"));
644        assert!(report.contains("UserController::get_user"));
645        assert!(report.contains("AdminController::get_admin_user"));
646        assert!(report.contains("Resolution suggestions"));
647    }
648
649    #[test]
650    fn test_validation_report_generation() {
651        let mut validator = RouteValidator::new();
652        
653        // Register multiple routes
654        for i in 0..5 {
655            let route = create_test_route(
656                &format!("Controller{}", i),
657                "handler",
658                HttpMethod::GET,
659                &format!("/api/resource{}/{}", i, "{id}"),
660                vec![ParamDef {
661                    name: "id".to_string(),
662                    param_type: "u32".to_string(),
663                    required: true,
664                    constraints: vec!["int".to_string()],
665                }]
666            );
667            validator.register_route(route).unwrap();
668        }
669
670        let report = validator.validate_all_routes().unwrap();
671        
672        assert_eq!(report.total_routes, 5);
673        assert_eq!(report.conflicts, 0);
674        assert!(report.performance_score > 0);
675    }
676
677    #[test]
678    fn test_performance_scoring() {
679        let validator = RouteValidator::new();
680        
681        // Empty validator should have perfect score
682        let score = validator.calculate_performance_score();
683        assert_eq!(score, 100);
684    }
685
686    #[test]
687    fn test_resolution_suggestions() {
688        let validator = RouteValidator::new();
689        
690        let route1 = RouteInfo {
691            method: HttpMethod::GET,
692            path: "/api/users".to_string(),
693            controller: "UserController".to_string(),
694            handler: "list".to_string(),
695            middleware: Vec::new(),
696            parameters: Vec::new(),
697        };
698
699        let route2 = RouteInfo {
700            method: HttpMethod::GET,
701            path: "/api/users".to_string(),
702            controller: "AdminController".to_string(),
703            handler: "list_admin".to_string(),
704            middleware: Vec::new(),
705            parameters: Vec::new(),
706        };
707
708        let suggestions = validator.generate_resolution_suggestions(&route1, &route2, &ConflictType::Exact);
709        
710        assert!(!suggestions.is_empty());
711        assert!(matches!(suggestions[0], ConflictResolution::DifferentControllerPaths { .. }));
712    }
713}