rustapi_validate/
error.rs

1//! Validation error types and JSON error format.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fmt;
6
7/// A single field validation error.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct FieldError {
10    /// The field name that failed validation
11    pub field: String,
12    /// The validation rule code (e.g., "email", "length", "range")
13    pub code: String,
14    /// Human-readable error message
15    pub message: String,
16    /// Optional additional parameters (e.g., min/max values)
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub params: Option<HashMap<String, serde_json::Value>>,
19}
20
21impl FieldError {
22    /// Create a new field error.
23    pub fn new(
24        field: impl Into<String>,
25        code: impl Into<String>,
26        message: impl Into<String>,
27    ) -> Self {
28        Self {
29            field: field.into(),
30            code: code.into(),
31            message: message.into(),
32            params: None,
33        }
34    }
35
36    /// Create a field error with parameters.
37    pub fn with_params(
38        field: impl Into<String>,
39        code: impl Into<String>,
40        message: impl Into<String>,
41        params: HashMap<String, serde_json::Value>,
42    ) -> Self {
43        Self {
44            field: field.into(),
45            code: code.into(),
46            message: message.into(),
47            params: Some(params),
48        }
49    }
50}
51
52/// Internal error structure for JSON serialization.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54struct ErrorBody {
55    #[serde(rename = "type")]
56    error_type: String,
57    message: String,
58    fields: Vec<FieldError>,
59}
60
61/// Wrapper for the error response format.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63struct ErrorWrapper {
64    error: ErrorBody,
65}
66
67/// Validation error containing all field errors.
68///
69/// This type serializes to the standard RustAPI error format:
70///
71/// ```json
72/// {
73///   "error": {
74///     "type": "validation_error",
75///     "message": "Validation failed",
76///     "fields": [...]
77///   }
78/// }
79/// ```
80#[derive(Debug, Clone)]
81pub struct ValidationError {
82    /// Collection of field-level validation errors
83    pub fields: Vec<FieldError>,
84    /// Custom error message (default: "Validation failed")
85    pub message: String,
86}
87
88impl ValidationError {
89    /// Create a new validation error with field errors.
90    pub fn new(fields: Vec<FieldError>) -> Self {
91        Self {
92            fields,
93            message: "Validation failed".to_string(),
94        }
95    }
96
97    /// Create a validation error with a custom message.
98    pub fn with_message(fields: Vec<FieldError>, message: impl Into<String>) -> Self {
99        Self {
100            fields,
101            message: message.into(),
102        }
103    }
104
105    /// Create a validation error for a single field.
106    pub fn field(
107        field: impl Into<String>,
108        code: impl Into<String>,
109        message: impl Into<String>,
110    ) -> Self {
111        Self::new(vec![FieldError::new(field, code, message)])
112    }
113
114    /// Check if there are any validation errors.
115    pub fn is_empty(&self) -> bool {
116        self.fields.is_empty()
117    }
118
119    /// Get the number of field errors.
120    pub fn len(&self) -> usize {
121        self.fields.len()
122    }
123
124    /// Add a field error.
125    pub fn add(&mut self, error: FieldError) {
126        self.fields.push(error);
127    }
128
129    /// Convert validator errors to our format.
130    pub fn from_validator_errors(errors: validator::ValidationErrors) -> Self {
131        let mut field_errors = Vec::new();
132
133        for (field, error_kinds) in errors.field_errors() {
134            for error in error_kinds {
135                let code = error.code.to_string();
136                let message = error
137                    .message
138                    .as_ref()
139                    .map(|m| m.to_string())
140                    .unwrap_or_else(|| format!("Validation failed for field '{}'", field));
141
142                let params = if error.params.is_empty() {
143                    None
144                } else {
145                    let mut map = HashMap::new();
146                    for (key, value) in &error.params {
147                        if let Ok(json_value) = serde_json::to_value(value) {
148                            map.insert(key.to_string(), json_value);
149                        }
150                    }
151                    Some(map)
152                };
153
154                field_errors.push(FieldError {
155                    field: field.to_string(),
156                    code,
157                    message,
158                    params,
159                });
160            }
161        }
162
163        Self::new(field_errors)
164    }
165}
166
167impl fmt::Display for ValidationError {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        write!(f, "{}: {} field error(s)", self.message, self.fields.len())
170    }
171}
172
173impl std::error::Error for ValidationError {}
174
175impl Serialize for ValidationError {
176    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
177    where
178        S: serde::Serializer,
179    {
180        let wrapper = ErrorWrapper {
181            error: ErrorBody {
182                error_type: "validation_error".to_string(),
183                message: self.message.clone(),
184                fields: self.fields.clone(),
185            },
186        };
187        wrapper.serialize(serializer)
188    }
189}
190
191impl<'de> Deserialize<'de> for ValidationError {
192    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
193    where
194        D: serde::Deserializer<'de>,
195    {
196        let wrapper = ErrorWrapper::deserialize(deserializer)?;
197        Ok(Self {
198            fields: wrapper.error.fields,
199            message: wrapper.error.message,
200        })
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn field_error_creation() {
210        let error = FieldError::new("email", "email", "Invalid email format");
211        assert_eq!(error.field, "email");
212        assert_eq!(error.code, "email");
213        assert_eq!(error.message, "Invalid email format");
214        assert!(error.params.is_none());
215    }
216
217    #[test]
218    fn validation_error_serialization() {
219        let error = ValidationError::new(vec![FieldError::new(
220            "email",
221            "email",
222            "Invalid email format",
223        )]);
224
225        let json = serde_json::to_value(&error).unwrap();
226
227        assert_eq!(json["error"]["type"], "validation_error");
228        assert_eq!(json["error"]["message"], "Validation failed");
229        assert_eq!(json["error"]["fields"][0]["field"], "email");
230    }
231
232    #[test]
233    fn validation_error_display() {
234        let error = ValidationError::new(vec![
235            FieldError::new("email", "email", "Invalid email"),
236            FieldError::new("age", "range", "Out of range"),
237        ]);
238
239        assert_eq!(error.to_string(), "Validation failed: 2 field error(s)");
240    }
241}