Skip to main content

validation_core/
set.rs

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}