use crate::config::ValidationConfig;
use crate::crucible::CrucibleAdapter;
use crate::rules::{BreakingChangeChecker, ComplexityChecker, ValidationRule, VisibilityChecker};
use crate::semantic::IntentValidator;
use serde::{Deserialize, Serialize};
use smelt_core::{IntentRecord, SemanticDelta};
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ValidationSeverity {
Info,
Warning,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Violation {
pub rule: String,
pub severity: ValidationSeverity,
pub message: String,
pub location: Option<String>,
pub suggestion: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationOutcome {
pub passed: bool,
pub violations: Vec<Violation>,
pub error_count: usize,
pub warning_count: usize,
pub info_count: usize,
}
impl ValidationOutcome {
pub fn pass() -> Self {
Self {
passed: true,
violations: Vec::new(),
error_count: 0,
warning_count: 0,
info_count: 0,
}
}
pub fn has_errors(&self) -> bool {
self.error_count > 0
}
pub fn has_warnings(&self) -> bool {
self.warning_count > 0
}
pub fn errors(&self) -> impl Iterator<Item = &Violation> {
self.violations
.iter()
.filter(|v| v.severity == ValidationSeverity::Error)
}
pub fn warnings(&self) -> impl Iterator<Item = &Violation> {
self.violations
.iter()
.filter(|v| v.severity == ValidationSeverity::Warning)
}
}
pub struct SmeltValidator {
config: ValidationConfig,
rules: Vec<Box<dyn ValidationRule>>,
}
impl SmeltValidator {
pub fn new(config: ValidationConfig) -> Self {
let rules: Vec<Box<dyn ValidationRule>> = vec![
Box::new(BreakingChangeChecker::new(config.semantic.clone())),
Box::new(VisibilityChecker::new(config.semantic.clone())),
Box::new(ComplexityChecker::new(config.semantic.complexity.clone())),
Box::new(IntentValidator::new(config.intent.clone())),
];
Self { config, rules }
}
pub fn default_config() -> Self {
Self::new(ValidationConfig::default())
}
pub fn strict() -> Self {
Self::new(ValidationConfig::strict())
}
pub fn from_smelt_dir(smelt_dir: &Path) -> Self {
let config = ValidationConfig::load_or_default(smelt_dir);
let mut validator = Self::new(config);
if validator.config.architecture.check_circular_deps
|| validator.config.architecture.enforce_layers
{
if let Some(project_root) = smelt_dir.parent() {
let crucible = CrucibleAdapter::new(project_root)
.with_circular_deps(validator.config.architecture.check_circular_deps);
validator.add_rule(Box::new(crucible));
}
}
validator
}
pub fn config(&self) -> &ValidationConfig {
&self.config
}
pub fn validate(
&self,
delta: &SemanticDelta,
intent: Option<&IntentRecord>,
) -> ValidationOutcome {
let mut violations = Vec::new();
for rule in &self.rules {
let rule_violations = rule.validate(delta, intent);
violations.extend(rule_violations);
}
let error_count = violations
.iter()
.filter(|v| v.severity == ValidationSeverity::Error)
.count();
let warning_count = violations
.iter()
.filter(|v| v.severity == ValidationSeverity::Warning)
.count();
let info_count = violations
.iter()
.filter(|v| v.severity == ValidationSeverity::Info)
.count();
ValidationOutcome {
passed: error_count == 0,
violations,
error_count,
warning_count,
info_count,
}
}
pub fn validate_simple(&self, delta: &SemanticDelta, intent: Option<&IntentRecord>) -> bool {
self.validate(delta, intent).passed
}
pub fn add_rule(&mut self, rule: Box<dyn ValidationRule>) {
self.rules.push(rule);
}
}
impl Default for SmeltValidator {
fn default() -> Self {
Self::default_config()
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use smelt_core::{ImpactSummary, SemanticChange};
use uuid::Uuid;
fn make_delta(changes: Vec<SemanticChange>) -> SemanticDelta {
SemanticDelta {
id: Uuid::new_v4(),
intent_id: Uuid::new_v4(),
timestamp: Utc::now(),
from_snapshot: Uuid::new_v4(),
to_snapshot: Uuid::new_v4(),
changes,
impact_summary: ImpactSummary::default(),
}
}
#[test]
fn test_empty_delta_passes() {
let validator = SmeltValidator::default_config();
let delta = make_delta(vec![]);
let outcome = validator.validate(&delta, None);
assert!(outcome.passed);
assert_eq!(outcome.error_count, 0);
}
#[test]
fn test_breaking_change_fails() {
let validator = SmeltValidator::default_config();
let delta = make_delta(vec![SemanticChange::FunctionRemoved {
name: "public_api".to_string(),
file: "lib.rs".to_string(),
was_public: true,
}]);
let outcome = validator.validate(&delta, None);
assert!(!outcome.passed);
assert_eq!(outcome.error_count, 1);
}
#[test]
fn test_private_removal_passes() {
let validator = SmeltValidator::default_config();
let delta = make_delta(vec![SemanticChange::FunctionRemoved {
name: "helper".to_string(),
file: "lib.rs".to_string(),
was_public: false,
}]);
let outcome = validator.validate(&delta, None);
assert!(outcome.passed);
}
#[test]
fn test_strict_validator() {
let validator = SmeltValidator::strict();
let delta = SemanticDelta {
id: Uuid::new_v4(),
intent_id: Uuid::new_v4(),
timestamp: Utc::now(),
from_snapshot: Uuid::new_v4(),
to_snapshot: Uuid::new_v4(),
changes: vec![],
impact_summary: ImpactSummary {
complexity_delta: 10, ..Default::default()
},
};
let outcome = validator.validate(&delta, None);
assert!(outcome.has_errors() || outcome.has_warnings());
}
#[test]
fn test_outcome_helpers() {
let outcome = ValidationOutcome {
passed: false,
violations: vec![
Violation {
rule: "test".to_string(),
severity: ValidationSeverity::Error,
message: "error".to_string(),
location: None,
suggestion: None,
},
Violation {
rule: "test".to_string(),
severity: ValidationSeverity::Warning,
message: "warning".to_string(),
location: None,
suggestion: None,
},
],
error_count: 1,
warning_count: 1,
info_count: 0,
};
assert!(outcome.has_errors());
assert!(outcome.has_warnings());
assert_eq!(outcome.errors().count(), 1);
assert_eq!(outcome.warnings().count(), 1);
}
}