1use crate::{ValidationConfig, ValidationMergeError, ValidationSettings};
2use serde::{Deserialize, Serialize};
3use thiserror::Error;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct ValidationRule {
7 pub name: String,
8 pub description: Option<String>,
9 pub settings: ValidationSettings,
10}
11
12impl ValidationRule {
13 pub fn resolve(&self) -> ValidationConfig {
14 self.settings.resolve()
15 }
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ValidationSet {
20 pub name: String,
21 pub description: Option<String>,
22 pub items: Vec<ValidationSetItem>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub enum ValidationSetItem {
27 GlobalRuleRef(String),
28 InlineRule {
29 name: Option<String>,
30 validation: ValidationSettings,
31 },
32}
33
34impl ValidationSet {
35 pub fn resolve_settings_with_rules<'a>(
36 &'a self,
37 rules: impl Fn(&str) -> Option<&'a ValidationRule>,
38 ) -> Result<ValidationSettings, ValidationSetResolveError> {
39 let settings = self.items.iter().map(|item| match item {
40 ValidationSetItem::GlobalRuleRef(name) => {
41 rules(name).map(|rule| &rule.settings).ok_or_else(|| {
42 ValidationSetResolveError::MissingGlobalRule { name: name.clone() }
43 })
44 }
45 ValidationSetItem::InlineRule { validation, .. } => Ok(validation),
46 });
47
48 let settings = settings.collect::<Result<Vec<_>, _>>()?;
49 Ok(ValidationSettings::merge_rules(settings)?)
50 }
51
52 pub fn resolve_with_rules<'a>(
53 &'a self,
54 rules: impl Fn(&str) -> Option<&'a ValidationRule>,
55 ) -> Result<ValidationConfig, ValidationSetResolveError> {
56 Ok(self.resolve_settings_with_rules(rules)?.resolve())
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Error)]
61pub enum ValidationSetResolveError {
62 #[error("validation set references missing global rule '{name}'")]
63 MissingGlobalRule { name: String },
64 #[error(transparent)]
65 Merge(#[from] ValidationMergeError),
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct AppliedValidation {
70 pub set_name: Option<String>,
71 pub settings: ValidationSettings,
72}
73
74impl AppliedValidation {
75 pub fn resolve(&self) -> ValidationConfig {
76 self.settings.resolve()
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use crate::{
84 CharacterFilterSettings, CharacterLimits, PatternSettings, PositionFilterSettings,
85 PositionRange,
86 };
87
88 #[test]
89 fn validation_set_merges_rule_fragments() {
90 let set = ValidationSet {
91 name: "phone".to_string(),
92 description: None,
93 items: vec![
94 ValidationSetItem::InlineRule {
95 name: Some("phone-length".to_string()),
96 validation: ValidationSettings {
97 character_limits: Some(CharacterLimits::new_range(10, 15)),
98 ..ValidationSettings::default()
99 },
100 },
101 ValidationSetItem::InlineRule {
102 name: Some("digits-only".to_string()),
103 validation: ValidationSettings {
104 pattern: Some(PatternSettings {
105 filters: vec![PositionFilterSettings {
106 positions: PositionRange::From(0),
107 filter: CharacterFilterSettings::Numeric,
108 }],
109 description: None,
110 }),
111 ..ValidationSettings::default()
112 },
113 },
114 ],
115 };
116
117 let settings = set
118 .resolve_settings_with_rules(|_| None)
119 .expect("set should resolve");
120
121 assert!(settings.character_limits.is_some());
122 assert_eq!(settings.pattern.expect("pattern").filters.len(), 1);
123 }
124
125 #[test]
126 fn validation_set_rejects_duplicate_singleton_rules() {
127 let set = ValidationSet {
128 name: "conflict".to_string(),
129 description: None,
130 items: vec![
131 ValidationSetItem::InlineRule {
132 name: Some("short".to_string()),
133 validation: ValidationSettings {
134 character_limits: Some(CharacterLimits::new(10)),
135 ..ValidationSettings::default()
136 },
137 },
138 ValidationSetItem::InlineRule {
139 name: Some("long".to_string()),
140 validation: ValidationSettings {
141 character_limits: Some(CharacterLimits::new(20)),
142 ..ValidationSettings::default()
143 },
144 },
145 ],
146 };
147
148 assert!(set.resolve_settings_with_rules(|_| None).is_err());
149 }
150}