1use anyhow::Result;
7use serde_json::Value;
8use std::collections::HashMap;
9
10type ValidatorFn = Box<dyn Fn(&str, &Value) -> Result<(), String> + Send + Sync>;
12
13type FilterFn = Box<dyn Fn(&str, Value) -> Result<Value> + Send + Sync>;
15
16pub struct EntityValidationConfig {
18 pub entity_type: String,
20
21 validators: HashMap<String, Vec<ValidatorFn>>,
23
24 filters: HashMap<String, Vec<FilterFn>>,
26}
27
28impl EntityValidationConfig {
29 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 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 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 pub fn validate_and_filter(&self, mut payload: Value) -> Result<Value, Vec<String>> {
64 let mut errors = Vec::new();
65
66 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 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 #[test]
111 fn test_new_creates_empty_config() {
112 let config = EntityValidationConfig::new("order");
113 assert_eq!(config.entity_type, "order");
114 }
115
116 #[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 #[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 #[test]
247 fn test_filters_applied_before_validators() {
248 let mut config = EntityValidationConfig::new("order");
249 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 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 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 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 #[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 #[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 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 #[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}