use anyhow::Result;
use serde_json::Value;
use std::collections::HashMap;
type ValidatorFn = Box<dyn Fn(&str, &Value) -> Result<(), String> + Send + Sync>;
type FilterFn = Box<dyn Fn(&str, Value) -> Result<Value> + Send + Sync>;
pub struct EntityValidationConfig {
pub entity_type: String,
validators: HashMap<String, Vec<ValidatorFn>>,
filters: HashMap<String, Vec<FilterFn>>,
}
impl EntityValidationConfig {
pub fn new(entity_type: &str) -> Self {
Self {
entity_type: entity_type.to_string(),
validators: HashMap::new(),
filters: HashMap::new(),
}
}
pub fn add_validator<F>(&mut self, field: &str, validator: F)
where
F: Fn(&str, &Value) -> Result<(), String> + Send + Sync + 'static,
{
self.validators
.entry(field.to_string())
.or_default()
.push(Box::new(validator));
}
pub fn add_filter<F>(&mut self, field: &str, filter: F)
where
F: Fn(&str, Value) -> Result<Value> + Send + Sync + 'static,
{
self.filters
.entry(field.to_string())
.or_default()
.push(Box::new(filter));
}
pub fn validate_and_filter(&self, mut payload: Value) -> Result<Value, Vec<String>> {
let mut errors = Vec::new();
if let Some(obj) = payload.as_object_mut() {
for (field, value) in obj.iter_mut() {
if let Some(field_filters) = self.filters.get(field) {
for filter in field_filters {
match filter(field, value.clone()) {
Ok(filtered) => *value = filtered,
Err(e) => {
errors.push(format!("Erreur de filtrage sur '{}': {}", field, e));
}
}
}
}
}
}
if let Some(obj) = payload.as_object() {
for (field, value) in obj.iter() {
if let Some(field_validators) = self.validators.get(field) {
for validator in field_validators {
if let Err(e) = validator(field, value) {
errors.push(e);
}
}
}
}
}
if errors.is_empty() {
Ok(payload)
} else {
Err(errors)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_new_creates_empty_config() {
let config = EntityValidationConfig::new("order");
assert_eq!(config.entity_type, "order");
}
#[test]
fn test_validate_valid_payload_returns_ok() {
let mut config = EntityValidationConfig::new("order");
config.add_validator("name", |_field, value| {
if value.is_null() {
Err("required".to_string())
} else {
Ok(())
}
});
let payload = json!({"name": "Test Order"});
let result = config.validate_and_filter(payload);
assert!(result.is_ok());
assert_eq!(result.expect("should be ok")["name"], "Test Order");
}
#[test]
fn test_validate_invalid_payload_returns_errors() {
let mut config = EntityValidationConfig::new("order");
config.add_validator("name", |field, value| {
if value.is_null() {
Err(format!("{} is required", field))
} else {
Ok(())
}
});
let payload = json!({"name": null});
let result = config.validate_and_filter(payload);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("required"));
}
#[test]
fn test_validate_multiple_errors_accumulated() {
let mut config = EntityValidationConfig::new("order");
config.add_validator("name", |field, value| {
if value.is_null() {
Err(format!("{} is required", field))
} else {
Ok(())
}
});
config.add_validator("price", |field, value| {
if let Some(n) = value.as_f64()
&& n <= 0.0
{
return Err(format!("{} must be positive", field));
}
Ok(())
});
let payload = json!({"name": null, "price": -5.0});
let result = config.validate_and_filter(payload);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 2);
}
#[test]
fn test_validate_multiple_validators_same_field() {
let mut config = EntityValidationConfig::new("order");
config.add_validator("name", |field, value| {
if value.is_null() {
Err(format!("{} is required", field))
} else {
Ok(())
}
});
config.add_validator("name", |field, value| {
if let Some(s) = value.as_str()
&& s.len() < 3
{
return Err(format!("{} too short", field));
}
Ok(())
});
let payload = json!({"name": "ab"});
let result = config.validate_and_filter(payload);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("too short"));
}
#[test]
fn test_filter_transforms_value() {
let mut config = EntityValidationConfig::new("order");
config.add_filter("name", |_field, value| {
if let Some(s) = value.as_str() {
Ok(Value::String(s.trim().to_string()))
} else {
Ok(value)
}
});
let payload = json!({"name": " hello "});
let result = config.validate_and_filter(payload);
assert!(result.is_ok());
assert_eq!(result.expect("should be ok")["name"], "hello");
}
#[test]
fn test_filter_chaining_multiple_filters_same_field() {
let mut config = EntityValidationConfig::new("order");
config.add_filter("code", |_field, value| {
if let Some(s) = value.as_str() {
Ok(Value::String(s.trim().to_string()))
} else {
Ok(value)
}
});
config.add_filter("code", |_field, value| {
if let Some(s) = value.as_str() {
Ok(Value::String(s.to_uppercase()))
} else {
Ok(value)
}
});
let payload = json!({"code": " hello "});
let result = config.validate_and_filter(payload);
assert!(result.is_ok());
assert_eq!(result.expect("should be ok")["code"], "HELLO");
}
#[test]
fn test_filters_applied_before_validators() {
let mut config = EntityValidationConfig::new("order");
config.add_filter("name", |_field, value| {
if let Some(s) = value.as_str() {
Ok(Value::String(s.trim().to_string()))
} else {
Ok(value)
}
});
config.add_validator("name", |field, value| {
if let Some(s) = value.as_str()
&& s.len() < 3
{
return Err(format!("{} too short", field));
}
Ok(())
});
let payload = json!({"name": " ab "});
let result = config.validate_and_filter(payload);
assert!(result.is_err());
assert!(result.unwrap_err()[0].contains("too short"));
}
#[test]
fn test_filters_transform_before_validation_passes() {
let mut config = EntityValidationConfig::new("order");
config.add_filter("name", |_field, value| {
if let Some(s) = value.as_str() {
Ok(Value::String(s.trim().to_string()))
} else {
Ok(value)
}
});
config.add_validator("name", |field, value| {
if let Some(s) = value.as_str()
&& s.len() < 3
{
return Err(format!("{} too short", field));
}
Ok(())
});
let payload = json!({"name": " hello "});
let result = config.validate_and_filter(payload);
assert!(result.is_ok());
assert_eq!(result.expect("should be ok")["name"], "hello");
}
#[test]
fn test_fields_without_validators_pass_through() {
let mut config = EntityValidationConfig::new("order");
config.add_validator("name", |_, _| Ok(()));
let payload = json!({"name": "Test", "extra_field": "untouched", "count": 42});
let result = config.validate_and_filter(payload);
assert!(result.is_ok());
let val = result.expect("should be ok");
assert_eq!(val["extra_field"], "untouched");
assert_eq!(val["count"], 42);
}
#[test]
fn test_empty_config_passes_everything() {
let config = EntityValidationConfig::new("order");
let payload = json!({"name": "anything", "price": -100});
let result = config.validate_and_filter(payload.clone());
assert!(result.is_ok());
assert_eq!(result.expect("should be ok"), payload);
}
#[test]
fn test_non_object_payload_string() {
let mut config = EntityValidationConfig::new("order");
config.add_validator("name", |_, _| Err("should not be called".to_string()));
let payload = json!("not an object");
let result = config.validate_and_filter(payload.clone());
assert!(result.is_ok());
assert_eq!(result.expect("should be ok"), payload);
}
#[test]
fn test_non_object_payload_array() {
let config = EntityValidationConfig::new("order");
let payload = json!([1, 2, 3]);
let result = config.validate_and_filter(payload.clone());
assert!(result.is_ok());
}
#[test]
fn test_non_object_payload_null() {
let config = EntityValidationConfig::new("order");
let payload = json!(null);
let result = config.validate_and_filter(payload);
assert!(result.is_ok());
}
#[test]
fn test_filter_error_is_captured() {
let mut config = EntityValidationConfig::new("order");
config.add_filter("name", |_field, _value| {
Err(anyhow::anyhow!("filter exploded"))
});
let payload = json!({"name": "test"});
let result = config.validate_and_filter(payload);
assert!(result.is_err());
let errors = result.unwrap_err();
assert_eq!(errors.len(), 1);
assert!(errors[0].contains("filtrage"));
assert!(errors[0].contains("filter exploded"));
}
}