Skip to main content

this/core/validation/
config.rs

1//! Entity validation configuration
2//!
3//! This module provides the configuration structure that holds validators and filters
4//! for an entity. It's generated by the macro system.
5
6use anyhow::Result;
7use serde_json::Value;
8use std::collections::HashMap;
9
10/// Type alias for validator function
11type ValidatorFn = Box<dyn Fn(&str, &Value) -> Result<(), String> + Send + Sync>;
12
13/// Type alias for filter function
14type FilterFn = Box<dyn Fn(&str, Value) -> Result<Value> + Send + Sync>;
15
16/// Configuration for validating and filtering an entity
17pub struct EntityValidationConfig {
18    /// Entity type name
19    pub entity_type: String,
20
21    /// Validators by field name
22    validators: HashMap<String, Vec<ValidatorFn>>,
23
24    /// Filters by field name
25    filters: HashMap<String, Vec<FilterFn>>,
26}
27
28impl EntityValidationConfig {
29    /// Create a new validation config for an entity
30    pub fn new(entity_type: &str) -> Self {
31        Self {
32            entity_type: entity_type.to_string(),
33            validators: HashMap::new(),
34            filters: HashMap::new(),
35        }
36    }
37
38    /// Add a validator for a specific field
39    pub fn add_validator<F>(&mut self, field: &str, validator: F)
40    where
41        F: Fn(&str, &Value) -> Result<(), String> + Send + Sync + 'static,
42    {
43        self.validators
44            .entry(field.to_string())
45            .or_default()
46            .push(Box::new(validator));
47    }
48
49    /// Add a filter for a specific field
50    pub fn add_filter<F>(&mut self, field: &str, filter: F)
51    where
52        F: Fn(&str, Value) -> Result<Value> + Send + Sync + 'static,
53    {
54        self.filters
55            .entry(field.to_string())
56            .or_default()
57            .push(Box::new(filter));
58    }
59
60    /// Validate and filter a complete payload
61    ///
62    /// Returns the filtered payload or a list of validation errors
63    pub fn validate_and_filter(&self, mut payload: Value) -> Result<Value, Vec<String>> {
64        let mut errors = Vec::new();
65
66        // Step 1: Apply all filters
67        if let Some(obj) = payload.as_object_mut() {
68            for (field, value) in obj.iter_mut() {
69                if let Some(field_filters) = self.filters.get(field) {
70                    for filter in field_filters {
71                        match filter(field, value.clone()) {
72                            Ok(filtered) => *value = filtered,
73                            Err(e) => {
74                                errors.push(format!("Erreur de filtrage sur '{}': {}", field, e));
75                            }
76                        }
77                    }
78                }
79            }
80        }
81
82        // Step 2: Apply all validators
83        if let Some(obj) = payload.as_object() {
84            for (field, value) in obj.iter() {
85                if let Some(field_validators) = self.validators.get(field) {
86                    for validator in field_validators {
87                        if let Err(e) = validator(field, value) {
88                            errors.push(e);
89                        }
90                    }
91                }
92            }
93        }
94
95        if errors.is_empty() {
96            Ok(payload)
97        } else {
98            Err(errors)
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use serde_json::json;
107
108    // === EntityValidationConfig::new ===
109
110    #[test]
111    fn test_new_creates_empty_config() {
112        let config = EntityValidationConfig::new("order");
113        assert_eq!(config.entity_type, "order");
114    }
115
116    // === validate_and_filter: validators only ===
117
118    #[test]
119    fn test_validate_valid_payload_returns_ok() {
120        let mut config = EntityValidationConfig::new("order");
121        config.add_validator("name", |_field, value| {
122            if value.is_null() {
123                Err("required".to_string())
124            } else {
125                Ok(())
126            }
127        });
128        let payload = json!({"name": "Test Order"});
129        let result = config.validate_and_filter(payload);
130        assert!(result.is_ok());
131        assert_eq!(result.expect("should be ok")["name"], "Test Order");
132    }
133
134    #[test]
135    fn test_validate_invalid_payload_returns_errors() {
136        let mut config = EntityValidationConfig::new("order");
137        config.add_validator("name", |field, value| {
138            if value.is_null() {
139                Err(format!("{} is required", field))
140            } else {
141                Ok(())
142            }
143        });
144        let payload = json!({"name": null});
145        let result = config.validate_and_filter(payload);
146        assert!(result.is_err());
147        let errors = result.unwrap_err();
148        assert_eq!(errors.len(), 1);
149        assert!(errors[0].contains("required"));
150    }
151
152    #[test]
153    fn test_validate_multiple_errors_accumulated() {
154        let mut config = EntityValidationConfig::new("order");
155        config.add_validator("name", |field, value| {
156            if value.is_null() {
157                Err(format!("{} is required", field))
158            } else {
159                Ok(())
160            }
161        });
162        config.add_validator("price", |field, value| {
163            if let Some(n) = value.as_f64()
164                && n <= 0.0
165            {
166                return Err(format!("{} must be positive", field));
167            }
168            Ok(())
169        });
170        let payload = json!({"name": null, "price": -5.0});
171        let result = config.validate_and_filter(payload);
172        assert!(result.is_err());
173        let errors = result.unwrap_err();
174        assert_eq!(errors.len(), 2);
175    }
176
177    #[test]
178    fn test_validate_multiple_validators_same_field() {
179        let mut config = EntityValidationConfig::new("order");
180        config.add_validator("name", |field, value| {
181            if value.is_null() {
182                Err(format!("{} is required", field))
183            } else {
184                Ok(())
185            }
186        });
187        config.add_validator("name", |field, value| {
188            if let Some(s) = value.as_str()
189                && s.len() < 3
190            {
191                return Err(format!("{} too short", field));
192            }
193            Ok(())
194        });
195        let payload = json!({"name": "ab"});
196        let result = config.validate_and_filter(payload);
197        assert!(result.is_err());
198        let errors = result.unwrap_err();
199        assert_eq!(errors.len(), 1);
200        assert!(errors[0].contains("too short"));
201    }
202
203    // === validate_and_filter: filters only ===
204
205    #[test]
206    fn test_filter_transforms_value() {
207        let mut config = EntityValidationConfig::new("order");
208        config.add_filter("name", |_field, value| {
209            if let Some(s) = value.as_str() {
210                Ok(Value::String(s.trim().to_string()))
211            } else {
212                Ok(value)
213            }
214        });
215        let payload = json!({"name": "  hello  "});
216        let result = config.validate_and_filter(payload);
217        assert!(result.is_ok());
218        assert_eq!(result.expect("should be ok")["name"], "hello");
219    }
220
221    #[test]
222    fn test_filter_chaining_multiple_filters_same_field() {
223        let mut config = EntityValidationConfig::new("order");
224        config.add_filter("code", |_field, value| {
225            if let Some(s) = value.as_str() {
226                Ok(Value::String(s.trim().to_string()))
227            } else {
228                Ok(value)
229            }
230        });
231        config.add_filter("code", |_field, value| {
232            if let Some(s) = value.as_str() {
233                Ok(Value::String(s.to_uppercase()))
234            } else {
235                Ok(value)
236            }
237        });
238        let payload = json!({"code": "  hello  "});
239        let result = config.validate_and_filter(payload);
240        assert!(result.is_ok());
241        assert_eq!(result.expect("should be ok")["code"], "HELLO");
242    }
243
244    // === validate_and_filter: filters THEN validators ===
245
246    #[test]
247    fn test_filters_applied_before_validators() {
248        let mut config = EntityValidationConfig::new("order");
249        // Filter: trim whitespace
250        config.add_filter("name", |_field, value| {
251            if let Some(s) = value.as_str() {
252                Ok(Value::String(s.trim().to_string()))
253            } else {
254                Ok(value)
255            }
256        });
257        // Validator: min length 3
258        config.add_validator("name", |field, value| {
259            if let Some(s) = value.as_str()
260                && s.len() < 3
261            {
262                return Err(format!("{} too short", field));
263            }
264            Ok(())
265        });
266        // "  ab  " -> trim -> "ab" -> validator fails (len 2 < 3)
267        let payload = json!({"name": "  ab  "});
268        let result = config.validate_and_filter(payload);
269        assert!(result.is_err());
270        assert!(result.unwrap_err()[0].contains("too short"));
271    }
272
273    #[test]
274    fn test_filters_transform_before_validation_passes() {
275        let mut config = EntityValidationConfig::new("order");
276        config.add_filter("name", |_field, value| {
277            if let Some(s) = value.as_str() {
278                Ok(Value::String(s.trim().to_string()))
279            } else {
280                Ok(value)
281            }
282        });
283        config.add_validator("name", |field, value| {
284            if let Some(s) = value.as_str()
285                && s.len() < 3
286            {
287                return Err(format!("{} too short", field));
288            }
289            Ok(())
290        });
291        // "  hello  " -> trim -> "hello" -> validator passes (len 5 >= 3)
292        let payload = json!({"name": "  hello  "});
293        let result = config.validate_and_filter(payload);
294        assert!(result.is_ok());
295        assert_eq!(result.expect("should be ok")["name"], "hello");
296    }
297
298    // === validate_and_filter: passthrough ===
299
300    #[test]
301    fn test_fields_without_validators_pass_through() {
302        let mut config = EntityValidationConfig::new("order");
303        config.add_validator("name", |_, _| Ok(()));
304        let payload = json!({"name": "Test", "extra_field": "untouched", "count": 42});
305        let result = config.validate_and_filter(payload);
306        assert!(result.is_ok());
307        let val = result.expect("should be ok");
308        assert_eq!(val["extra_field"], "untouched");
309        assert_eq!(val["count"], 42);
310    }
311
312    #[test]
313    fn test_empty_config_passes_everything() {
314        let config = EntityValidationConfig::new("order");
315        let payload = json!({"name": "anything", "price": -100});
316        let result = config.validate_and_filter(payload.clone());
317        assert!(result.is_ok());
318        assert_eq!(result.expect("should be ok"), payload);
319    }
320
321    // === validate_and_filter: non-object payload ===
322
323    #[test]
324    fn test_non_object_payload_string() {
325        let mut config = EntityValidationConfig::new("order");
326        config.add_validator("name", |_, _| Err("should not be called".to_string()));
327        let payload = json!("not an object");
328        // Non-object: filters and validators don't iterate, so no errors
329        let result = config.validate_and_filter(payload.clone());
330        assert!(result.is_ok());
331        assert_eq!(result.expect("should be ok"), payload);
332    }
333
334    #[test]
335    fn test_non_object_payload_array() {
336        let config = EntityValidationConfig::new("order");
337        let payload = json!([1, 2, 3]);
338        let result = config.validate_and_filter(payload.clone());
339        assert!(result.is_ok());
340    }
341
342    #[test]
343    fn test_non_object_payload_null() {
344        let config = EntityValidationConfig::new("order");
345        let payload = json!(null);
346        let result = config.validate_and_filter(payload);
347        assert!(result.is_ok());
348    }
349
350    // === filter error handling ===
351
352    #[test]
353    fn test_filter_error_is_captured() {
354        let mut config = EntityValidationConfig::new("order");
355        config.add_filter("name", |_field, _value| {
356            Err(anyhow::anyhow!("filter exploded"))
357        });
358        let payload = json!({"name": "test"});
359        let result = config.validate_and_filter(payload);
360        assert!(result.is_err());
361        let errors = result.unwrap_err();
362        assert_eq!(errors.len(), 1);
363        assert!(errors[0].contains("filtrage"));
364        assert!(errors[0].contains("filter exploded"));
365    }
366}