elif_validation/
traits.rs1use crate::error::{ValidationError, ValidationErrors, ValidationResult};
4use async_trait::async_trait;
5use serde_json::Value;
6use std::collections::HashMap;
7
8#[async_trait]
10pub trait ValidationRule: Send + Sync {
11 async fn validate(&self, value: &Value, field: &str) -> ValidationResult<()>;
13
14 fn rule_name(&self) -> &'static str;
16
17 fn parameters(&self) -> Option<Value> {
19 None
20 }
21}
22
23#[async_trait]
25pub trait ValidateField: Send + Sync {
26 async fn validate_field(&self, field: &str, value: &Value) -> ValidationResult<()>;
28}
29
30#[async_trait]
32pub trait ValidateRequest: Send + Sync {
33 async fn validate_request(&self, data: &HashMap<String, Value>) -> ValidationResult<()>;
35}
36
37#[async_trait]
39pub trait Validate: ValidateField + ValidateRequest + Send + Sync {
40 async fn validate(&self, data: &HashMap<String, Value>) -> ValidationResult<()> {
42 let mut errors = ValidationErrors::new();
43
44 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 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
64impl<T> Validate for T where T: ValidateField + ValidateRequest + Send + Sync {}
66
67pub 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
138pub 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}