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 #[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 #[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
88pub struct LengthValidator {
90 min: Option<usize>,
91 max: Option<usize>,
92}
93
94impl LengthValidator {
95 #[must_use]
97 pub const fn new(min: Option<usize>, max: Option<usize>) -> Self {
98 Self { min, max }
99 }
100
101 #[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 #[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
146pub struct RangeValidator {
148 min: Option<i64>,
149 max: Option<i64>,
150}
151
152impl RangeValidator {
153 #[must_use]
155 pub const fn new(min: Option<i64>, max: Option<i64>) -> Self {
156 Self { min, max }
157 }
158
159 #[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 #[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
187pub struct EnumValidator {
189 allowed_values: std::collections::HashSet<String>,
190}
191
192impl EnumValidator {
193 #[must_use]
195 pub fn new(values: Vec<String>) -> Self {
196 Self {
197 allowed_values: values.into_iter().collect(),
198 }
199 }
200
201 #[must_use]
203 pub fn validate_enum(&self, value: &str) -> bool {
204 self.allowed_values.contains(value)
205 }
206
207 #[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
237pub 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
256const 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
259const PHONE_E164_PATTERN: &str = r"^\+[1-9]\d{6,14}$";
261
262#[must_use]
269pub fn create_validator_from_rule(rule: &ValidationRule) -> Option<Box<dyn Validator>> {
270 match rule {
271 ValidationRule::Pattern { pattern, message } => {
272 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 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 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, }
303}