Skip to main content

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    /// Localize validation errors using a translator.
148    pub fn localize<T: Translator>(&self, translator: &T) -> Self {
149        let fields = self
150            .fields
151            .iter()
152            .map(|f| {
153                let message = translator
154                    .translate(&f.code, &f.field, f.params.as_ref())
155                    .unwrap_or_else(|| f.message.clone());
156
157                FieldError {
158                    field: f.field.clone(),
159                    code: f.code.clone(),
160                    message,
161                    params: f.params.clone(),
162                }
163            })
164            .collect();
165
166        Self {
167            fields,
168            message: self.message.clone(),
169        }
170    }
171}
172
173impl fmt::Display for ValidationError {
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        write!(f, "{}: {} field error(s)", self.message, self.fields.len())
176    }
177}
178
179impl std::error::Error for ValidationError {}
180
181impl Serialize for ValidationError {
182    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
183    where
184        S: serde::Serializer,
185    {
186        let wrapper = ErrorWrapper {
187            error: ErrorBody {
188                error_type: "validation_error".to_string(),
189                message: self.message.clone(),
190                fields: self.fields.clone(),
191            },
192        };
193        wrapper.serialize(serializer)
194    }
195}
196
197impl<'de> Deserialize<'de> for ValidationError {
198    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
199    where
200        D: serde::Deserializer<'de>,
201    {
202        let wrapper = ErrorWrapper::deserialize(deserializer)?;
203        Ok(Self {
204            fields: wrapper.error.fields,
205            message: wrapper.error.message,
206        })
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn field_error_creation() {
216        let error = FieldError::new("email", "email", "Invalid email format");
217        assert_eq!(error.field, "email");
218        assert_eq!(error.code, "email");
219        assert_eq!(error.message, "Invalid email format");
220        assert!(error.params.is_none());
221    }
222
223    #[test]
224    fn validation_error_serialization() {
225        let error = ValidationError::new(vec![FieldError::new(
226            "email",
227            "email",
228            "Invalid email format",
229        )]);
230
231        let json = serde_json::to_value(&error).unwrap();
232
233        assert_eq!(json["error"]["type"], "validation_error");
234        assert_eq!(json["error"]["message"], "Validation failed");
235        assert_eq!(json["error"]["fields"][0]["field"], "email");
236    }
237
238    #[test]
239    fn validation_error_display() {
240        let error = ValidationError::new(vec![
241            FieldError::new("email", "email", "Invalid email"),
242            FieldError::new("age", "range", "Out of range"),
243        ]);
244
245        assert_eq!(error.to_string(), "Validation failed: 2 field error(s)");
246    }
247}