elif_validation/
error.rs

1//! Validation error types and handling
2
3use std::collections::HashMap;
4use std::fmt;
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8pub type ValidationResult<T> = Result<T, ValidationErrors>;
9
10/// Individual validation error for a specific field
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub struct ValidationError {
13    /// The field that failed validation
14    pub field: String,
15    /// Human-readable error message
16    pub message: String,
17    /// Error code for programmatic handling
18    pub code: String,
19    /// Additional context or hints
20    pub context: Option<serde_json::Value>,
21}
22
23impl ValidationError {
24    /// Create a new validation error
25    pub fn new(field: impl Into<String>, message: impl Into<String>) -> Self {
26        Self {
27            field: field.into(),
28            message: message.into(),
29            code: "validation_failed".to_string(),
30            context: None,
31        }
32    }
33
34    /// Create a validation error with a specific code
35    pub fn with_code(field: impl Into<String>, message: impl Into<String>, code: impl Into<String>) -> Self {
36        Self {
37            field: field.into(),
38            message: message.into(),
39            code: code.into(),
40            context: None,
41        }
42    }
43
44    /// Create a validation error with additional context
45    pub fn with_context(field: impl Into<String>, message: impl Into<String>, context: serde_json::Value) -> Self {
46        Self {
47            field: field.into(),
48            message: message.into(),
49            code: "validation_failed".to_string(),
50            context: Some(context),
51        }
52    }
53
54    /// Set the error code
55    pub fn code(mut self, code: impl Into<String>) -> Self {
56        self.code = code.into();
57        self
58    }
59
60    /// Set additional context
61    pub fn context(mut self, context: serde_json::Value) -> Self {
62        self.context = Some(context);
63        self
64    }
65}
66
67impl fmt::Display for ValidationError {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        write!(f, "{}: {}", self.field, self.message)
70    }
71}
72
73/// Collection of validation errors, typically one per field
74#[derive(Debug, Clone, Serialize, Deserialize, Error)]
75pub struct ValidationErrors {
76    /// Map of field names to their validation errors
77    pub errors: HashMap<String, Vec<ValidationError>>,
78}
79
80impl ValidationErrors {
81    /// Create a new empty validation errors collection
82    pub fn new() -> Self {
83        Self {
84            errors: HashMap::new(),
85        }
86    }
87
88    /// Add a single validation error
89    pub fn add(&mut self, error: ValidationError) {
90        self.errors
91            .entry(error.field.clone())
92            .or_default()
93            .push(error);
94    }
95
96    /// Add multiple validation errors for a field
97    pub fn add_errors(&mut self, field: impl Into<String>, errors: Vec<ValidationError>) {
98        let field = field.into();
99        self.errors
100            .entry(field)
101            .or_default()
102            .extend(errors);
103    }
104
105    /// Add a simple validation error with field and message
106    pub fn add_error(&mut self, field: impl Into<String>, message: impl Into<String>) {
107        let error = ValidationError::new(field.into(), message);
108        self.add(error);
109    }
110
111    /// Check if there are any validation errors
112    pub fn is_empty(&self) -> bool {
113        self.errors.is_empty()
114    }
115
116    /// Get the number of fields with errors
117    pub fn len(&self) -> usize {
118        self.errors.len()
119    }
120
121    /// Get total number of validation errors across all fields
122    pub fn total_errors(&self) -> usize {
123        self.errors.values().map(|v| v.len()).sum()
124    }
125
126    /// Get errors for a specific field
127    pub fn get_field_errors(&self, field: &str) -> Option<&Vec<ValidationError>> {
128        self.errors.get(field)
129    }
130
131    /// Check if a specific field has errors
132    pub fn has_field_errors(&self, field: &str) -> bool {
133        self.errors.contains_key(field) && !self.errors[field].is_empty()
134    }
135
136    /// Merge another ValidationErrors into this one
137    pub fn merge(&mut self, other: ValidationErrors) {
138        for (field, errors) in other.errors {
139            self.errors
140                .entry(field)
141                .or_default()
142                .extend(errors);
143        }
144    }
145
146    /// Create ValidationErrors from a single error
147    pub fn from_error(error: ValidationError) -> Self {
148        let mut errors = Self::new();
149        errors.add(error);
150        errors
151    }
152
153    /// Convert to a JSON-serializable format for API responses
154    pub fn to_json(&self) -> serde_json::Value {
155        serde_json::json!({
156            "error": {
157                "code": "validation_failed",
158                "message": "Validation failed",
159                "fields": self.errors
160            }
161        })
162    }
163}
164
165impl Default for ValidationErrors {
166    fn default() -> Self {
167        Self::new()
168    }
169}
170
171impl fmt::Display for ValidationErrors {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        if self.errors.is_empty() {
174            write!(f, "No validation errors")
175        } else {
176            write!(f, "Validation failed for {} field(s):", self.errors.len())?;
177            for (field, field_errors) in &self.errors {
178                for error in field_errors {
179                    write!(f, "\n  {}: {}", field, error.message)?;
180                }
181            }
182            Ok(())
183        }
184    }
185}
186
187impl From<ValidationError> for ValidationErrors {
188    fn from(error: ValidationError) -> Self {
189        Self::from_error(error)
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_validation_error_creation() {
199        let error = ValidationError::new("email", "Invalid email format");
200        assert_eq!(error.field, "email");
201        assert_eq!(error.message, "Invalid email format");
202        assert_eq!(error.code, "validation_failed");
203        assert!(error.context.is_none());
204    }
205
206    #[test]
207    fn test_validation_error_with_code() {
208        let error = ValidationError::with_code("age", "Must be positive", "positive_number");
209        assert_eq!(error.code, "positive_number");
210    }
211
212    #[test]
213    fn test_validation_errors_collection() {
214        let mut errors = ValidationErrors::new();
215        
216        errors.add_error("email", "Invalid format");
217        errors.add_error("age", "Must be positive");
218        errors.add_error("email", "Already exists");
219
220        assert_eq!(errors.len(), 2); // Two fields with errors
221        assert_eq!(errors.total_errors(), 3); // Three total errors
222        assert!(errors.has_field_errors("email"));
223        assert!(errors.has_field_errors("age"));
224        assert!(!errors.has_field_errors("name"));
225
226        let email_errors = errors.get_field_errors("email").unwrap();
227        assert_eq!(email_errors.len(), 2);
228    }
229
230    #[test]
231    fn test_validation_errors_merge() {
232        let mut errors1 = ValidationErrors::new();
233        errors1.add_error("field1", "Error 1");
234
235        let mut errors2 = ValidationErrors::new();
236        errors2.add_error("field2", "Error 2");
237        errors2.add_error("field1", "Error 3");
238
239        errors1.merge(errors2);
240
241        assert_eq!(errors1.len(), 2);
242        assert_eq!(errors1.total_errors(), 3);
243        assert_eq!(errors1.get_field_errors("field1").unwrap().len(), 2);
244    }
245}