Skip to main content

ferro_rs/validation/
error.rs

1//! Validation error types.
2
3use serde::Serialize;
4use std::collections::HashMap;
5
6/// A collection of validation errors.
7#[derive(Debug, Clone, Default, Serialize)]
8pub struct ValidationError {
9    /// Field-specific errors.
10    errors: HashMap<String, Vec<String>>,
11}
12
13impl ValidationError {
14    /// Create a new empty validation error.
15    pub fn new() -> Self {
16        Self::default()
17    }
18
19    /// Add an error message for a field.
20    pub fn add(&mut self, field: &str, message: impl Into<String>) {
21        self.errors
22            .entry(field.to_string())
23            .or_default()
24            .push(message.into());
25    }
26
27    /// Check if there are any errors.
28    pub fn is_empty(&self) -> bool {
29        self.errors.is_empty()
30    }
31
32    /// Check if a specific field has errors.
33    pub fn has(&self, field: &str) -> bool {
34        self.errors.contains_key(field)
35    }
36
37    /// Get errors for a specific field.
38    pub fn get(&self, field: &str) -> Option<&Vec<String>> {
39        self.errors.get(field)
40    }
41
42    /// Get the first error for a field.
43    pub fn first(&self, field: &str) -> Option<&String> {
44        self.errors.get(field).and_then(|v| v.first())
45    }
46
47    /// Get all errors as a map.
48    pub fn all(&self) -> &HashMap<String, Vec<String>> {
49        &self.errors
50    }
51
52    /// Get the total number of errors.
53    pub fn count(&self) -> usize {
54        self.errors.values().map(|v| v.len()).sum()
55    }
56
57    /// Get all error messages as a flat list.
58    pub fn messages(&self) -> Vec<&String> {
59        self.errors.values().flatten().collect()
60    }
61
62    /// Consume the error and return the inner HashMap of field -> messages.
63    /// Useful for passing errors to templates.
64    pub fn into_messages(self) -> HashMap<String, Vec<String>> {
65        self.errors
66    }
67
68    /// Convert to JSON-compatible format for API responses.
69    pub fn to_json(&self) -> serde_json::Value {
70        serde_json::json!({
71            "message": "The given data was invalid.",
72            "errors": self.errors
73        })
74    }
75}
76
77impl std::fmt::Display for ValidationError {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        let messages: Vec<String> = self
80            .errors
81            .iter()
82            .flat_map(|(field, msgs)| msgs.iter().map(move |m| format!("{field}: {m}")))
83            .collect();
84        write!(f, "{}", messages.join(", "))
85    }
86}
87
88impl std::error::Error for ValidationError {}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_validation_error_add() {
96        let mut errors = ValidationError::new();
97        errors.add("email", "The email field is required.");
98        errors.add("email", "The email must be a valid email address.");
99        errors.add("password", "The password must be at least 8 characters.");
100
101        assert!(!errors.is_empty());
102        assert!(errors.has("email"));
103        assert!(errors.has("password"));
104        assert!(!errors.has("name"));
105        assert_eq!(errors.count(), 3);
106    }
107
108    #[test]
109    fn test_validation_error_first() {
110        let mut errors = ValidationError::new();
111        errors.add("email", "First error");
112        errors.add("email", "Second error");
113
114        assert_eq!(errors.first("email"), Some(&"First error".to_string()));
115        assert_eq!(errors.first("name"), None);
116    }
117
118    #[test]
119    fn test_validation_error_to_json() {
120        let mut errors = ValidationError::new();
121        errors.add("email", "Required");
122
123        let json = errors.to_json();
124        assert!(json.get("message").is_some());
125        assert!(json.get("errors").is_some());
126    }
127}