159 lines
4.7 KiB
Rust
159 lines
4.7 KiB
Rust
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<ValidationError>> {
|
|
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<ValidationError>> {
|
|
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);
|
|
}
|
|
}
|