use crate::{ValidationConfig, ValidationMergeError, ValidationSettings}; use serde::{Deserialize, Serialize}; use thiserror::Error; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ValidationRule { pub name: String, pub description: Option, pub settings: ValidationSettings, } impl ValidationRule { pub fn resolve(&self) -> ValidationConfig { self.settings.resolve() } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ValidationSet { pub name: String, pub description: Option, pub items: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ValidationSetItem { GlobalRuleRef(String), InlineRule { name: Option, validation: ValidationSettings, }, } impl ValidationSet { pub fn resolve_settings_with_rules<'a>( &'a self, rules: impl Fn(&str) -> Option<&'a ValidationRule>, ) -> Result { let settings = self.items.iter().map(|item| match item { ValidationSetItem::GlobalRuleRef(name) => { rules(name).map(|rule| &rule.settings).ok_or_else(|| { ValidationSetResolveError::MissingGlobalRule { name: name.clone() } }) } ValidationSetItem::InlineRule { validation, .. } => Ok(validation), }); let settings = settings.collect::, _>>()?; Ok(ValidationSettings::merge_rules(settings)?) } pub fn resolve_with_rules<'a>( &'a self, rules: impl Fn(&str) -> Option<&'a ValidationRule>, ) -> Result { Ok(self.resolve_settings_with_rules(rules)?.resolve()) } } #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum ValidationSetResolveError { #[error("validation set references missing global rule '{name}'")] MissingGlobalRule { name: String }, #[error(transparent)] Merge(#[from] ValidationMergeError), } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppliedValidation { pub set_name: Option, pub settings: ValidationSettings, } impl AppliedValidation { pub fn resolve(&self) -> ValidationConfig { self.settings.resolve() } } #[cfg(test)] mod tests { use super::*; use crate::{ CharacterFilterSettings, CharacterLimits, PatternSettings, PositionFilterSettings, PositionRange, }; #[test] fn validation_set_merges_rule_fragments() { let set = ValidationSet { name: "phone".to_string(), description: None, items: vec![ ValidationSetItem::InlineRule { name: Some("phone-length".to_string()), validation: ValidationSettings { character_limits: Some(CharacterLimits::new_range(10, 15)), ..ValidationSettings::default() }, }, ValidationSetItem::InlineRule { name: Some("digits-only".to_string()), validation: ValidationSettings { pattern: Some(PatternSettings { filters: vec![PositionFilterSettings { positions: PositionRange::From(0), filter: CharacterFilterSettings::Numeric, }], description: None, }), ..ValidationSettings::default() }, }, ], }; let settings = set .resolve_settings_with_rules(|_| None) .expect("set should resolve"); assert!(settings.character_limits.is_some()); assert_eq!(settings.pattern.expect("pattern").filters.len(), 1); } #[test] fn validation_set_rejects_duplicate_singleton_rules() { let set = ValidationSet { name: "conflict".to_string(), description: None, items: vec![ ValidationSetItem::InlineRule { name: Some("short".to_string()), validation: ValidationSettings { character_limits: Some(CharacterLimits::new(10)), ..ValidationSettings::default() }, }, ValidationSetItem::InlineRule { name: Some("long".to_string()), validation: ValidationSettings { character_limits: Some(CharacterLimits::new(20)), ..ValidationSettings::default() }, }, ], }; assert!(set.resolve_settings_with_rules(|_| None).is_err()); } }