use crate::error::{ValidationError, ValidationResult};
use crate::rules::RuleRegistry;
#[derive(Debug, Clone)]
pub struct FieldConstraint {
pub path: String,
pub rule_ids: Vec<String>,
}
impl FieldConstraint {
pub fn new(
path: impl Into<String>,
rule_ids: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
Self {
path: path.into(),
rule_ids: rule_ids.into_iter().map(Into::into).collect(),
}
}
}
#[derive(Debug, Default)]
pub struct ConstraintSet {
constraints: Vec<FieldConstraint>,
}
impl ConstraintSet {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, constraint: FieldConstraint) {
self.constraints.push(constraint);
}
pub(crate) fn for_path(&self, path: &str) -> Vec<&FieldConstraint> {
self.constraints.iter().filter(|c| c.path == path).collect()
}
pub fn validate_field(
&self,
path: &str,
value: &str,
registry: &RuleRegistry,
) -> ValidationResult {
let rule_ids: Vec<&str> = self
.for_path(path)
.into_iter()
.flat_map(|c| c.rule_ids.iter().map(String::as_str))
.collect();
if rule_ids.is_empty() {
return ValidationResult::default();
}
let errors: Vec<ValidationError> = registry.validate_field(value, path, &rule_ids);
ValidationResult::new(errors)
}
pub fn len(&self) -> usize {
self.constraints.len()
}
pub fn is_empty(&self) -> bool {
self.constraints.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rules::RuleRegistry;
#[test]
fn empty_constraint_set_produces_no_errors() {
let cs = ConstraintSet::new();
let registry = RuleRegistry::with_defaults();
let result = cs.validate_field("/some/path", "value", ®istry);
assert!(result.is_valid());
}
#[test]
fn constraint_set_with_iban_validates() {
let mut cs = ConstraintSet::new();
cs.add(FieldConstraint::new("/iban", ["IBAN_CHECK"]));
let registry = RuleRegistry::with_defaults();
let ok = cs.validate_field("/iban", "GB82WEST12345698765432", ®istry);
assert!(ok.is_valid());
let fail = cs.validate_field("/iban", "NOTANIBAN", ®istry);
assert!(!fail.is_valid());
}
#[test]
fn constraint_set_with_bic_validates() {
let mut cs = ConstraintSet::new();
cs.add(FieldConstraint::new("/bic", ["BIC_CHECK"]));
let registry = RuleRegistry::with_defaults();
let ok = cs.validate_field("/bic", "AAAAGB2L", ®istry);
assert!(ok.is_valid());
let fail = cs.validate_field("/bic", "bad", ®istry);
assert!(!fail.is_valid());
}
#[test]
fn unknown_rule_id_does_not_panic() {
let mut cs = ConstraintSet::new();
cs.add(FieldConstraint::new("/f", ["NO_SUCH_RULE"]));
let registry = RuleRegistry::with_defaults();
let result = cs.validate_field("/f", "anything", ®istry);
assert!(result.is_valid());
}
#[test]
fn for_path_returns_correct_constraints() {
let mut cs = ConstraintSet::new();
cs.add(FieldConstraint::new("/a", ["IBAN_CHECK"]));
cs.add(FieldConstraint::new("/b", ["BIC_CHECK"]));
cs.add(FieldConstraint::new("/a", ["BIC_CHECK"]));
let a_constraints = cs.for_path("/a");
assert_eq!(a_constraints.len(), 2);
let b_constraints = cs.for_path("/b");
assert_eq!(b_constraints.len(), 1);
let c_constraints = cs.for_path("/c");
assert!(c_constraints.is_empty());
}
#[test]
fn len_and_is_empty() {
let mut cs = ConstraintSet::new();
assert!(cs.is_empty());
assert_eq!(cs.len(), 0);
cs.add(FieldConstraint::new("/a", ["R1"]));
assert!(!cs.is_empty());
assert_eq!(cs.len(), 1);
}
}