use crate::abi::{K9Contract, MustRule, ValidationResult, Violation};
use crate::codegen::parser::{ParsedEntry, ValueType};
pub fn validate_config(entries: &[ParsedEntry], contract: &K9Contract) -> ValidationResult {
let mut violations = Vec::new();
for rule in &contract.must_rules {
let entry = entries.iter().find(|e| {
e.key == rule.key || e.key.ends_with(&format!(".{}", rule.key))
});
match entry {
None => {
violations.push(Violation {
rule: rule.to_string(),
key: Some(rule.key.clone()),
message: format!("Key '{}' not found in config", rule.key),
});
}
Some(entry) => {
if let Some(violation) = check_must_rule(entry, rule) {
violations.push(violation);
}
}
}
}
if violations.is_empty() {
ValidationResult::Pass
} else {
ValidationResult::Fail(violations)
}
}
fn check_must_rule(entry: &ParsedEntry, rule: &MustRule) -> Option<Violation> {
match entry.value_type {
ValueType::Int | ValueType::Float => check_numeric_rule(entry, rule),
ValueType::String => check_string_rule(entry, rule),
ValueType::Bool => check_string_rule(entry, rule),
ValueType::Array | ValueType::Table => {
check_string_rule(entry, rule)
}
}
}
fn check_numeric_rule(entry: &ParsedEntry, rule: &MustRule) -> Option<Violation> {
let actual: f64 = match entry.value.parse() {
Ok(v) => v,
Err(_) => {
return Some(Violation {
rule: rule.to_string(),
key: Some(rule.key.clone()),
message: format!(
"Cannot parse '{}' as number for numeric comparison",
entry.value
),
});
}
};
let expected: f64 = match rule.value.parse() {
Ok(v) => v,
Err(_) => {
return check_string_rule(entry, rule);
}
};
let passed = match rule.operator.as_str() {
">" => actual > expected,
"<" => actual < expected,
">=" => actual >= expected,
"<=" => actual <= expected,
"==" => (actual - expected).abs() < f64::EPSILON,
"!=" => (actual - expected).abs() >= f64::EPSILON,
_ => {
return Some(Violation {
rule: rule.to_string(),
key: Some(rule.key.clone()),
message: format!("Unknown operator '{}'", rule.operator),
});
}
};
if passed {
None
} else {
Some(Violation {
rule: rule.to_string(),
key: Some(rule.key.clone()),
message: format!(
"Value {} does not satisfy {} {}",
entry.value, rule.operator, rule.value
),
})
}
}
fn check_string_rule(entry: &ParsedEntry, rule: &MustRule) -> Option<Violation> {
let actual = &entry.value;
let expected = unquote_value(&rule.value);
let passed = match rule.operator.as_str() {
"==" => actual == &expected,
"!=" => actual != &expected,
">" | "<" | ">=" | "<=" => {
match rule.operator.as_str() {
">" => actual.as_str() > expected.as_str(),
"<" => actual.as_str() < expected.as_str(),
">=" => actual.as_str() >= expected.as_str(),
"<=" => actual.as_str() <= expected.as_str(),
_ => unreachable!(),
}
}
_ => {
return Some(Violation {
rule: rule.to_string(),
key: Some(rule.key.clone()),
message: format!("Unknown operator '{}'", rule.operator),
});
}
};
if passed {
None
} else {
Some(Violation {
rule: rule.to_string(),
key: Some(rule.key.clone()),
message: format!(
"Value '{}' does not satisfy {} {}",
actual, rule.operator, rule.value
),
})
}
}
fn unquote_value(s: &str) -> String {
if (s.starts_with('\'') && s.ends_with('\'')) || (s.starts_with('"') && s.ends_with('"')) {
s[1..s.len() - 1].to_string()
} else {
s.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::abi::{ConfigFormat, SafetyTier};
use crate::codegen::contract::build_k9_contract;
#[test]
fn test_validate_passing_config() {
let entries = vec![
ParsedEntry {
key: "server.port".into(),
value: "8080".into(),
value_type: ValueType::Int,
},
ParsedEntry {
key: "server.host".into(),
value: "localhost".into(),
value_type: ValueType::String,
},
];
let contract = build_k9_contract(
"test",
"config/test.toml",
ConfigFormat::Toml,
SafetyTier::Kennel,
&["port > 0".into(), "port < 65536".into(), "host != ''".into()],
&[],
&[],
&[],
);
let result = validate_config(&entries, &contract);
assert!(result.is_pass());
}
#[test]
fn test_validate_failing_config() {
let entries = vec![ParsedEntry {
key: "port".into(),
value: "-1".into(),
value_type: ValueType::Int,
}];
let contract = build_k9_contract(
"test",
"config/test.toml",
ConfigFormat::Toml,
SafetyTier::Kennel,
&["port > 0".into()],
&[],
&[],
&[],
);
let result = validate_config(&entries, &contract);
assert!(!result.is_pass());
assert_eq!(result.violations().len(), 1);
}
#[test]
fn test_validate_missing_key() {
let entries = vec![];
let contract = build_k9_contract(
"test",
"config/test.toml",
ConfigFormat::Toml,
SafetyTier::Kennel,
&["port > 0".into()],
&[],
&[],
&[],
);
let result = validate_config(&entries, &contract);
assert!(!result.is_pass());
assert!(result.violations()[0].message.contains("not found"));
}
#[test]
fn test_validate_string_not_empty() {
let entries = vec![ParsedEntry {
key: "host".into(),
value: "".into(),
value_type: ValueType::String,
}];
let contract = build_k9_contract(
"test",
"config/test.toml",
ConfigFormat::Toml,
SafetyTier::Kennel,
&["host != ''".into()],
&[],
&[],
&[],
);
let result = validate_config(&entries, &contract);
assert!(!result.is_pass());
}
}