1use std::marker::PhantomData;
25use std::sync::Arc;
26
27#[derive(Debug, Clone, PartialEq)]
31pub struct ValidationError {
32 pub field: String,
34 pub message: String,
36 pub code: Option<String>,
38}
39
40impl ValidationError {
41 pub fn new(field: impl Into<String>, message: impl Into<String>) -> Self {
42 Self {
43 field: field.into(),
44 message: message.into(),
45 code: None,
46 }
47 }
48
49 pub fn with_code(mut self, code: impl Into<String>) -> Self {
50 self.code = Some(code.into());
51 self
52 }
53}
54
55impl std::fmt::Display for ValidationError {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 write!(f, "{}: {}", self.field, self.message)
58 }
59}
60
61#[derive(Debug, Clone, Default)]
63pub struct ValidationErrors(Vec<ValidationError>);
64
65impl ValidationErrors {
66 pub fn new() -> Self {
67 Self(Vec::new())
68 }
69
70 pub fn push(&mut self, error: ValidationError) {
71 self.0.push(error);
72 }
73
74 pub fn is_empty(&self) -> bool {
75 self.0.is_empty()
76 }
77
78 pub fn errors(&self) -> &[ValidationError] {
79 &self.0
80 }
81
82 pub fn into_errors(self) -> Vec<ValidationError> {
83 self.0
84 }
85
86 pub fn into_result(self) -> Result<(), ValidationErrors> {
88 if self.is_empty() {
89 Ok(())
90 } else {
91 Err(self)
92 }
93 }
94}
95
96impl std::fmt::Display for ValidationErrors {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 for error in &self.0 {
99 writeln!(f, "{error}")?;
100 }
101 Ok(())
102 }
103}
104
105pub trait FieldRule<E>: Send + Sync {
111 fn validate(&self, entity: &E) -> Vec<ValidationError>;
112}
113
114pub struct EntityValidator<E> {
121 rules: Vec<Arc<dyn FieldRule<E>>>,
122 _phantom: PhantomData<E>,
123}
124
125impl<E: Send + Sync + 'static> EntityValidator<E> {
126 pub fn new() -> Self {
127 Self {
128 rules: Vec::new(),
129 _phantom: PhantomData,
130 }
131 }
132
133 pub fn rule(mut self, rule: impl FieldRule<E> + 'static) -> Self {
135 self.rules.push(Arc::new(rule));
136 self
137 }
138
139 pub fn validate(&self, entity: &E) -> ValidationErrors {
141 let mut errors = ValidationErrors::new();
142 for rule in &self.rules {
143 for error in rule.validate(entity) {
144 errors.push(error);
145 }
146 }
147 errors
148 }
149
150 pub fn validate_result(&self, entity: &E) -> Result<(), ValidationErrors> {
152 self.validate(entity).into_result()
153 }
154}
155
156impl<E: Send + Sync + 'static> Default for EntityValidator<E> {
157 fn default() -> Self {
158 Self::new()
159 }
160}
161
162pub struct RequiredString<E, F> {
166 field_name: &'static str,
167 accessor: F,
168 _phantom: PhantomData<E>,
169}
170
171impl<E, F: Fn(&E) -> &str + Send + Sync> RequiredString<E, F> {
172 pub fn new(field_name: &'static str, accessor: F) -> Self {
173 Self {
174 field_name,
175 accessor,
176 _phantom: PhantomData,
177 }
178 }
179}
180
181impl<E: Send + Sync, F: Fn(&E) -> &str + Send + Sync> FieldRule<E> for RequiredString<E, F> {
182 fn validate(&self, entity: &E) -> Vec<ValidationError> {
183 let value = (self.accessor)(entity);
184 if value.trim().is_empty() {
185 vec![ValidationError::new(
186 self.field_name,
187 format!("{} is required", self.field_name),
188 )
189 .with_code("required")]
190 } else {
191 vec![]
192 }
193 }
194}
195
196pub struct MaxLength<E, F> {
198 field_name: &'static str,
199 accessor: F,
200 max_len: usize,
201 _phantom: PhantomData<E>,
202}
203
204impl<E, F: Fn(&E) -> &str + Send + Sync> MaxLength<E, F> {
205 pub fn new(field_name: &'static str, accessor: F, max_len: usize) -> Self {
206 Self {
207 field_name,
208 accessor,
209 max_len,
210 _phantom: PhantomData,
211 }
212 }
213}
214
215impl<E: Send + Sync, F: Fn(&E) -> &str + Send + Sync> FieldRule<E> for MaxLength<E, F> {
216 fn validate(&self, entity: &E) -> Vec<ValidationError> {
217 let value = (self.accessor)(entity);
218 if value.chars().count() > self.max_len {
219 vec![ValidationError::new(
220 self.field_name,
221 format!(
222 "{} must be at most {} characters",
223 self.field_name, self.max_len
224 ),
225 )
226 .with_code("max_length")]
227 } else {
228 vec![]
229 }
230 }
231}
232
233pub struct NonNegative<E, F> {
235 field_name: &'static str,
236 accessor: F,
237 _phantom: PhantomData<E>,
238}
239
240impl<E, F: Fn(&E) -> i64 + Send + Sync> NonNegative<E, F> {
241 pub fn new(field_name: &'static str, accessor: F) -> Self {
242 Self {
243 field_name,
244 accessor,
245 _phantom: PhantomData,
246 }
247 }
248}
249
250impl<E: Send + Sync, F: Fn(&E) -> i64 + Send + Sync> FieldRule<E> for NonNegative<E, F> {
251 fn validate(&self, entity: &E) -> Vec<ValidationError> {
252 let value = (self.accessor)(entity);
253 if value < 0 {
254 vec![ValidationError::new(
255 self.field_name,
256 format!("{} must be 0 or greater", self.field_name),
257 )
258 .with_code("non_negative")]
259 } else {
260 vec![]
261 }
262 }
263}
264
265pub struct OptionalNotBlank<E, F> {
267 field_name: &'static str,
268 accessor: F,
269 _phantom: PhantomData<E>,
270}
271
272impl<E, F: Fn(&E) -> Option<&str> + Send + Sync> OptionalNotBlank<E, F> {
273 pub fn new(field_name: &'static str, accessor: F) -> Self {
274 Self {
275 field_name,
276 accessor,
277 _phantom: PhantomData,
278 }
279 }
280}
281
282impl<E: Send + Sync, F: Fn(&E) -> Option<&str> + Send + Sync> FieldRule<E>
283 for OptionalNotBlank<E, F>
284{
285 fn validate(&self, entity: &E) -> Vec<ValidationError> {
286 if let Some(value) = (self.accessor)(entity) {
287 if value.trim().is_empty() {
288 return vec![ValidationError::new(
289 self.field_name,
290 format!("{} must not be blank when provided", self.field_name),
291 )
292 .with_code("not_blank")];
293 }
294 }
295 vec![]
296 }
297}
298
299pub struct RequiredUuid<E, F> {
303 field_name: &'static str,
304 accessor: F,
305 _phantom: PhantomData<E>,
306}
307
308impl<E, F: Fn(&E) -> &str + Send + Sync> RequiredUuid<E, F> {
309 pub fn new(field_name: &'static str, accessor: F) -> Self {
310 Self {
311 field_name,
312 accessor,
313 _phantom: PhantomData,
314 }
315 }
316}
317
318impl<E: Send + Sync, F: Fn(&E) -> &str + Send + Sync> FieldRule<E> for RequiredUuid<E, F> {
319 fn validate(&self, entity: &E) -> Vec<ValidationError> {
320 let value = (self.accessor)(entity);
321 if value.trim().is_empty() {
322 return vec![ValidationError::new(
323 self.field_name,
324 format!("{} is required", self.field_name),
325 )
326 .with_code("required")];
327 }
328 if uuid::Uuid::parse_str(value).is_err() {
329 return vec![ValidationError::new(
330 self.field_name,
331 format!("{} must be a valid UUID", self.field_name),
332 )
333 .with_code("invalid_uuid")];
334 }
335 vec![]
336 }
337}
338
339pub struct Regex<E, F> {
341 field_name: &'static str,
342 accessor: F,
343 pattern: &'static str,
344 message: &'static str,
345 _phantom: PhantomData<E>,
346}
347
348impl<E, F: Fn(&E) -> &str + Send + Sync> Regex<E, F> {
349 pub fn new(
350 field_name: &'static str,
351 accessor: F,
352 pattern: &'static str,
353 message: &'static str,
354 ) -> Self {
355 Self {
356 field_name,
357 accessor,
358 pattern,
359 message,
360 _phantom: PhantomData,
361 }
362 }
363}
364
365impl<E: Send + Sync, F: Fn(&E) -> &str + Send + Sync> FieldRule<E> for Regex<E, F> {
366 fn validate(&self, entity: &E) -> Vec<ValidationError> {
367 let value = (self.accessor)(entity);
368 match regex::Regex::new(self.pattern) {
369 Ok(re) if re.is_match(value) => vec![],
370 Ok(_) => vec![ValidationError::new(self.field_name, self.message).with_code("pattern")],
371 Err(_) => vec![ValidationError::new(
372 self.field_name,
373 format!("invalid regex pattern: {}", self.pattern),
374 )
375 .with_code("invalid_pattern")],
376 }
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383
384 #[derive(Debug)]
385 struct User {
386 name: String,
387 age: i64,
388 bio: Option<String>,
389 }
390
391 #[test]
392 fn required_string_rejects_blank() {
393 let rule = RequiredString::new("name", |u: &User| u.name.as_str());
394 let user = User {
395 name: " ".into(),
396 age: 30,
397 bio: None,
398 };
399 let errors = rule.validate(&user);
400 assert_eq!(errors.len(), 1);
401 assert_eq!(errors[0].code.as_deref(), Some("required"));
402 }
403
404 #[test]
405 fn max_length_allows_exact_length() {
406 let rule = MaxLength::new("name", |u: &User| u.name.as_str(), 3);
407 let user = User {
408 name: "abc".into(),
409 age: 30,
410 bio: None,
411 };
412 assert!(rule.validate(&user).is_empty());
413 }
414
415 #[test]
416 fn max_length_rejects_over_limit() {
417 let rule = MaxLength::new("name", |u: &User| u.name.as_str(), 3);
418 let user = User {
419 name: "abcd".into(),
420 age: 30,
421 bio: None,
422 };
423 assert!(!rule.validate(&user).is_empty());
424 }
425
426 #[test]
427 fn regex_rule_validates_pattern() {
428 let rule = Regex::new("phone", |u: &User| u.name.as_str(), r"^\+\d{7,15}$", "must be E.164");
429
430 let valid = User { name: "+628123456789".into(), age: 0, bio: None };
431 assert!(rule.validate(&valid).is_empty(), "valid E.164 should pass");
432
433 let invalid = User { name: "no-plus".into(), age: 0, bio: None };
434 let errors = rule.validate(&invalid);
435 assert_eq!(errors.len(), 1);
436 assert_eq!(errors[0].code.as_deref(), Some("pattern"));
437 }
438
439 #[test]
440 fn entity_validator_collects_all_errors() {
441 let validator = EntityValidator::new()
442 .rule(RequiredString::new("name", |u: &User| u.name.as_str()))
443 .rule(NonNegative::new("age", |u: &User| u.age));
444
445 let user = User {
446 name: "".into(),
447 age: -1,
448 bio: None,
449 };
450
451 let errors = validator.validate(&user);
452 assert_eq!(errors.errors().len(), 2);
453 }
454}