rustapi_validate/
error.rs

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