use thiserror::Error; use crate::route_types::{RouteConfig, RouteActionType}; /// Validation errors for route configurations. #[derive(Debug, Error)] pub enum ValidationError { #[error("Route '{name}' has no targets but action type is 'forward'")] MissingTargets { name: String }, #[error("Route '{name}' has empty targets list")] EmptyTargets { name: String }, #[error("Route '{name}' has no ports specified")] NoPorts { name: String }, #[error("Route '{name}' port {port} is invalid (must be 1-65535)")] InvalidPort { name: String, port: u16 }, #[error("Route '{name}': socket-handler action type is not supported in JSON config")] SocketHandlerInJson { name: String }, #[error("Route '{name}': duplicate route ID '{id}'")] DuplicateId { name: String, id: String }, #[error("Route '{name}': {message}")] Custom { name: String, message: String }, } /// Validate a single route configuration. pub fn validate_route(route: &RouteConfig) -> Result<(), Vec> { let mut errors = Vec::new(); let name = route.name.clone().unwrap_or_else(|| { route.id.clone().unwrap_or_else(|| "unnamed".to_string()) }); // Check ports let ports = route.listening_ports(); if ports.is_empty() { errors.push(ValidationError::NoPorts { name: name.clone() }); } for &port in &ports { if port == 0 { errors.push(ValidationError::InvalidPort { name: name.clone(), port, }); } } // Check forward action has targets if route.action.action_type == RouteActionType::Forward { match &route.action.targets { None => { errors.push(ValidationError::MissingTargets { name: name.clone() }); } Some(targets) if targets.is_empty() => { errors.push(ValidationError::EmptyTargets { name: name.clone() }); } _ => {} } } if errors.is_empty() { Ok(()) } else { Err(errors) } } /// Validate an entire list of routes. pub fn validate_routes(routes: &[RouteConfig]) -> Result<(), Vec> { let mut all_errors = Vec::new(); let mut seen_ids = std::collections::HashSet::new(); for route in routes { // Check for duplicate IDs if let Some(id) = &route.id { if !seen_ids.insert(id.clone()) { let name = route.name.clone().unwrap_or_else(|| id.clone()); all_errors.push(ValidationError::DuplicateId { name, id: id.clone(), }); } } // Validate individual route if let Err(errors) = validate_route(route) { all_errors.extend(errors); } } if all_errors.is_empty() { Ok(()) } else { Err(all_errors) } } #[cfg(test)] mod tests { use super::*; use crate::route_types::*; fn make_valid_route() -> RouteConfig { crate::helpers::create_http_route("example.com", "localhost", 8080) } #[test] fn test_valid_route_passes() { let route = make_valid_route(); assert!(validate_route(&route).is_ok()); } #[test] fn test_missing_targets() { let mut route = make_valid_route(); route.action.targets = None; let errors = validate_route(&route).unwrap_err(); assert!(errors.iter().any(|e| matches!(e, ValidationError::MissingTargets { .. }))); } #[test] fn test_empty_targets() { let mut route = make_valid_route(); route.action.targets = Some(vec![]); let errors = validate_route(&route).unwrap_err(); assert!(errors.iter().any(|e| matches!(e, ValidationError::EmptyTargets { .. }))); } #[test] fn test_invalid_port_zero() { let mut route = make_valid_route(); route.route_match.ports = PortRange::Single(0); let errors = validate_route(&route).unwrap_err(); assert!(errors.iter().any(|e| matches!(e, ValidationError::InvalidPort { port: 0, .. }))); } #[test] fn test_duplicate_ids() { let mut r1 = make_valid_route(); r1.id = Some("route-1".to_string()); let mut r2 = make_valid_route(); r2.id = Some("route-1".to_string()); let errors = validate_routes(&[r1, r2]).unwrap_err(); assert!(errors.iter().any(|e| matches!(e, ValidationError::DuplicateId { .. }))); } #[test] fn test_multiple_errors_collected() { let mut r1 = make_valid_route(); r1.action.targets = None; // MissingTargets r1.route_match.ports = PortRange::Single(0); // InvalidPort let errors = validate_route(&r1).unwrap_err(); assert!(errors.len() >= 2); } }