pub mod amount;
pub mod bic;
pub(crate) mod checkdigit;
pub mod country;
pub mod currency;
pub mod datetime;
pub mod iban;
pub mod lei;
pub mod length;
pub mod pattern;
use crate::error::ValidationError;
use std::collections::BTreeMap;
pub trait Rule: Send + Sync {
fn id(&self) -> &str;
fn validate(&self, value: &str, path: &str) -> Vec<ValidationError>;
}
pub struct RuleRegistry {
rules: BTreeMap<String, Box<dyn Rule>>,
}
impl RuleRegistry {
pub fn new() -> Self {
Self {
rules: BTreeMap::new(),
}
}
pub fn with_defaults() -> Self {
let mut registry = Self::new();
registry.register(Box::new(iban::IbanRule));
registry.register(Box::new(bic::BicRule));
registry.register(Box::new(currency::CurrencyRule));
registry.register(Box::new(country::CountryCodeRule));
registry.register(Box::new(lei::LeiRule));
registry.register(Box::new(amount::AmountFormatRule));
registry.register(Box::new(datetime::IsoDateTimeRule));
registry.register(Box::new(datetime::IsoDateRule));
registry
}
pub fn register(&mut self, rule: Box<dyn Rule>) {
self.rules.insert(rule.id().to_owned(), rule);
}
pub fn get(&self, rule_id: &str) -> Option<&dyn Rule> {
self.rules.get(rule_id).map(std::convert::AsRef::as_ref)
}
pub fn validate_field(
&self,
value: &str,
path: &str,
rule_ids: &[&str],
) -> Vec<ValidationError> {
rule_ids
.iter()
.filter_map(|id| self.rules.get(*id))
.flat_map(|rule| rule.validate(value, path))
.collect()
}
}
impl Default for RuleRegistry {
fn default() -> Self {
Self::with_defaults()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Severity;
struct AlwaysFailRule;
impl Rule for AlwaysFailRule {
fn id(&self) -> &'static str {
"ALWAYS_FAIL"
}
fn validate(&self, _value: &str, path: &str) -> Vec<ValidationError> {
vec![ValidationError::new(
path,
Severity::Error,
"ALWAYS_FAIL",
"always fails",
)]
}
}
#[test]
fn empty_registry_produces_no_errors() {
let registry = RuleRegistry::new();
let errors = registry.validate_field("any", "/p", &["IBAN_CHECK"]);
assert!(errors.is_empty());
}
#[test]
fn registered_rule_is_invoked() {
let mut registry = RuleRegistry::new();
registry.register(Box::new(AlwaysFailRule));
let errors = registry.validate_field("any", "/p", &["ALWAYS_FAIL"]);
assert_eq!(errors.len(), 1);
}
#[test]
fn unknown_rule_id_is_skipped() {
let registry = RuleRegistry::with_defaults();
let errors = registry.validate_field("any", "/p", &["NO_SUCH_RULE"]);
assert!(errors.is_empty());
}
#[test]
fn with_defaults_includes_iban_check() {
let registry = RuleRegistry::with_defaults();
assert!(registry.get("IBAN_CHECK").is_some());
}
#[test]
fn with_defaults_includes_bic_check() {
let registry = RuleRegistry::with_defaults();
assert!(registry.get("BIC_CHECK").is_some());
}
#[test]
fn with_defaults_includes_all_new_rules() {
let registry = RuleRegistry::with_defaults();
assert!(registry.get("CURRENCY_CHECK").is_some());
assert!(registry.get("COUNTRY_CHECK").is_some());
assert!(registry.get("LEI_CHECK").is_some());
assert!(registry.get("AMOUNT_FORMAT").is_some());
assert!(registry.get("DATETIME_CHECK").is_some());
assert!(registry.get("DATE_CHECK").is_some());
}
#[test]
fn registering_replaces_existing_rule() {
let mut registry = RuleRegistry::new();
registry.register(Box::new(AlwaysFailRule));
registry.register(Box::new(AlwaysFailRule)); let errors = registry.validate_field("any", "/p", &["ALWAYS_FAIL"]);
assert_eq!(errors.len(), 1);
}
#[test]
fn validate_field_with_multiple_rules() {
let mut registry = RuleRegistry::new();
registry.register(Box::new(AlwaysFailRule));
registry.register(Box::new(iban::IbanRule));
let errors = registry.validate_field("NOTANIBAN", "/p", &["ALWAYS_FAIL", "IBAN_CHECK"]);
assert_eq!(errors.len(), 2);
}
#[test]
fn default_registry_is_with_defaults() {
let registry = RuleRegistry::default();
assert!(registry.get("IBAN_CHECK").is_some());
assert!(registry.get("BIC_CHECK").is_some());
}
#[test]
fn valid_iban_through_registry() {
let registry = RuleRegistry::with_defaults();
let errors = registry.validate_field("GB82WEST12345698765432", "/path", &["IBAN_CHECK"]);
assert!(errors.is_empty());
}
#[test]
fn valid_bic_through_registry() {
let registry = RuleRegistry::with_defaults();
let errors = registry.validate_field("AAAAGB2L", "/path", &["BIC_CHECK"]);
assert!(errors.is_empty());
}
}