Skip to main content

fraiseql_core/validation/
validators.rs

1//! Basic field validators for input validation.
2//!
3//! Provides simple validators for patterns, lengths, numeric ranges, and enums.
4//! These validators are combined to create comprehensive input validation rules.
5
6use regex::Regex;
7
8use super::rules::{LengthCheck, ValidationRule};
9use crate::error::{FraiseQLError, Result, ValidationFieldError};
10
11/// Basic validator trait for field validation.
12pub trait Validator {
13    /// Validate a value and return an error if validation fails.
14    ///
15    /// # Errors
16    ///
17    /// Returns `FraiseQLError::Validation` if the value does not pass the validation check.
18    fn validate(&self, value: &str, field: &str) -> Result<()>;
19}
20
21/// Pattern validator using regular expressions.
22pub struct PatternValidator {
23    regex:   Regex,
24    message: String,
25}
26
27impl PatternValidator {
28    /// Create a new pattern validator.
29    ///
30    /// # Errors
31    /// Returns error if the regex pattern is invalid.
32    pub fn new(pattern: impl Into<String>, message: impl Into<String>) -> Result<Self> {
33        let pattern_str = pattern.into();
34        let regex = Regex::new(&pattern_str)
35            .map_err(|e| FraiseQLError::validation(format!("Invalid regex pattern: {}", e)))?;
36        Ok(Self {
37            regex,
38            message: message.into(),
39        })
40    }
41
42    /// Create a new pattern validator with default message.
43    ///
44    /// # Errors
45    ///
46    /// Returns `FraiseQLError::Validation` if the regex pattern is invalid.
47    pub fn new_default_message(pattern: impl Into<String>) -> Result<Self> {
48        let pattern_str = pattern.into();
49        Self::new(pattern_str.clone(), format!("Value must match pattern: {}", pattern_str))
50    }
51
52    /// Build a pattern validator from an already-compiled regex.
53    ///
54    /// Used by `create_validator_from_rule` when the [`ValidationRule::Pattern`]
55    /// variant has already compiled the pattern at construction time, so we
56    /// avoid a redundant `Regex::new` call here.
57    #[must_use]
58    pub fn from_compiled(regex: Regex, message: impl Into<String>) -> Self {
59        Self {
60            regex,
61            message: message.into(),
62        }
63    }
64
65    /// Validate that a value matches the pattern.
66    #[must_use]
67    pub fn validate_pattern(&self, value: &str) -> bool {
68        self.regex.is_match(value)
69    }
70}
71
72impl Validator for PatternValidator {
73    fn validate(&self, value: &str, field: &str) -> Result<()> {
74        if self.validate_pattern(value) {
75            Ok(())
76        } else {
77            Err(FraiseQLError::Validation {
78                message: format!(
79                    "Field validation failed: {}",
80                    ValidationFieldError::new(field, "pattern", &self.message)
81                ),
82                path:    Some(field.to_string()),
83            })
84        }
85    }
86}
87
88/// String length validator.
89pub struct LengthValidator {
90    min: Option<usize>,
91    max: Option<usize>,
92}
93
94impl LengthValidator {
95    /// Create a new length validator.
96    #[must_use]
97    pub const fn new(min: Option<usize>, max: Option<usize>) -> Self {
98        Self { min, max }
99    }
100
101    /// Validate that a string is within the specified length bounds.
102    #[must_use]
103    pub fn validate_length(&self, value: &str) -> bool {
104        crate::validation::check_length(value, self.min, self.max) == LengthCheck::Ok
105    }
106
107    /// Get a descriptive error message for length validation failure.
108    #[must_use]
109    pub fn error_message(&self) -> String {
110        match (self.min, self.max) {
111            (Some(m), Some(x)) => format!("Length must be between {} and {}", m, x),
112            (Some(m), None) => format!("Length must be at least {}", m),
113            (None, Some(x)) => format!("Length must be at most {}", x),
114            (None, None) => "Length validation failed".to_string(),
115        }
116    }
117}
118
119impl Validator for LengthValidator {
120    fn validate(&self, value: &str, field: &str) -> Result<()> {
121        if self.validate_length(value) {
122            Ok(())
123        } else {
124            Err(FraiseQLError::Validation {
125                message: format!(
126                    "Field validation failed: {}",
127                    ValidationFieldError::new(field, "length", self.error_message())
128                ),
129                path:    Some(field.to_string()),
130            })
131        }
132    }
133}
134
135/// Numeric range validator.
136pub struct RangeValidator {
137    min: Option<i64>,
138    max: Option<i64>,
139}
140
141impl RangeValidator {
142    /// Create a new range validator.
143    #[must_use]
144    pub const fn new(min: Option<i64>, max: Option<i64>) -> Self {
145        Self { min, max }
146    }
147
148    /// Validate that a number is within the specified range.
149    #[must_use]
150    pub const fn validate_range(&self, value: i64) -> bool {
151        if let Some(min) = self.min {
152            if value < min {
153                return false;
154            }
155        }
156        if let Some(max) = self.max {
157            if value > max {
158                return false;
159            }
160        }
161        true
162    }
163
164    /// Get a descriptive error message for range validation failure.
165    #[must_use]
166    pub fn error_message(&self) -> String {
167        match (self.min, self.max) {
168            (Some(m), Some(x)) => format!("Value must be between {} and {}", m, x),
169            (Some(m), None) => format!("Value must be at least {}", m),
170            (None, Some(x)) => format!("Value must be at most {}", x),
171            (None, None) => "Range validation failed".to_string(),
172        }
173    }
174}
175
176/// Enum validator - allows only specified values.
177pub struct EnumValidator {
178    allowed_values: std::collections::HashSet<String>,
179}
180
181impl EnumValidator {
182    /// Create a new enum validator.
183    #[must_use]
184    pub fn new(values: Vec<String>) -> Self {
185        Self {
186            allowed_values: values.into_iter().collect(),
187        }
188    }
189
190    /// Validate that a value is in the allowed set.
191    #[must_use]
192    pub fn validate_enum(&self, value: &str) -> bool {
193        self.allowed_values.contains(value)
194    }
195
196    /// Get the list of allowed values.
197    #[must_use]
198    pub fn allowed_values(&self) -> Vec<&str> {
199        self.allowed_values.iter().map(|s| s.as_str()).collect()
200    }
201}
202
203impl Validator for EnumValidator {
204    fn validate(&self, value: &str, field: &str) -> Result<()> {
205        if self.validate_enum(value) {
206            Ok(())
207        } else {
208            let mut allowed_vec: Vec<_> = self.allowed_values.iter().cloned().collect();
209            allowed_vec.sort();
210            let allowed = allowed_vec.join(", ");
211            Err(FraiseQLError::Validation {
212                message: format!(
213                    "Field validation failed: {}",
214                    ValidationFieldError::new(
215                        field,
216                        "enum",
217                        format!("Must be one of: {}", allowed)
218                    )
219                ),
220                path:    Some(field.to_string()),
221            })
222        }
223    }
224}
225
226/// Required field validator.
227pub struct RequiredValidator;
228
229impl Validator for RequiredValidator {
230    fn validate(&self, value: &str, field: &str) -> Result<()> {
231        if value.is_empty() {
232            Err(FraiseQLError::Validation {
233                message: format!(
234                    "Field validation failed: {}",
235                    ValidationFieldError::new(field, "required", "Field is required")
236                ),
237                path:    Some(field.to_string()),
238            })
239        } else {
240            Ok(())
241        }
242    }
243}
244
245/// RFC 5321 practical email regex, shared with `async_validators`.
246const EMAIL_PATTERN: &str = r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$";
247
248/// E.164 international phone number regex, shared with `async_validators`.
249const PHONE_E164_PATTERN: &str = r"^\+[1-9]\d{6,14}$";
250
251/// Create a validator from a `ValidationRule`.
252///
253/// Returns `None` for rule types that are handled elsewhere (e.g. cross-field,
254/// composite, or async validators). The `Pattern` variant carries an
255/// already-compiled `Regex`, so this function does not re-invoke the regex
256/// engine.
257#[must_use]
258pub fn create_validator_from_rule(rule: &ValidationRule) -> Option<Box<dyn Validator>> {
259    match rule {
260        ValidationRule::Pattern { pattern, message } => {
261            // The pattern was already compiled into a `Regex` at rule
262            // construction time, so we can build the validator directly
263            // without re-invoking the regex engine.
264            let msg = message.clone().unwrap_or_else(|| "Pattern mismatch".to_string());
265            Some(Box::new(PatternValidator::from_compiled(pattern.regex().clone(), msg))
266                as Box<dyn Validator>)
267        },
268        ValidationRule::Length { min, max } => {
269            Some(Box::new(LengthValidator::new(*min, *max)) as Box<dyn Validator>)
270        },
271        ValidationRule::Enum { values } => {
272            Some(Box::new(EnumValidator::new(values.clone())) as Box<dyn Validator>)
273        },
274        ValidationRule::Required => Some(Box::new(RequiredValidator) as Box<dyn Validator>),
275        ValidationRule::Email => {
276            // Reuse the same regex as EmailFormatValidator in async_validators.
277            PatternValidator::new(EMAIL_PATTERN, "Invalid email address format")
278                .ok()
279                .map(|v| Box::new(v) as Box<dyn Validator>)
280        },
281        ValidationRule::Phone => {
282            // Reuse the same regex as PhoneE164Validator in async_validators.
283            PatternValidator::new(
284                PHONE_E164_PATTERN,
285                "Invalid E.164 phone number (expected +<country><number>)",
286            )
287            .ok()
288            .map(|v| Box::new(v) as Box<dyn Validator>)
289        },
290        _ => None, // Other validators handled separately (cross-field, composite, async)
291    }
292}