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::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 const fn validate_length(&self, value: &str) -> bool {
104        let len = value.len();
105        if let Some(min) = self.min {
106            if len < min {
107                return false;
108            }
109        }
110        if let Some(max) = self.max {
111            if len > max {
112                return false;
113            }
114        }
115        true
116    }
117
118    /// Get a descriptive error message for length validation failure.
119    #[must_use]
120    pub fn error_message(&self) -> String {
121        match (self.min, self.max) {
122            (Some(m), Some(x)) => format!("Length must be between {} and {}", m, x),
123            (Some(m), None) => format!("Length must be at least {}", m),
124            (None, Some(x)) => format!("Length must be at most {}", x),
125            (None, None) => "Length validation failed".to_string(),
126        }
127    }
128}
129
130impl Validator for LengthValidator {
131    fn validate(&self, value: &str, field: &str) -> Result<()> {
132        if self.validate_length(value) {
133            Ok(())
134        } else {
135            Err(FraiseQLError::Validation {
136                message: format!(
137                    "Field validation failed: {}",
138                    ValidationFieldError::new(field, "length", self.error_message())
139                ),
140                path:    Some(field.to_string()),
141            })
142        }
143    }
144}
145
146/// Numeric range validator.
147pub struct RangeValidator {
148    min: Option<i64>,
149    max: Option<i64>,
150}
151
152impl RangeValidator {
153    /// Create a new range validator.
154    #[must_use]
155    pub const fn new(min: Option<i64>, max: Option<i64>) -> Self {
156        Self { min, max }
157    }
158
159    /// Validate that a number is within the specified range.
160    #[must_use]
161    pub const fn validate_range(&self, value: i64) -> bool {
162        if let Some(min) = self.min {
163            if value < min {
164                return false;
165            }
166        }
167        if let Some(max) = self.max {
168            if value > max {
169                return false;
170            }
171        }
172        true
173    }
174
175    /// Get a descriptive error message for range validation failure.
176    #[must_use]
177    pub fn error_message(&self) -> String {
178        match (self.min, self.max) {
179            (Some(m), Some(x)) => format!("Value must be between {} and {}", m, x),
180            (Some(m), None) => format!("Value must be at least {}", m),
181            (None, Some(x)) => format!("Value must be at most {}", x),
182            (None, None) => "Range validation failed".to_string(),
183        }
184    }
185}
186
187/// Enum validator - allows only specified values.
188pub struct EnumValidator {
189    allowed_values: std::collections::HashSet<String>,
190}
191
192impl EnumValidator {
193    /// Create a new enum validator.
194    #[must_use]
195    pub fn new(values: Vec<String>) -> Self {
196        Self {
197            allowed_values: values.into_iter().collect(),
198        }
199    }
200
201    /// Validate that a value is in the allowed set.
202    #[must_use]
203    pub fn validate_enum(&self, value: &str) -> bool {
204        self.allowed_values.contains(value)
205    }
206
207    /// Get the list of allowed values.
208    #[must_use]
209    pub fn allowed_values(&self) -> Vec<&str> {
210        self.allowed_values.iter().map(|s| s.as_str()).collect()
211    }
212}
213
214impl Validator for EnumValidator {
215    fn validate(&self, value: &str, field: &str) -> Result<()> {
216        if self.validate_enum(value) {
217            Ok(())
218        } else {
219            let mut allowed_vec: Vec<_> = self.allowed_values.iter().cloned().collect();
220            allowed_vec.sort();
221            let allowed = allowed_vec.join(", ");
222            Err(FraiseQLError::Validation {
223                message: format!(
224                    "Field validation failed: {}",
225                    ValidationFieldError::new(
226                        field,
227                        "enum",
228                        format!("Must be one of: {}", allowed)
229                    )
230                ),
231                path:    Some(field.to_string()),
232            })
233        }
234    }
235}
236
237/// Required field validator.
238pub struct RequiredValidator;
239
240impl Validator for RequiredValidator {
241    fn validate(&self, value: &str, field: &str) -> Result<()> {
242        if value.is_empty() {
243            Err(FraiseQLError::Validation {
244                message: format!(
245                    "Field validation failed: {}",
246                    ValidationFieldError::new(field, "required", "Field is required")
247                ),
248                path:    Some(field.to_string()),
249            })
250        } else {
251            Ok(())
252        }
253    }
254}
255
256/// RFC 5321 practical email regex, shared with `async_validators`.
257const 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])?)+$";
258
259/// E.164 international phone number regex, shared with `async_validators`.
260const PHONE_E164_PATTERN: &str = r"^\+[1-9]\d{6,14}$";
261
262/// Create a validator from a `ValidationRule`.
263///
264/// Returns `None` for rule types that are handled elsewhere (e.g. cross-field,
265/// composite, or async validators). The `Pattern` variant carries an
266/// already-compiled `Regex`, so this function does not re-invoke the regex
267/// engine.
268#[must_use]
269pub fn create_validator_from_rule(rule: &ValidationRule) -> Option<Box<dyn Validator>> {
270    match rule {
271        ValidationRule::Pattern { pattern, message } => {
272            // The pattern was already compiled into a `Regex` at rule
273            // construction time, so we can build the validator directly
274            // without re-invoking the regex engine.
275            let msg = message.clone().unwrap_or_else(|| "Pattern mismatch".to_string());
276            Some(Box::new(PatternValidator::from_compiled(pattern.regex().clone(), msg))
277                as Box<dyn Validator>)
278        },
279        ValidationRule::Length { min, max } => {
280            Some(Box::new(LengthValidator::new(*min, *max)) as Box<dyn Validator>)
281        },
282        ValidationRule::Enum { values } => {
283            Some(Box::new(EnumValidator::new(values.clone())) as Box<dyn Validator>)
284        },
285        ValidationRule::Required => Some(Box::new(RequiredValidator) as Box<dyn Validator>),
286        ValidationRule::Email => {
287            // Reuse the same regex as EmailFormatValidator in async_validators.
288            PatternValidator::new(EMAIL_PATTERN, "Invalid email address format")
289                .ok()
290                .map(|v| Box::new(v) as Box<dyn Validator>)
291        },
292        ValidationRule::Phone => {
293            // Reuse the same regex as PhoneE164Validator in async_validators.
294            PatternValidator::new(
295                PHONE_E164_PATTERN,
296                "Invalid E.164 phone number (expected +<country><number>)",
297            )
298            .ok()
299            .map(|v| Box::new(v) as Box<dyn Validator>)
300        },
301        _ => None, // Other validators handled separately (cross-field, composite, async)
302    }
303}