ferro_rs/validation/
validator.rs1use crate::validation::{Rule, ValidationError};
4use serde_json::Value;
5use std::collections::HashMap;
6
7pub struct Validator<'a> {
31 data: &'a Value,
32 rules: HashMap<String, Vec<Box<dyn Rule>>>,
33 custom_messages: HashMap<String, String>,
34 custom_attributes: HashMap<String, String>,
35 stop_on_first_failure: bool,
36}
37
38impl<'a> Validator<'a> {
39 pub fn new(data: &'a Value) -> Self {
41 Self {
42 data,
43 rules: HashMap::new(),
44 custom_messages: HashMap::new(),
45 custom_attributes: HashMap::new(),
46 stop_on_first_failure: false,
47 }
48 }
49
50 pub fn rule<R: Rule + 'static>(mut self, field: impl Into<String>, rule: R) -> Self {
52 let field = field.into();
53 self.rules
54 .entry(field)
55 .or_default()
56 .push(Box::new(rule) as Box<dyn Rule>);
57 self
58 }
59
60 pub fn rules(mut self, field: impl Into<String>, rules: Vec<Box<dyn Rule>>) -> Self {
73 self.rules.insert(field.into(), rules);
74 self
75 }
76
77 pub fn boxed_rules(mut self, field: impl Into<String>, rules: Vec<Box<dyn Rule>>) -> Self {
79 self.rules.insert(field.into(), rules);
80 self
81 }
82
83 pub fn message(mut self, key: impl Into<String>, message: impl Into<String>) -> Self {
94 self.custom_messages.insert(key.into(), message.into());
95 self
96 }
97
98 pub fn messages(mut self, messages: HashMap<String, String>) -> Self {
100 self.custom_messages.extend(messages);
101 self
102 }
103
104 pub fn attribute(mut self, field: impl Into<String>, name: impl Into<String>) -> Self {
115 self.custom_attributes.insert(field.into(), name.into());
116 self
117 }
118
119 pub fn attributes(mut self, attributes: HashMap<String, String>) -> Self {
121 self.custom_attributes.extend(attributes);
122 self
123 }
124
125 pub fn stop_on_first_failure(mut self) -> Self {
127 self.stop_on_first_failure = true;
128 self
129 }
130
131 pub fn validate(self) -> Result<(), ValidationError> {
133 let mut errors = ValidationError::new();
134
135 for (field, rules) in &self.rules {
136 let value = self.get_value(field);
137 let display_field = self.get_display_field(field);
138
139 let has_nullable = rules.iter().any(|r| r.name() == "nullable");
141 if has_nullable && value.is_null() {
142 continue;
143 }
144
145 for rule in rules {
146 if rule.name() == "nullable" {
148 continue;
149 }
150
151 if let Err(default_message) = rule.validate(&display_field, &value, self.data) {
152 let message_key = format!("{}.{}", field, rule.name());
154 let message = self
155 .custom_messages
156 .get(&message_key)
157 .cloned()
158 .unwrap_or(default_message);
159
160 errors.add(field, message);
161 }
162 }
163
164 if self.stop_on_first_failure && errors.has(field) {
165 break;
166 }
167 }
168
169 if errors.is_empty() {
170 Ok(())
171 } else {
172 Err(errors)
173 }
174 }
175
176 pub fn passes(&self) -> bool {
178 let mut errors = ValidationError::new();
179
180 for (field, rules) in &self.rules {
181 let value = self.get_value(field);
182 let display_field = self.get_display_field(field);
183
184 let has_nullable = rules.iter().any(|r| r.name() == "nullable");
185 if has_nullable && value.is_null() {
186 continue;
187 }
188
189 for rule in rules {
190 if rule.name() == "nullable" {
191 continue;
192 }
193
194 if rule.validate(&display_field, &value, self.data).is_err() {
195 errors.add(field, "failed");
196 }
197 }
198 }
199
200 errors.is_empty()
201 }
202
203 pub fn fails(&self) -> bool {
205 !self.passes()
206 }
207
208 fn get_value(&self, field: &str) -> Value {
210 get_nested_value(self.data, field)
211 .cloned()
212 .unwrap_or(Value::Null)
213 }
214
215 fn get_display_field(&self, field: &str) -> String {
217 self.custom_attributes
218 .get(field)
219 .cloned()
220 .unwrap_or_else(|| {
221 field.split('_').collect::<Vec<_>>().join(" ")
223 })
224 }
225}
226
227fn get_nested_value<'a>(data: &'a Value, path: &str) -> Option<&'a Value> {
229 let parts: Vec<&str> = path.split('.').collect();
230 let mut current = data;
231
232 for part in parts {
233 if let Value::Object(map) = current {
235 current = map.get(part)?;
236 }
237 else if let Value::Array(arr) = current {
239 let index: usize = part.parse().ok()?;
240 current = arr.get(index)?;
241 } else {
242 return None;
243 }
244 }
245
246 Some(current)
247}
248
249pub fn validate<I, F>(data: &Value, rules: I) -> Result<(), ValidationError>
264where
265 I: IntoIterator<Item = (F, Vec<Box<dyn Rule>>)>,
266 F: Into<String>,
267{
268 let mut validator = Validator::new(data);
269 for (field, field_rules) in rules {
270 validator = validator.rules(field, field_rules);
271 }
272 validator.validate()
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use crate::rules;
279 use crate::validation::rules::*;
280 use serde_json::json;
281
282 #[test]
283 fn test_validator_passes() {
284 let data = json!({
285 "email": "test@example.com",
286 "name": "John Doe"
287 });
288
289 let result = Validator::new(&data)
290 .rules("email", rules![required(), email()])
291 .rules("name", rules![required(), string()])
292 .validate();
293
294 assert!(result.is_ok());
295 }
296
297 #[test]
298 fn test_validator_fails() {
299 let data = json!({
300 "email": "invalid-email",
301 "name": ""
302 });
303
304 let result = Validator::new(&data)
305 .rules("email", rules![required(), email()])
306 .rules("name", rules![required()])
307 .validate();
308
309 assert!(result.is_err());
310 let errors = result.unwrap_err();
311 assert!(errors.has("email"));
312 assert!(errors.has("name"));
313 }
314
315 #[test]
316 fn test_validator_custom_message() {
317 let data = json!({"email": ""});
318
319 let result = Validator::new(&data)
320 .rules("email", rules![required()])
321 .message("email.required", "We need your email!")
322 .validate();
323
324 let errors = result.unwrap_err();
325 assert_eq!(
326 errors.first("email"),
327 Some(&"We need your email!".to_string())
328 );
329 }
330
331 #[test]
332 fn test_validator_custom_attribute() {
333 let data = json!({"user_email": ""});
334
335 let validator = Validator::new(&data)
337 .rules("user_email", rules![required()])
338 .attribute("user_email", "email address");
339
340 let result = validator.validate();
342 assert!(result.is_err());
343 let errors = result.unwrap_err();
344 assert!(
345 errors.first("user_email").is_some(),
346 "Expected error for 'user_email'"
347 );
348
349 }
354
355 #[test]
356 fn test_validator_nullable() {
357 let data = json!({"nickname": null});
358
359 let result = Validator::new(&data)
360 .rules("nickname", rules![nullable(), string(), min(3)])
361 .validate();
362
363 assert!(result.is_ok());
364 }
365
366 #[test]
367 fn test_nested_value() {
368 let data = json!({
369 "user": {
370 "profile": {
371 "email": "test@example.com"
372 }
373 }
374 });
375
376 let value = get_nested_value(&data, "user.profile.email");
377 assert_eq!(value, Some(&json!("test@example.com")));
378 }
379
380 #[test]
381 fn test_validate_function() {
382 let data = json!({"email": "test@example.com"});
383
384 let result = validate(&data, vec![("email", rules![required(), email()])]);
385
386 assert!(result.is_ok());
387 }
388
389 #[test]
390 fn test_passes_and_fails() {
391 let data = json!({"email": "invalid"});
392
393 let validator = Validator::new(&data).rules("email", rules![email()]);
394
395 assert!(validator.fails());
396 }
397}