1use std::collections::HashMap;
2use std::fmt::Display;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct ValidationError {
7 pub property: String,
8 pub message: String,
9}
10
11impl ValidationError {
12 pub fn new(property: impl Into<String>, message: impl Into<String>) -> Self {
13 Self {
14 property: property.into(),
15 message: message.into(),
16 }
17 }
18}
19
20impl Display for ValidationError {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 write!(f, "{}: {}", self.property, self.message)
23 }
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct ValidationResult {
29 errors: Vec<ValidationError>,
30}
31
32impl ValidationResult {
33 pub fn new() -> Self {
35 Self { errors: Vec::new() }
36 }
37
38 pub fn add_error(&mut self, error: ValidationError) {
40 self.errors.push(error);
41 }
42
43 pub fn add_errors(&mut self, errors: Vec<ValidationError>) {
45 self.errors.extend(errors);
46 }
47
48 pub fn is_valid(&self) -> bool {
50 self.errors.is_empty()
51 }
52
53 pub fn errors(&self) -> &[ValidationError] {
55 &self.errors
56 }
57
58 pub fn errors_by_property(&self) -> HashMap<String, Vec<String>> {
60 let mut grouped: HashMap<String, Vec<String>> = HashMap::new();
61 for error in &self.errors {
62 grouped
63 .entry(error.property.clone())
64 .or_insert_with(Vec::new)
65 .push(error.message.clone());
66 }
67 grouped
68 }
69
70 pub fn first_error_for(&self, property: &str) -> Option<&str> {
72 self.errors
73 .iter()
74 .find(|e| e.property == property)
75 .map(|e| e.message.as_str())
76 }
77}
78
79impl Default for ValidationResult {
80 fn default() -> Self {
81 Self::new()
82 }
83}
84
85pub trait Validator<T> {
87 fn validate(&self, instance: &T) -> ValidationResult;
88}
89
90pub type Rule<T> = Box<dyn Fn(&T) -> Option<String>>;
92
93pub struct RuleBuilder<T> {
95 property_name: String,
96 rules: Vec<Rule<T>>,
97}
98
99impl<T> RuleBuilder<T> {
100 pub fn for_property(property_name: impl Into<String>) -> Self {
102 Self {
103 property_name: property_name.into(),
104 rules: Vec::new(),
105 }
106 }
107
108 pub fn rule(mut self, rule: impl Fn(&T) -> Option<String> + 'static) -> Self {
110 self.rules.push(Box::new(rule));
111 self
112 }
113
114 pub fn not_empty(self, message: Option<impl Into<String>>) -> Self
119 where
120 T: AsRef<str>,
121 {
122 let msg = message.map(|m| m.into()).unwrap_or_else(|| "must not be empty".to_string());
123 self.rule(move |value| {
124 if value.as_ref().trim().is_empty() {
125 Some(msg.clone())
126 } else {
127 None
128 }
129 })
130 }
131
132 pub fn not_null(self, message: Option<impl Into<String>>) -> Self
137 where
138 T: OptionLike,
139 {
140 let msg = message.map(|m| m.into()).unwrap_or_else(|| "must not be null".to_string());
141 self.rule(move |value| {
142 if value.is_none() {
143 Some(msg.clone())
144 } else {
145 None
146 }
147 })
148 }
149
150 pub fn min_length(self, min: usize, message: Option<impl Into<String> + Clone + 'static>) -> Self
156 where
157 T: AsRef<str>,
158 {
159 let msg = message.map(|m| m.into());
160 self.rule(move |value| {
161 let len = value.as_ref().len();
162 if len < min {
163 Some(msg.clone().unwrap_or_else(|| format!("must be at least {} characters long", min)))
164 } else {
165 None
166 }
167 })
168 }
169
170 pub fn max_length(self, max: usize, message: Option<impl Into<String> + Clone + 'static>) -> Self
176 where
177 T: AsRef<str>,
178 {
179 let msg = message.map(|m| m.into());
180 self.rule(move |value| {
181 let len = value.as_ref().len();
182 if len > max {
183 Some(msg.clone().unwrap_or_else(|| format!("must be at most {} characters long", max)))
184 } else {
185 None
186 }
187 })
188 }
189
190 pub fn length(self, min: usize, max: usize, min_message: Option<impl Into<String> + Clone + 'static>, max_message: Option<impl Into<String> + Clone + 'static>) -> Self
198 where
199 T: AsRef<str>,
200 {
201 self.min_length(min, min_message).max_length(max, max_message)
202 }
203
204 pub fn email(self, message: Option<impl Into<String>>) -> Self
209 where
210 T: AsRef<str>,
211 {
212 let msg = message.map(|m| m.into()).unwrap_or_else(|| "must be a valid email address".to_string());
213 self.rule(move |value| {
214 let email_regex = regex::Regex::new(
215 r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
216 )
217 .unwrap();
218 if !email_regex.is_match(value.as_ref()) {
219 Some(msg.clone())
220 } else {
221 None
222 }
223 })
224 }
225
226 pub fn greater_than(self, min: impl Into<f64> + Copy + 'static, message: Option<impl Into<String> + Clone + 'static>) -> Self
232 where
233 T: Numeric,
234 {
235 let min_val = min.into();
236 let msg = message.map(|m| m.into());
237 self.rule(move |value| {
238 if value.to_f64() <= min_val {
239 Some(msg.clone().unwrap_or_else(|| format!("must be greater than {}", min_val)))
240 } else {
241 None
242 }
243 })
244 }
245
246 pub fn greater_than_or_equal(self, min: impl Into<f64> + Copy + 'static, message: Option<impl Into<String> + Clone + 'static>) -> Self
252 where
253 T: Numeric,
254 {
255 let min_val = min.into();
256 let msg = message.map(|m| m.into());
257 self.rule(move |value| {
258 if value.to_f64() < min_val {
259 Some(msg.clone().unwrap_or_else(|| format!("must be greater than or equal to {}", min_val)))
260 } else {
261 None
262 }
263 })
264 }
265
266 pub fn less_than(self, max: impl Into<f64> + Copy + 'static, message: Option<impl Into<String> + Clone + 'static>) -> Self
272 where
273 T: Numeric,
274 {
275 let max_val = max.into();
276 let msg = message.map(|m| m.into());
277 self.rule(move |value| {
278 if value.to_f64() >= max_val {
279 Some(msg.clone().unwrap_or_else(|| format!("must be less than {}", max_val)))
280 } else {
281 None
282 }
283 })
284 }
285
286 pub fn less_than_or_equal(self, max: impl Into<f64> + Copy + 'static, message: Option<impl Into<String> + Clone + 'static>) -> Self
292 where
293 T: Numeric,
294 {
295 let max_val = max.into();
296 let msg = message.map(|m| m.into());
297 self.rule(move |value| {
298 if value.to_f64() > max_val {
299 Some(msg.clone().unwrap_or_else(|| format!("must be less than or equal to {}", max_val)))
300 } else {
301 None
302 }
303 })
304 }
305
306 pub fn inclusive_between(self, min: impl Into<f64> + Copy + 'static, max: impl Into<f64> + Copy + 'static, message: Option<impl Into<String> + Clone + 'static>) -> Self
313 where
314 T: Numeric,
315 {
316 let min_val = min.into();
317 let max_val = max.into();
318 let msg = message.map(|m| m.into());
319 self.rule(move |value| {
320 let val = value.to_f64();
321 if val < min_val || val > max_val {
322 Some(msg.clone().unwrap_or_else(|| format!("must be between {} and {}", min_val, max_val)))
323 } else {
324 None
325 }
326 })
327 }
328
329 pub fn must(self, predicate: impl Fn(&T) -> bool + 'static, message: impl Into<String> + Clone + 'static) -> Self {
331 let msg = message.into();
332 self.rule(move |value| {
333 if !predicate(value) {
334 Some(msg.clone())
335 } else {
336 None
337 }
338 })
339 }
340
341 pub fn build(self) -> impl Fn(&T) -> Vec<ValidationError> {
343 let property_name = self.property_name.clone();
344 let rules = self.rules;
345 move |value: &T| {
346 let mut errors = Vec::new();
347 for rule in &rules {
348 if let Some(message) = rule(value) {
349 errors.push(ValidationError::new(property_name.clone(), message));
350 }
351 }
352 errors
353 }
354 }
355}
356
357pub trait Numeric {
359 fn to_f64(&self) -> f64;
360}
361
362impl Numeric for i8 { fn to_f64(&self) -> f64 { *self as f64 } }
363impl Numeric for i16 { fn to_f64(&self) -> f64 { *self as f64 } }
364impl Numeric for i32 { fn to_f64(&self) -> f64 { *self as f64 } }
365impl Numeric for i64 { fn to_f64(&self) -> f64 { *self as f64 } }
366impl Numeric for u8 { fn to_f64(&self) -> f64 { *self as f64 } }
367impl Numeric for u16 { fn to_f64(&self) -> f64 { *self as f64 } }
368impl Numeric for u32 { fn to_f64(&self) -> f64 { *self as f64 } }
369impl Numeric for u64 { fn to_f64(&self) -> f64 { *self as f64 } }
370impl Numeric for f32 { fn to_f64(&self) -> f64 { *self as f64 } }
371impl Numeric for f64 { fn to_f64(&self) -> f64 { *self } }
372
373pub trait OptionLike {
375 fn is_none(&self) -> bool;
376}
377
378impl<T> OptionLike for Option<T> {
379 fn is_none(&self) -> bool {
380 Option::is_none(self)
381 }
382}
383
384pub struct ValidatorBuilder<T> {
386 rules: Vec<Box<dyn Fn(&T) -> Vec<ValidationError>>>,
387}
388
389impl<T> ValidatorBuilder<T> {
390 pub fn new() -> Self {
392 Self { rules: Vec::new() }
393 }
394
395 pub fn rule_for<F, V>(mut self, _property_name: impl Into<String>, accessor: F, builder: RuleBuilder<V>) -> Self
397 where
398 F: Fn(&T) -> &V + 'static,
399 V: 'static,
400 {
401 let rule_fn = builder.build();
402 self.rules.push(Box::new(move |instance: &T| {
403 let value = accessor(instance);
404 rule_fn(value)
405 }));
406 self
407 }
408
409 pub fn must<F, V, P>(mut self, property_name: impl Into<String>, accessor: F, predicate: P, message: impl Into<String>) -> Self
433 where
434 F: Fn(&T) -> &V + 'static,
435 V: 'static,
436 P: Fn(&T, &V) -> bool + 'static,
437 {
438 let property_name = property_name.into();
439 let msg = message.into();
440 self.rules.push(Box::new(move |instance: &T| {
441 let value = accessor(instance);
442 if !predicate(instance, value) {
443 vec![ValidationError::new(property_name.clone(), msg.clone())]
444 } else {
445 Vec::new()
446 }
447 }));
448 self
449 }
450
451 pub fn build(self) -> impl Validator<T> {
453 ValidatorImpl { rules: self.rules }
454 }
455}
456
457impl<T> Default for ValidatorBuilder<T> {
458 fn default() -> Self {
459 Self::new()
460 }
461}
462
463struct ValidatorImpl<T> {
464 rules: Vec<Box<dyn Fn(&T) -> Vec<ValidationError>>>,
465}
466
467impl<T> Validator<T> for ValidatorImpl<T> {
468 fn validate(&self, instance: &T) -> ValidationResult {
469 let mut result = ValidationResult::new();
470 for rule in &self.rules {
471 let errors = rule(instance);
472 result.add_errors(errors);
473 }
474 result
475 }
476}
477
478pub fn validate<T>(instance: &T, validator: &dyn Validator<T>) -> ValidationResult {
480 validator.validate(instance)
481}
482
483