Skip to main content

validation_core/
config.rs

1use crate::rules::{
2    CharacterFilter, CharacterLimits, DisplayMask, PatternFilters, PositionFilter, PositionRange,
3};
4use serde::{Deserialize, Serialize};
5use std::sync::Arc;
6use thiserror::Error;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct AllowedValues {
10    pub values: Vec<String>,
11    pub allow_empty: bool,
12    pub case_insensitive: bool,
13}
14
15impl AllowedValues {
16    pub fn new(values: Vec<String>) -> Self {
17        Self {
18            values,
19            allow_empty: true,
20            case_insensitive: false,
21        }
22    }
23
24    pub fn allow_empty(mut self, allow_empty: bool) -> Self {
25        self.allow_empty = allow_empty;
26        self
27    }
28
29    pub fn case_insensitive(mut self, case_insensitive: bool) -> Self {
30        self.case_insensitive = case_insensitive;
31        self
32    }
33
34    pub fn matches(&self, text: &str) -> bool {
35        if self.case_insensitive {
36            self.values
37                .iter()
38                .any(|allowed| allowed.eq_ignore_ascii_case(text))
39        } else {
40            self.values.iter().any(|allowed| allowed == text)
41        }
42    }
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub enum CharacterFilterSettings {
47    Alphabetic,
48    Numeric,
49    Alphanumeric,
50    Exact(char),
51    OneOf(Vec<char>),
52    Regex(String),
53}
54
55impl CharacterFilterSettings {
56    pub fn resolve(&self) -> CharacterFilter {
57        match self {
58            Self::Alphabetic => CharacterFilter::Alphabetic,
59            Self::Numeric => CharacterFilter::Numeric,
60            Self::Alphanumeric => CharacterFilter::Alphanumeric,
61            Self::Exact(ch) => CharacterFilter::Exact(*ch),
62            Self::OneOf(chars) => CharacterFilter::OneOf(chars.clone()),
63            Self::Regex(pattern) => {
64                #[cfg(feature = "regex")]
65                {
66                    match regex::Regex::new(pattern) {
67                        Ok(regex) => CharacterFilter::Custom(Arc::new(move |ch| {
68                            regex.is_match(&ch.to_string())
69                        })),
70                        Err(_) => CharacterFilter::Custom(Arc::new(|_| false)),
71                    }
72                }
73                #[cfg(not(feature = "regex"))]
74                {
75                    let _ = pattern;
76                    CharacterFilter::Custom(Arc::new(|_| false))
77                }
78            }
79        }
80    }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct PositionFilterSettings {
85    pub positions: PositionRange,
86    pub filter: CharacterFilterSettings,
87}
88
89impl PositionFilterSettings {
90    pub fn resolve(&self) -> PositionFilter {
91        PositionFilter::new(self.positions.clone(), self.filter.resolve())
92    }
93}
94
95#[derive(Debug, Clone, Default, Serialize, Deserialize)]
96pub struct PatternSettings {
97    pub filters: Vec<PositionFilterSettings>,
98    pub description: Option<String>,
99}
100
101impl PatternSettings {
102    pub fn resolve(&self) -> PatternFilters {
103        PatternFilters::new().add_filters(
104            self.filters
105                .iter()
106                .map(PositionFilterSettings::resolve)
107                .collect(),
108        )
109    }
110}
111
112#[derive(Debug, Clone, Default, Serialize, Deserialize)]
113pub struct ValidationSettings {
114    pub required: bool,
115    pub character_limits: Option<CharacterLimits>,
116    pub pattern: Option<PatternSettings>,
117    pub allowed_values: Option<AllowedValues>,
118    pub display_mask: Option<DisplayMask>,
119    pub external_validation_enabled: bool,
120}
121
122impl ValidationSettings {
123    pub fn resolve(&self) -> ValidationConfig {
124        ValidationConfig {
125            required: self.required,
126            character_limits: self.character_limits.clone(),
127            pattern_filters: self.pattern.as_ref().map(PatternSettings::resolve),
128            allowed_values: self.allowed_values.clone(),
129            display_mask: self.display_mask.clone(),
130            external_validation_enabled: self.external_validation_enabled,
131        }
132    }
133
134    pub fn merge_rules<'a>(
135        rules: impl IntoIterator<Item = &'a ValidationSettings>,
136    ) -> Result<Self, ValidationMergeError> {
137        let mut merged = ValidationSettings::default();
138
139        for rule in rules {
140            merged.merge_rule(rule)?;
141        }
142
143        Ok(merged)
144    }
145
146    pub fn merge_rule(&mut self, rule: &ValidationSettings) -> Result<(), ValidationMergeError> {
147        self.required |= rule.required;
148        self.external_validation_enabled |= rule.external_validation_enabled;
149
150        merge_singleton(
151            "character_limits",
152            &mut self.character_limits,
153            &rule.character_limits,
154        )?;
155        merge_singleton(
156            "allowed_values",
157            &mut self.allowed_values,
158            &rule.allowed_values,
159        )?;
160        merge_singleton("display_mask", &mut self.display_mask, &rule.display_mask)?;
161
162        if let Some(pattern) = &rule.pattern {
163            match &mut self.pattern {
164                Some(existing) => {
165                    existing.filters.extend(pattern.filters.clone());
166                    if existing.description.is_none() {
167                        existing.description = pattern.description.clone();
168                    }
169                }
170                None => self.pattern = Some(pattern.clone()),
171            }
172        }
173
174        Ok(())
175    }
176}
177
178fn merge_singleton<T: Clone>(
179    field_name: &'static str,
180    target: &mut Option<T>,
181    source: &Option<T>,
182) -> Result<(), ValidationMergeError> {
183    if let Some(source) = source {
184        if target.is_some() {
185            return Err(ValidationMergeError::DuplicateSingleton { field_name });
186        }
187
188        *target = Some(source.clone());
189    }
190
191    Ok(())
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Error)]
195pub enum ValidationMergeError {
196    #[error("validation set contains more than one rule configuring {field_name}")]
197    DuplicateSingleton { field_name: &'static str },
198}
199
200#[derive(Debug, Clone, Default)]
201pub struct ValidationConfig {
202    pub required: bool,
203    pub character_limits: Option<CharacterLimits>,
204    pub pattern_filters: Option<PatternFilters>,
205    pub allowed_values: Option<AllowedValues>,
206    pub display_mask: Option<DisplayMask>,
207    pub external_validation_enabled: bool,
208}
209
210impl ValidationConfig {
211    pub fn validate_content(&self, text: &str) -> ValidationResult {
212        if text.is_empty() {
213            if self.required {
214                return ValidationResult::error("Value required");
215            }
216
217            if let Some(allowed_values) = &self.allowed_values {
218                if !allowed_values.allow_empty {
219                    return ValidationResult::error("Empty value is not allowed");
220                }
221            }
222
223            return ValidationResult::Valid;
224        }
225
226        if let Some(limits) = &self.character_limits {
227            if let Some(result) = limits.validate_content(text) {
228                if !result.is_acceptable() {
229                    return result;
230                }
231            }
232        }
233
234        if let Some(pattern_filters) = &self.pattern_filters {
235            if let Err(message) = pattern_filters.validate_text(text) {
236                return ValidationResult::error(message);
237            }
238        }
239
240        if let Some(allowed_values) = &self.allowed_values {
241            if !allowed_values.matches(text) {
242                return ValidationResult::error("Value must be one of the allowed options");
243            }
244        }
245
246        ValidationResult::Valid
247    }
248
249    pub fn has_validation(&self) -> bool {
250        self.required
251            || self.character_limits.is_some()
252            || self.pattern_filters.is_some()
253            || self.allowed_values.is_some()
254            || self.display_mask.is_some()
255            || self.external_validation_enabled
256    }
257}
258
259#[derive(Debug, Clone, PartialEq, Eq)]
260pub enum ValidationResult {
261    Valid,
262    Warning { message: String },
263    Error { message: String },
264}
265
266impl ValidationResult {
267    pub fn is_acceptable(&self) -> bool {
268        matches!(self, Self::Valid | Self::Warning { .. })
269    }
270
271    pub fn is_error(&self) -> bool {
272        matches!(self, Self::Error { .. })
273    }
274
275    pub fn message(&self) -> Option<&str> {
276        match self {
277            Self::Valid => None,
278            Self::Warning { message } | Self::Error { message } => Some(message),
279        }
280    }
281
282    pub fn warning(message: impl Into<String>) -> Self {
283        Self::Warning {
284            message: message.into(),
285        }
286    }
287
288    pub fn error(message: impl Into<String>) -> Self {
289        Self::Error {
290            message: message.into(),
291        }
292    }
293}