use parking_lot::Mutex;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ValidationSeverity {
Info,
Warning,
Error,
}
impl ValidationSeverity {
pub fn as_str(&self) -> &'static str {
match self {
ValidationSeverity::Info => "INFO",
ValidationSeverity::Warning => "WARNING",
ValidationSeverity::Error => "ERROR",
}
}
}
#[derive(Debug, Clone)]
pub struct ValidationIssue {
pub severity: ValidationSeverity,
pub field: String,
pub message: String,
pub suggestion: Option<String>,
}
impl ValidationIssue {
pub fn new(
severity: ValidationSeverity,
field: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
severity,
field: field.into(),
message: message.into(),
suggestion: None,
}
}
pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
self.suggestion = Some(suggestion.into());
self
}
pub fn error(field: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(ValidationSeverity::Error, field, message)
}
pub fn warning(field: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(ValidationSeverity::Warning, field, message)
}
pub fn info(field: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(ValidationSeverity::Info, field, message)
}
}
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub valid: bool,
pub issues: Vec<ValidationIssue>,
}
impl ValidationResult {
pub fn success() -> Self {
Self {
valid: true,
issues: Vec::new(),
}
}
pub fn failure(issues: Vec<ValidationIssue>) -> Self {
let has_errors = issues
.iter()
.any(|i| i.severity == ValidationSeverity::Error);
Self {
valid: !has_errors,
issues,
}
}
pub fn add_issue(&mut self, issue: ValidationIssue) {
if issue.severity == ValidationSeverity::Error {
self.valid = false;
}
self.issues.push(issue);
}
pub fn has_errors(&self) -> bool {
self.issues
.iter()
.any(|i| i.severity == ValidationSeverity::Error)
}
pub fn errors(&self) -> Vec<&ValidationIssue> {
self.issues
.iter()
.filter(|i| i.severity == ValidationSeverity::Error)
.collect()
}
pub fn warnings(&self) -> Vec<&ValidationIssue> {
self.issues
.iter()
.filter(|i| i.severity == ValidationSeverity::Warning)
.collect()
}
}
pub trait ConfigValidator: Send + Sync {
fn name(&self) -> &str;
fn validate(&self, config: &HashMap<String, String>) -> ValidationResult;
fn auto_fix(&self, config: &HashMap<String, String>) -> HashMap<String, String> {
let _ = config;
HashMap::new()
}
fn priority(&self) -> u32 {
100
}
}
#[derive(Default)]
pub struct ValidatorRegistry {
validators: Mutex<Vec<Arc<dyn ConfigValidator>>>,
}
impl ValidatorRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&self, validator: Arc<dyn ConfigValidator>) {
let mut validators = self.validators.lock();
validators.push(validator);
validators.sort_by_key(|v| v.priority());
}
pub fn validate_all(&self, config: &HashMap<String, String>) -> ValidationResult {
let validators = self.validators.lock();
let mut result = ValidationResult::success();
for validator in validators.iter() {
let validator_result = validator.validate(config);
for issue in validator_result.issues {
result.add_issue(issue);
}
}
result
}
pub fn auto_fix_all(&self, config: &HashMap<String, String>) -> HashMap<String, String> {
let validators = self.validators.lock();
let mut fixes = HashMap::new();
for validator in validators.iter() {
let validator_fixes = validator.auto_fix(config);
fixes.extend(validator_fixes);
}
fixes
}
pub fn clear(&self) {
self.validators.lock().clear();
}
pub fn count(&self) -> usize {
self.validators.lock().len()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestValidator;
impl ConfigValidator for TestValidator {
fn name(&self) -> &str {
"test"
}
fn validate(&self, config: &HashMap<String, String>) -> ValidationResult {
let mut result = ValidationResult::success();
if config.get("invalid_field").is_some() {
result.add_issue(ValidationIssue::error("invalid_field", "Test error"));
}
result
}
}
#[test]
fn test_validation_result_detects_errors() {
let mut result = ValidationResult::success();
assert!(result.valid);
result.add_issue(ValidationIssue::warning("field", "warning"));
assert!(result.valid);
result.add_issue(ValidationIssue::error("field", "error"));
assert!(!result.valid);
}
#[test]
fn test_validator_registry_runs_all_validators() {
let registry = ValidatorRegistry::new();
registry.register(Arc::new(TestValidator));
let mut config = HashMap::new();
config.insert("invalid_field".to_string(), "value".to_string());
let result = registry.validate_all(&config);
assert!(!result.valid);
assert_eq!(result.errors().len(), 1);
}
#[test]
fn test_validation_issue_builder() {
let issue = ValidationIssue::error("field", "message").with_suggestion("fix");
assert_eq!(issue.severity, ValidationSeverity::Error);
assert_eq!(issue.field, "field");
assert_eq!(issue.message, "message");
assert_eq!(issue.suggestion, Some("fix".to_string()));
}
struct CrossFieldValidator;
impl ConfigValidator for CrossFieldValidator {
fn name(&self) -> &str {
"cross-field"
}
fn validate(&self, config: &HashMap<String, String>) -> ValidationResult {
let mut result = ValidationResult::success();
if let (Some(opt_level), Some(size_budget)) =
(config.get("profile.opt_level"), config.get("size_budget"))
{
if opt_level == "0" && size_budget.parse::<u64>().unwrap_or(0) < 100_000 {
result.add_issue(
ValidationIssue::error(
"profile.opt_level + size_budget",
"opt-level=0 with small size budget is incompatible",
)
.with_suggestion("Use opt-level='z' or 's' for size optimization"),
);
}
}
if let (Some(wasm_opt_flags), Some(target)) =
(config.get("wasm_opt.flags"), config.get("target"))
{
if target.contains("wasm32")
&& wasm_opt_flags.contains("--enable-simd")
&& !target.contains("simd")
{
result.add_issue(
ValidationIssue::warning(
"wasm_opt.flags + target",
"SIMD flags enabled but target doesn't explicitly support SIMD",
)
.with_suggestion(
"Verify target architecture supports SIMD or remove --enable-simd flag",
),
);
}
}
result
}
}
#[test]
fn test_validator_detects_incompatible_opt_level_and_budget() {
let validator = CrossFieldValidator;
let mut config = HashMap::new();
config.insert("profile.opt_level".to_string(), "0".to_string());
config.insert("size_budget".to_string(), "50000".to_string());
let result = validator.validate(&config);
assert!(
!result.valid,
"Should detect incompatible opt_level and size_budget"
);
assert_eq!(result.errors().len(), 1);
assert!(result.errors()[0].message.contains("incompatible"));
assert!(result.errors()[0].suggestion.is_some());
}
#[test]
fn test_validator_detects_invalid_wasm_opt_for_target() {
let validator = CrossFieldValidator;
let mut config = HashMap::new();
config.insert("wasm_opt.flags".to_string(), "--enable-simd".to_string());
config.insert("target".to_string(), "wasm32-unknown-unknown".to_string());
let result = validator.validate(&config);
assert!(result.valid, "Should be valid with warnings");
assert_eq!(result.warnings().len(), 1);
assert!(result.warnings()[0].message.contains("SIMD"));
}
#[test]
fn test_validator_with_multiple_constraint_violations() {
let validator = CrossFieldValidator;
let mut config = HashMap::new();
config.insert("profile.opt_level".to_string(), "0".to_string());
config.insert("size_budget".to_string(), "10000".to_string());
config.insert("wasm_opt.flags".to_string(), "--enable-simd".to_string());
config.insert("target".to_string(), "wasm32-unknown-unknown".to_string());
let result = validator.validate(&config);
assert!(!result.valid, "Should detect multiple violations");
assert_eq!(result.errors().len(), 1, "Should have 1 error");
assert_eq!(result.warnings().len(), 1, "Should have 1 warning");
}
#[test]
fn test_validator_allows_compatible_configurations() {
let validator = CrossFieldValidator;
let mut config = HashMap::new();
config.insert("profile.opt_level".to_string(), "z".to_string());
config.insert("size_budget".to_string(), "50000".to_string());
let result = validator.validate(&config);
assert!(result.valid);
assert_eq!(result.errors().len(), 0);
assert_eq!(result.warnings().len(), 0);
}
#[test]
fn test_validator_handles_missing_fields_gracefully() {
let validator = CrossFieldValidator;
let config = HashMap::new();
let result = validator.validate(&config);
assert!(result.valid);
assert_eq!(result.errors().len(), 0);
}
#[test]
fn test_validator_with_partial_cross_field_data() {
let validator = CrossFieldValidator;
let mut config = HashMap::new();
config.insert("profile.opt_level".to_string(), "0".to_string());
let result = validator.validate(&config);
assert!(result.valid);
}
#[test]
fn test_validation_severity_ordering() {
assert!(ValidationSeverity::Error > ValidationSeverity::Warning);
assert!(ValidationSeverity::Warning > ValidationSeverity::Info);
assert!(ValidationSeverity::Info < ValidationSeverity::Warning);
}
#[test]
fn test_validation_issue_with_all_severities() {
let error = ValidationIssue {
severity: ValidationSeverity::Error,
field: "test_field".to_string(),
message: "Error message".to_string(),
suggestion: None,
};
assert_eq!(error.severity, ValidationSeverity::Error);
let warning = ValidationIssue {
severity: ValidationSeverity::Warning,
field: "test_field".to_string(),
message: "Warning message".to_string(),
suggestion: None,
};
assert_eq!(warning.severity, ValidationSeverity::Warning);
let info = ValidationIssue {
severity: ValidationSeverity::Info,
field: "test_field".to_string(),
message: "Info message".to_string(),
suggestion: None,
};
assert_eq!(info.severity, ValidationSeverity::Info);
}
#[test]
fn test_validation_result_is_valid_with_no_issues() {
let result = ValidationResult::success();
assert!(result.valid);
assert!(!result.has_errors());
}
#[test]
fn test_validation_result_has_errors() {
let mut result = ValidationResult::success();
result.add_issue(ValidationIssue {
severity: ValidationSeverity::Error,
field: "field".to_string(),
message: "Error".to_string(),
suggestion: None,
});
assert!(result.has_errors());
assert!(!result.valid);
}
#[test]
fn test_validation_result_has_warnings_only() {
let mut result = ValidationResult::success();
result.add_issue(ValidationIssue {
severity: ValidationSeverity::Warning,
field: "field".to_string(),
message: "Warning".to_string(),
suggestion: None,
});
assert!(!result.warnings().is_empty());
assert!(!result.has_errors());
assert!(result.valid); }
#[test]
fn test_validation_result_with_mixed_severities() {
let mut result = ValidationResult::success();
result.add_issue(ValidationIssue {
severity: ValidationSeverity::Error,
field: "field1".to_string(),
message: "Error".to_string(),
suggestion: None,
});
result.add_issue(ValidationIssue {
severity: ValidationSeverity::Warning,
field: "field2".to_string(),
message: "Warning".to_string(),
suggestion: None,
});
result.add_issue(ValidationIssue {
severity: ValidationSeverity::Info,
field: "field3".to_string(),
message: "Info".to_string(),
suggestion: None,
});
assert!(result.has_errors());
assert!(!result.warnings().is_empty());
assert!(!result.valid);
assert_eq!(result.issues.len(), 3);
}
#[test]
fn test_validation_result_failure_constructor() {
let result = ValidationResult::failure(vec![ValidationIssue {
severity: ValidationSeverity::Error,
field: "test".to_string(),
message: "Error".to_string(),
suggestion: None,
}]);
assert!(!result.valid);
assert!(result.has_errors());
}
#[test]
fn test_validation_issue_field_and_message() {
let issue = ValidationIssue {
severity: ValidationSeverity::Error,
field: "test_field_name".to_string(),
message: "Test error message".to_string(),
suggestion: None,
};
assert_eq!(issue.field, "test_field_name");
assert_eq!(issue.message, "Test error message");
}
}