Skip to main content

this/core/validation/
validators.rs

1//! Reusable field validators
2//!
3//! These validators are used by the macro system to validate entity fields
4
5use serde_json::Value;
6
7/// Validator: field is required (not null)
8pub fn required() -> impl Fn(&str, &Value) -> Result<(), String> + Send + Sync + Clone {
9    |field: &str, value: &Value| {
10        if value.is_null() {
11            Err(format!("Le champ '{}' est requis", field))
12        } else {
13            Ok(())
14        }
15    }
16}
17
18/// Validator: field is optional (always valid)
19pub fn optional() -> impl Fn(&str, &Value) -> Result<(), String> + Send + Sync + Clone {
20    |_: &str, _: &Value| Ok(())
21}
22
23/// Validator: number must be positive
24pub fn positive() -> impl Fn(&str, &Value) -> Result<(), String> + Send + Sync + Clone {
25    |field: &str, value: &Value| {
26        if let Some(num) = value.as_f64() {
27            if num <= 0.0 {
28                Err(format!(
29                    "Le champ '{}' doit être positif (valeur: {})",
30                    field, num
31                ))
32            } else {
33                Ok(())
34            }
35        } else {
36            Ok(()) // Si ce n'est pas un nombre, on laisse passer (autre validateur gérera)
37        }
38    }
39}
40
41/// Validator: string length must be within range
42pub fn string_length(
43    min: usize,
44    max: usize,
45) -> impl Fn(&str, &Value) -> Result<(), String> + Send + Sync + Clone {
46    move |field: &str, value: &Value| {
47        if let Some(s) = value.as_str() {
48            let len = s.len();
49            if len < min {
50                Err(format!(
51                    "'{}' doit avoir au moins {} caractères (actuellement: {})",
52                    field, min, len
53                ))
54            } else if len > max {
55                Err(format!(
56                    "'{}' ne doit pas dépasser {} caractères (actuellement: {})",
57                    field, max, len
58                ))
59            } else {
60                Ok(())
61            }
62        } else {
63            Ok(())
64        }
65    }
66}
67
68/// Validator: number must not exceed maximum
69pub fn max_value(max: f64) -> impl Fn(&str, &Value) -> Result<(), String> + Send + Sync + Clone {
70    move |field: &str, value: &Value| {
71        if let Some(num) = value.as_f64() {
72            if num > max {
73                Err(format!(
74                    "'{}' ne doit pas dépasser {} (valeur: {})",
75                    field, max, num
76                ))
77            } else {
78                Ok(())
79            }
80        } else {
81            Ok(())
82        }
83    }
84}
85
86/// Validator: value must be in allowed list
87pub fn in_list(
88    allowed: Vec<String>,
89) -> impl Fn(&str, &Value) -> Result<(), String> + Send + Sync + Clone {
90    move |field: &str, value: &Value| {
91        if let Some(s) = value.as_str() {
92            if !allowed.contains(&s.to_string()) {
93                Err(format!(
94                    "'{}' doit être l'une des valeurs: {:?} (valeur actuelle: {})",
95                    field, allowed, s
96                ))
97            } else {
98                Ok(())
99            }
100        } else {
101            Ok(())
102        }
103    }
104}
105
106/// Validator: date must match format
107pub fn date_format(
108    format: &'static str,
109) -> impl Fn(&str, &Value) -> Result<(), String> + Send + Sync + Clone {
110    move |field: &str, value: &Value| {
111        if let Some(s) = value.as_str() {
112            match chrono::NaiveDate::parse_from_str(s, format) {
113                Ok(_) => Ok(()),
114                Err(_) => Err(format!(
115                    "'{}' doit être au format {} (valeur actuelle: {})",
116                    field, format, s
117                )),
118            }
119        } else {
120            Ok(())
121        }
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use serde_json::json;
129
130    // === required() ===
131
132    #[test]
133    fn test_required_null_value_returns_error() {
134        let v = required();
135        let result = v("name", &json!(null));
136        assert!(result.is_err());
137        assert!(result.unwrap_err().contains("requis"));
138    }
139
140    #[test]
141    fn test_required_string_value_returns_ok() {
142        let v = required();
143        assert!(v("name", &json!("hello")).is_ok());
144    }
145
146    #[test]
147    fn test_required_number_value_returns_ok() {
148        let v = required();
149        assert!(v("age", &json!(42)).is_ok());
150    }
151
152    #[test]
153    fn test_required_bool_value_returns_ok() {
154        let v = required();
155        assert!(v("active", &json!(true)).is_ok());
156    }
157
158    #[test]
159    fn test_required_object_value_returns_ok() {
160        let v = required();
161        assert!(v("data", &json!({"key": "val"})).is_ok());
162    }
163
164    #[test]
165    fn test_required_empty_string_returns_ok() {
166        let v = required();
167        assert!(v("name", &json!("")).is_ok());
168    }
169
170    #[test]
171    fn test_required_array_returns_ok() {
172        let v = required();
173        assert!(v("tags", &json!([1, 2, 3])).is_ok());
174    }
175
176    // === optional() ===
177
178    #[test]
179    fn test_optional_always_ok_for_null() {
180        let v = optional();
181        assert!(v("field", &json!(null)).is_ok());
182    }
183
184    #[test]
185    fn test_optional_always_ok_for_string() {
186        let v = optional();
187        assert!(v("field", &json!("value")).is_ok());
188    }
189
190    // === positive() ===
191
192    #[test]
193    fn test_positive_negative_number_returns_error() {
194        let v = positive();
195        let result = v("price", &json!(-5.0));
196        assert!(result.is_err());
197        assert!(result.unwrap_err().contains("positif"));
198    }
199
200    #[test]
201    fn test_positive_zero_returns_error() {
202        let v = positive();
203        assert!(v("price", &json!(0.0)).is_err());
204    }
205
206    #[test]
207    fn test_positive_positive_number_returns_ok() {
208        let v = positive();
209        assert!(v("price", &json!(42.5)).is_ok());
210    }
211
212    #[test]
213    fn test_positive_non_number_passthrough() {
214        let v = positive();
215        assert!(v("name", &json!("hello")).is_ok());
216    }
217
218    #[test]
219    fn test_positive_integer_positive() {
220        let v = positive();
221        assert!(v("count", &json!(1)).is_ok());
222    }
223
224    #[test]
225    fn test_positive_integer_negative() {
226        let v = positive();
227        assert!(v("count", &json!(-1)).is_err());
228    }
229
230    // === string_length() ===
231
232    #[test]
233    fn test_string_length_too_short_returns_error() {
234        let v = string_length(3, 50);
235        let result = v("name", &json!("ab"));
236        assert!(result.is_err());
237        assert!(result.unwrap_err().contains("au moins 3"));
238    }
239
240    #[test]
241    fn test_string_length_too_long_returns_error() {
242        let v = string_length(1, 5);
243        let result = v("name", &json!("abcdef"));
244        assert!(result.is_err());
245        assert!(result.unwrap_err().contains("dépasser 5"));
246    }
247
248    #[test]
249    fn test_string_length_exact_min_returns_ok() {
250        let v = string_length(3, 10);
251        assert!(v("name", &json!("abc")).is_ok());
252    }
253
254    #[test]
255    fn test_string_length_exact_max_returns_ok() {
256        let v = string_length(1, 5);
257        assert!(v("name", &json!("abcde")).is_ok());
258    }
259
260    #[test]
261    fn test_string_length_within_range_returns_ok() {
262        let v = string_length(2, 10);
263        assert!(v("name", &json!("hello")).is_ok());
264    }
265
266    #[test]
267    fn test_string_length_non_string_passthrough() {
268        let v = string_length(5, 10);
269        assert!(v("age", &json!(42)).is_ok());
270    }
271
272    // === max_value() ===
273
274    #[test]
275    fn test_max_value_over_returns_error() {
276        let v = max_value(100.0);
277        let result = v("score", &json!(101.0));
278        assert!(result.is_err());
279        assert!(result.unwrap_err().contains("dépasser 100"));
280    }
281
282    #[test]
283    fn test_max_value_equal_returns_ok() {
284        let v = max_value(100.0);
285        assert!(v("score", &json!(100.0)).is_ok());
286    }
287
288    #[test]
289    fn test_max_value_under_returns_ok() {
290        let v = max_value(100.0);
291        assert!(v("score", &json!(50.0)).is_ok());
292    }
293
294    #[test]
295    fn test_max_value_non_number_passthrough() {
296        let v = max_value(100.0);
297        assert!(v("name", &json!("hello")).is_ok());
298    }
299
300    #[test]
301    fn test_max_value_negative() {
302        let v = max_value(0.0);
303        assert!(v("temp", &json!(-10.0)).is_ok());
304    }
305
306    // === in_list() ===
307
308    #[test]
309    fn test_in_list_value_in_list_returns_ok() {
310        let v = in_list(vec!["active".into(), "inactive".into(), "pending".into()]);
311        assert!(v("status", &json!("active")).is_ok());
312    }
313
314    #[test]
315    fn test_in_list_value_not_in_list_returns_error() {
316        let v = in_list(vec!["active".into(), "inactive".into()]);
317        let result = v("status", &json!("deleted"));
318        assert!(result.is_err());
319        assert!(result.unwrap_err().contains("valeurs"));
320    }
321
322    #[test]
323    fn test_in_list_non_string_passthrough() {
324        let v = in_list(vec!["yes".into(), "no".into()]);
325        assert!(v("flag", &json!(42)).is_ok());
326    }
327
328    #[test]
329    fn test_in_list_empty_list_always_error_for_strings() {
330        let v = in_list(vec![]);
331        assert!(v("status", &json!("anything")).is_err());
332    }
333
334    // === date_format() ===
335
336    #[test]
337    fn test_date_format_valid_date_returns_ok() {
338        let v = date_format("%Y-%m-%d");
339        assert!(v("birthday", &json!("2024-01-15")).is_ok());
340    }
341
342    #[test]
343    fn test_date_format_invalid_date_returns_error() {
344        let v = date_format("%Y-%m-%d");
345        let result = v("birthday", &json!("not-a-date"));
346        assert!(result.is_err());
347        assert!(result.unwrap_err().contains("format"));
348    }
349
350    #[test]
351    fn test_date_format_non_string_passthrough() {
352        let v = date_format("%Y-%m-%d");
353        assert!(v("birthday", &json!(12345)).is_ok());
354    }
355
356    #[test]
357    fn test_date_format_wrong_format_returns_error() {
358        let v = date_format("%d/%m/%Y");
359        assert!(v("date", &json!("2024-01-15")).is_err());
360    }
361
362    #[test]
363    fn test_date_format_correct_custom_format() {
364        let v = date_format("%d/%m/%Y");
365        assert!(v("date", &json!("15/01/2024")).is_ok());
366    }
367}