elif_validation/
traits.rs

1//! Core validation traits for the elif framework
2
3use crate::error::{ValidationError, ValidationErrors, ValidationResult};
4use async_trait::async_trait;
5use serde_json::Value;
6use std::collections::HashMap;
7
8/// Core validation trait that all validators must implement
9#[async_trait]
10pub trait ValidationRule: Send + Sync {
11    /// Validate a single value
12    async fn validate(&self, value: &Value, field: &str) -> ValidationResult<()>;
13    
14    /// Get the validation rule name/type
15    fn rule_name(&self) -> &'static str;
16    
17    /// Get validation rule parameters/configuration as JSON
18    fn parameters(&self) -> Option<Value> {
19        None
20    }
21}
22
23/// Trait for validating individual fields
24#[async_trait]
25pub trait ValidateField: Send + Sync {
26    /// Validate a single field value
27    async fn validate_field(&self, field: &str, value: &Value) -> ValidationResult<()>;
28}
29
30/// Trait for validating entire requests/objects
31#[async_trait]  
32pub trait ValidateRequest: Send + Sync {
33    /// Validate the entire request data
34    async fn validate_request(&self, data: &HashMap<String, Value>) -> ValidationResult<()>;
35}
36
37/// Main validation trait that combines field and request validation
38#[async_trait]
39pub trait Validate: ValidateField + ValidateRequest + Send + Sync {
40    /// Validate both individual fields and the entire request
41    async fn validate(&self, data: &HashMap<String, Value>) -> ValidationResult<()> {
42        let mut errors = ValidationErrors::new();
43        
44        // First validate individual fields
45        for (field, value) in data {
46            if let Err(field_errors) = self.validate_field(field, value).await {
47                errors.merge(field_errors);
48            }
49        }
50        
51        // Then validate the entire request for cross-field rules
52        if let Err(request_errors) = self.validate_request(data).await {
53            errors.merge(request_errors);
54        }
55        
56        if errors.is_empty() {
57            Ok(())
58        } else {
59            Err(errors)
60        }
61    }
62}
63
64/// Auto-implementation of Validate for types that implement both ValidateField and ValidateRequest
65impl<T> Validate for T where T: ValidateField + ValidateRequest + Send + Sync {}
66
67/// Trait for types that can be converted to a validation value
68pub trait ToValidationValue {
69    fn to_validation_value(&self) -> Value;
70}
71
72impl ToValidationValue for String {
73    fn to_validation_value(&self) -> Value {
74        Value::String(self.clone())
75    }
76}
77
78impl ToValidationValue for &str {
79    fn to_validation_value(&self) -> Value {
80        Value::String(self.to_string())
81    }
82}
83
84impl ToValidationValue for i32 {
85    fn to_validation_value(&self) -> Value {
86        Value::Number(serde_json::Number::from(*self))
87    }
88}
89
90impl ToValidationValue for i64 {
91    fn to_validation_value(&self) -> Value {
92        Value::Number(serde_json::Number::from(*self))
93    }
94}
95
96impl ToValidationValue for f64 {
97    fn to_validation_value(&self) -> Value {
98        Value::Number(serde_json::Number::from_f64(*self).unwrap_or(serde_json::Number::from(0)))
99    }
100}
101
102impl ToValidationValue for bool {
103    fn to_validation_value(&self) -> Value {
104        Value::Bool(*self)
105    }
106}
107
108impl ToValidationValue for Value {
109    fn to_validation_value(&self) -> Value {
110        self.clone()
111    }
112}
113
114impl<T> ToValidationValue for Option<T> 
115where 
116    T: ToValidationValue,
117{
118    fn to_validation_value(&self) -> Value {
119        match self {
120            Some(value) => value.to_validation_value(),
121            None => Value::Null,
122        }
123    }
124}
125
126impl<T> ToValidationValue for Vec<T>
127where 
128    T: ToValidationValue,
129{
130    fn to_validation_value(&self) -> Value {
131        let values: Vec<Value> = self.iter()
132            .map(|item| item.to_validation_value())
133            .collect();
134        Value::Array(values)
135    }
136}
137
138/// Helper trait for creating validation errors
139pub trait CreateValidationError {
140    fn validation_error(field: &str, message: &str) -> ValidationError {
141        ValidationError::new(field, message)
142    }
143    
144    fn validation_error_with_code(field: &str, message: &str, code: &str) -> ValidationError {
145        ValidationError::with_code(field, message, code)
146    }
147}
148
149impl<T> CreateValidationError for T {}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use std::collections::HashMap;
155
156    struct TestValidator;
157
158    #[async_trait]
159    impl ValidateField for TestValidator {
160        async fn validate_field(&self, field: &str, value: &Value) -> ValidationResult<()> {
161            if field == "email" && value.as_str().map(|s| !s.contains('@')).unwrap_or(true) {
162                return Err(ValidationErrors::from_error(
163                    ValidationError::new(field, "Invalid email format")
164                ));
165            }
166            Ok(())
167        }
168    }
169
170    #[async_trait]
171    impl ValidateRequest for TestValidator {
172        async fn validate_request(&self, data: &HashMap<String, Value>) -> ValidationResult<()> {
173            if data.get("password").is_some() && data.get("password_confirmation").is_none() {
174                return Err(ValidationErrors::from_error(
175                    ValidationError::new("password_confirmation", "Password confirmation required")
176                ));
177            }
178            Ok(())
179        }
180    }
181
182    #[tokio::test]
183    async fn test_field_validation() {
184        let validator = TestValidator;
185        let value = Value::String("invalid-email".to_string());
186        
187        let result = validator.validate_field("email", &value).await;
188        assert!(result.is_err());
189        
190        let errors = result.unwrap_err();
191        assert!(errors.has_field_errors("email"));
192    }
193
194    #[tokio::test]
195    async fn test_request_validation() {
196        let validator = TestValidator;
197        let mut data = HashMap::new();
198        data.insert("password".to_string(), Value::String("secret".to_string()));
199        
200        let result = validator.validate_request(&data).await;
201        assert!(result.is_err());
202        
203        let errors = result.unwrap_err();
204        assert!(errors.has_field_errors("password_confirmation"));
205    }
206
207    #[tokio::test]
208    async fn test_combined_validation() {
209        let validator = TestValidator;
210        let mut data = HashMap::new();
211        data.insert("email".to_string(), Value::String("invalid-email".to_string()));
212        data.insert("password".to_string(), Value::String("secret".to_string()));
213        
214        let result = validator.validate(&data).await;
215        assert!(result.is_err());
216        
217        let errors = result.unwrap_err();
218        assert!(errors.has_field_errors("email"));
219        assert!(errors.has_field_errors("password_confirmation"));
220        assert_eq!(errors.len(), 2);
221    }
222
223    #[test]
224    fn test_to_validation_value() {
225        assert_eq!("hello".to_validation_value(), Value::String("hello".to_string()));
226        assert_eq!(42i32.to_validation_value(), Value::Number(serde_json::Number::from(42)));
227        assert_eq!(true.to_validation_value(), Value::Bool(true));
228        
229        let opt_str: Option<String> = Some("test".to_string());
230        assert_eq!(opt_str.to_validation_value(), Value::String("test".to_string()));
231        
232        let opt_none: Option<String> = None;
233        assert_eq!(opt_none.to_validation_value(), Value::Null);
234    }
235}