fraiseql_core/validation/
validators.rs1use regex::Regex;
7
8use super::rules::ValidationRule;
9use crate::error::{FraiseQLError, Result, ValidationFieldError};
10
11pub trait Validator {
13 fn validate(&self, value: &str, field: &str) -> Result<()>;
19}
20
21pub struct PatternValidator {
23 regex: Regex,
24 message: String,
25}
26
27impl PatternValidator {
28 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 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 pub fn validate_pattern(&self, value: &str) -> bool {
54 self.regex.is_match(value)
55 }
56}
57
58impl Validator for PatternValidator {
59 fn validate(&self, value: &str, field: &str) -> Result<()> {
60 if self.validate_pattern(value) {
61 Ok(())
62 } else {
63 Err(FraiseQLError::Validation {
64 message: format!(
65 "Field validation failed: {}",
66 ValidationFieldError::new(field, "pattern", &self.message)
67 ),
68 path: Some(field.to_string()),
69 })
70 }
71 }
72}
73
74pub struct LengthValidator {
76 min: Option<usize>,
77 max: Option<usize>,
78}
79
80impl LengthValidator {
81 pub const fn new(min: Option<usize>, max: Option<usize>) -> Self {
83 Self { min, max }
84 }
85
86 pub const fn validate_length(&self, value: &str) -> bool {
88 let len = value.len();
89 if let Some(min) = self.min {
90 if len < min {
91 return false;
92 }
93 }
94 if let Some(max) = self.max {
95 if len > max {
96 return false;
97 }
98 }
99 true
100 }
101
102 pub fn error_message(&self) -> String {
104 match (self.min, self.max) {
105 (Some(m), Some(x)) => format!("Length must be between {} and {}", m, x),
106 (Some(m), None) => format!("Length must be at least {}", m),
107 (None, Some(x)) => format!("Length must be at most {}", x),
108 (None, None) => "Length validation failed".to_string(),
109 }
110 }
111}
112
113impl Validator for LengthValidator {
114 fn validate(&self, value: &str, field: &str) -> Result<()> {
115 if self.validate_length(value) {
116 Ok(())
117 } else {
118 Err(FraiseQLError::Validation {
119 message: format!(
120 "Field validation failed: {}",
121 ValidationFieldError::new(field, "length", self.error_message())
122 ),
123 path: Some(field.to_string()),
124 })
125 }
126 }
127}
128
129pub struct RangeValidator {
131 min: Option<i64>,
132 max: Option<i64>,
133}
134
135impl RangeValidator {
136 pub const fn new(min: Option<i64>, max: Option<i64>) -> Self {
138 Self { min, max }
139 }
140
141 pub const fn validate_range(&self, value: i64) -> bool {
143 if let Some(min) = self.min {
144 if value < min {
145 return false;
146 }
147 }
148 if let Some(max) = self.max {
149 if value > max {
150 return false;
151 }
152 }
153 true
154 }
155
156 pub fn error_message(&self) -> String {
158 match (self.min, self.max) {
159 (Some(m), Some(x)) => format!("Value must be between {} and {}", m, x),
160 (Some(m), None) => format!("Value must be at least {}", m),
161 (None, Some(x)) => format!("Value must be at most {}", x),
162 (None, None) => "Range validation failed".to_string(),
163 }
164 }
165}
166
167pub struct EnumValidator {
169 allowed_values: std::collections::HashSet<String>,
170}
171
172impl EnumValidator {
173 pub fn new(values: Vec<String>) -> Self {
175 Self {
176 allowed_values: values.into_iter().collect(),
177 }
178 }
179
180 pub fn validate_enum(&self, value: &str) -> bool {
182 self.allowed_values.contains(value)
183 }
184
185 pub fn allowed_values(&self) -> Vec<&str> {
187 self.allowed_values.iter().map(|s| s.as_str()).collect()
188 }
189}
190
191impl Validator for EnumValidator {
192 fn validate(&self, value: &str, field: &str) -> Result<()> {
193 if self.validate_enum(value) {
194 Ok(())
195 } else {
196 let mut allowed_vec: Vec<_> = self.allowed_values.iter().cloned().collect();
197 allowed_vec.sort();
198 let allowed = allowed_vec.join(", ");
199 Err(FraiseQLError::Validation {
200 message: format!(
201 "Field validation failed: {}",
202 ValidationFieldError::new(
203 field,
204 "enum",
205 format!("Must be one of: {}", allowed)
206 )
207 ),
208 path: Some(field.to_string()),
209 })
210 }
211 }
212}
213
214pub struct RequiredValidator;
216
217impl Validator for RequiredValidator {
218 fn validate(&self, value: &str, field: &str) -> Result<()> {
219 if value.is_empty() {
220 Err(FraiseQLError::Validation {
221 message: format!(
222 "Field validation failed: {}",
223 ValidationFieldError::new(field, "required", "Field is required")
224 ),
225 path: Some(field.to_string()),
226 })
227 } else {
228 Ok(())
229 }
230 }
231}
232
233const 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])?)+$";
235
236const PHONE_E164_PATTERN: &str = r"^\+[1-9]\d{6,14}$";
238
239pub fn create_validator_from_rule(rule: &ValidationRule) -> Option<Box<dyn Validator>> {
245 match rule {
246 ValidationRule::Pattern { pattern, message } => {
247 let msg = message.clone().unwrap_or_else(|| "Pattern mismatch".to_string());
248 match PatternValidator::new(pattern.clone(), msg) {
249 Ok(v) => Some(Box::new(v) as Box<dyn Validator>),
250 Err(e) => {
251 tracing::warn!(
252 pattern = %pattern,
253 error = %e,
254 "Invalid regex in ValidationRule::Pattern — validator skipped"
255 );
256 None
257 },
258 }
259 },
260 ValidationRule::Length { min, max } => {
261 Some(Box::new(LengthValidator::new(*min, *max)) as Box<dyn Validator>)
262 },
263 ValidationRule::Enum { values } => {
264 Some(Box::new(EnumValidator::new(values.clone())) as Box<dyn Validator>)
265 },
266 ValidationRule::Required => Some(Box::new(RequiredValidator) as Box<dyn Validator>),
267 ValidationRule::Email => {
268 PatternValidator::new(EMAIL_PATTERN, "Invalid email address format")
270 .ok()
271 .map(|v| Box::new(v) as Box<dyn Validator>)
272 },
273 ValidationRule::Phone => {
274 PatternValidator::new(
276 PHONE_E164_PATTERN,
277 "Invalid E.164 phone number (expected +<country><number>)",
278 )
279 .ok()
280 .map(|v| Box::new(v) as Box<dyn Validator>)
281 },
282 _ => None, }
284}
285
286#[cfg(test)]
287mod tests {
288 #![allow(clippy::unwrap_used)] use super::*;
291
292 #[test]
293 fn test_pattern_validator() {
294 let validator = PatternValidator::new_default_message("^[a-z]+$").unwrap();
295 assert!(validator.validate_pattern("hello"));
296 assert!(!validator.validate_pattern("Hello"));
297 assert!(!validator.validate_pattern("hello123"));
298 }
299
300 #[test]
301 fn test_pattern_validator_validation() {
302 let validator = PatternValidator::new_default_message("^[a-z]+$").unwrap();
303 validator
304 .validate("hello", "name")
305 .unwrap_or_else(|e| panic!("lowercase-only string should pass pattern: {e}"));
306 assert!(
307 matches!(validator.validate("Hello", "name"), Err(FraiseQLError::Validation { .. })),
308 "mixed-case string should fail pattern with Validation error"
309 );
310 }
311
312 #[test]
313 fn test_length_validator() {
314 let validator = LengthValidator::new(Some(3), Some(10));
315 assert!(validator.validate_length("hello"));
316 assert!(!validator.validate_length("ab"));
317 assert!(!validator.validate_length("this is too long"));
318 }
319
320 #[test]
321 fn test_length_validator_error_message() {
322 let validator = LengthValidator::new(Some(5), Some(10));
323 let msg = validator.error_message();
324 assert!(msg.contains('5'));
325 assert!(msg.contains("10"));
326 }
327
328 #[test]
329 fn test_range_validator() {
330 let validator = RangeValidator::new(Some(0), Some(100));
331 assert!(validator.validate_range(50));
332 assert!(!validator.validate_range(-1));
333 assert!(!validator.validate_range(101));
334 }
335
336 #[test]
337 fn test_enum_validator() {
338 let validator = EnumValidator::new(vec![
339 "active".to_string(),
340 "inactive".to_string(),
341 "pending".to_string(),
342 ]);
343 assert!(validator.validate_enum("active"));
344 assert!(!validator.validate_enum("unknown"));
345 }
346
347 #[test]
348 fn test_required_validator() {
349 let validator = RequiredValidator;
350 validator
351 .validate("hello", "name")
352 .unwrap_or_else(|e| panic!("non-empty string should pass required validator: {e}"));
353 assert!(
354 matches!(validator.validate("", "name"), Err(FraiseQLError::Validation { .. })),
355 "empty string should fail required validator with Validation error"
356 );
357 }
358
359 #[test]
360 fn test_create_validator_from_rule() {
361 let rule = ValidationRule::Pattern {
362 pattern: "^test".to_string(),
363 message: None,
364 };
365 let validator = create_validator_from_rule(&rule);
366 assert!(validator.is_some());
367 }
368}