rustapi_validate/v2/
error.rs

1//! Error types for the v2 validation engine.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fmt;
6
7/// Error from a single validation rule.
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9pub struct RuleError {
10    /// The validation rule code (e.g., "email", "length", "range")
11    pub code: String,
12    /// Human-readable error message
13    pub message: String,
14    /// Optional parameters for message interpolation
15    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
16    pub params: HashMap<String, serde_json::Value>,
17}
18
19impl RuleError {
20    /// Create a new rule error.
21    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
22        Self {
23            code: code.into(),
24            message: message.into(),
25            params: HashMap::new(),
26        }
27    }
28
29    /// Create a rule error with parameters.
30    pub fn with_params(
31        code: impl Into<String>,
32        message: impl Into<String>,
33        params: HashMap<String, serde_json::Value>,
34    ) -> Self {
35        Self {
36            code: code.into(),
37            message: message.into(),
38            params,
39        }
40    }
41
42    /// Add a parameter to the error.
43    pub fn param(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
44        if let Ok(v) = serde_json::to_value(value) {
45            self.params.insert(key.into(), v);
46        }
47        self
48    }
49
50    /// Interpolate parameters into the message.
51    ///
52    /// Replaces `{param_name}` placeholders with actual values.
53    pub fn interpolate_message(&self) -> String {
54        let mut result = self.message.clone();
55        for (key, value) in &self.params {
56            let placeholder = format!("{{{}}}", key);
57            let replacement = match value {
58                serde_json::Value::String(s) => s.clone(),
59                serde_json::Value::Number(n) => n.to_string(),
60                serde_json::Value::Bool(b) => b.to_string(),
61                _ => value.to_string(),
62            };
63            result = result.replace(&placeholder, &replacement);
64        }
65        result
66    }
67}
68
69impl fmt::Display for RuleError {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        write!(f, "[{}] {}", self.code, self.interpolate_message())
72    }
73}
74
75impl std::error::Error for RuleError {}
76
77/// Collection of validation errors for multiple fields.
78#[derive(Debug, Clone, Default, Serialize, Deserialize)]
79pub struct ValidationErrors {
80    /// Map of field name to list of errors for that field
81    #[serde(flatten)]
82    pub fields: HashMap<String, Vec<RuleError>>,
83}
84
85impl ValidationErrors {
86    /// Create an empty validation errors collection.
87    pub fn new() -> Self {
88        Self {
89            fields: HashMap::new(),
90        }
91    }
92
93    /// Add an error for a field.
94    pub fn add(&mut self, field: impl Into<String>, error: RuleError) {
95        self.fields.entry(field.into()).or_default().push(error);
96    }
97
98    /// Add multiple errors for a field.
99    pub fn add_all(&mut self, field: impl Into<String>, errors: Vec<RuleError>) {
100        let field = field.into();
101        for error in errors {
102            self.add(field.clone(), error);
103        }
104    }
105
106    /// Merge another ValidationErrors into this one.
107    pub fn merge(&mut self, other: ValidationErrors) {
108        for (field, errors) in other.fields {
109            self.add_all(field, errors);
110        }
111    }
112
113    /// Check if there are any errors.
114    pub fn is_empty(&self) -> bool {
115        self.fields.is_empty()
116    }
117
118    /// Get the total number of errors.
119    pub fn len(&self) -> usize {
120        self.fields.values().map(|v| v.len()).sum()
121    }
122
123    /// Get errors for a specific field.
124    pub fn get(&self, field: &str) -> Option<&Vec<RuleError>> {
125        self.fields.get(field)
126    }
127
128    /// Convert to Result - Ok if no errors, Err otherwise.
129    pub fn into_result(self) -> Result<(), Self> {
130        if self.is_empty() {
131            Ok(())
132        } else {
133            Err(self)
134        }
135    }
136
137    /// Get all field names with errors.
138    pub fn field_names(&self) -> Vec<&str> {
139        self.fields.keys().map(|s| s.as_str()).collect()
140    }
141
142    /// Convert to the standard RustAPI error format.
143    pub fn to_api_error(&self) -> ApiValidationError {
144        let fields: Vec<FieldErrorResponse> = self
145            .fields
146            .iter()
147            .flat_map(|(field, errors)| {
148                errors.iter().map(move |e| FieldErrorResponse {
149                    field: field.clone(),
150                    code: e.code.clone(),
151                    message: e.interpolate_message(),
152                    params: if e.params.is_empty() {
153                        None
154                    } else {
155                        Some(e.params.clone())
156                    },
157                })
158            })
159            .collect();
160
161        ApiValidationError {
162            error: ErrorBody {
163                error_type: "validation_error".to_string(),
164                message: "Validation failed".to_string(),
165                fields,
166            },
167        }
168    }
169}
170
171impl fmt::Display for ValidationErrors {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        write!(f, "Validation failed: {} error(s)", self.len())
174    }
175}
176
177impl std::error::Error for ValidationErrors {}
178
179/// API response format for validation errors.
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct ApiValidationError {
182    pub error: ErrorBody,
183}
184
185/// Error body in API response.
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct ErrorBody {
188    #[serde(rename = "type")]
189    pub error_type: String,
190    pub message: String,
191    pub fields: Vec<FieldErrorResponse>,
192}
193
194/// Single field error in API response.
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct FieldErrorResponse {
197    pub field: String,
198    pub code: String,
199    pub message: String,
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub params: Option<HashMap<String, serde_json::Value>>,
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn rule_error_creation() {
210        let error = RuleError::new("email", "Invalid email format");
211        assert_eq!(error.code, "email");
212        assert_eq!(error.message, "Invalid email format");
213        assert!(error.params.is_empty());
214    }
215
216    #[test]
217    fn rule_error_with_params() {
218        let error = RuleError::new("length", "Must be between {min} and {max} characters")
219            .param("min", 3)
220            .param("max", 50);
221
222        assert_eq!(
223            error.interpolate_message(),
224            "Must be between 3 and 50 characters"
225        );
226    }
227
228    #[test]
229    fn validation_errors_add_and_get() {
230        let mut errors = ValidationErrors::new();
231        errors.add("email", RuleError::new("email", "Invalid email"));
232        errors.add("email", RuleError::new("required", "Email is required"));
233        errors.add("age", RuleError::new("range", "Age out of range"));
234
235        assert_eq!(errors.len(), 3);
236        assert_eq!(errors.get("email").unwrap().len(), 2);
237        assert_eq!(errors.get("age").unwrap().len(), 1);
238    }
239
240    #[test]
241    fn validation_errors_into_result() {
242        let errors = ValidationErrors::new();
243        assert!(errors.into_result().is_ok());
244
245        let mut errors = ValidationErrors::new();
246        errors.add("field", RuleError::new("code", "message"));
247        assert!(errors.into_result().is_err());
248    }
249
250    #[test]
251    fn validation_errors_to_api_error() {
252        let mut errors = ValidationErrors::new();
253        errors.add("email", RuleError::new("email", "Invalid email format"));
254
255        let api_error = errors.to_api_error();
256        assert_eq!(api_error.error.error_type, "validation_error");
257        assert_eq!(api_error.error.fields.len(), 1);
258        assert_eq!(api_error.error.fields[0].field, "email");
259    }
260
261    #[test]
262    fn validation_errors_merge() {
263        let mut errors1 = ValidationErrors::new();
264        errors1.add("email", RuleError::new("email", "Invalid"));
265
266        let mut errors2 = ValidationErrors::new();
267        errors2.add("age", RuleError::new("range", "Out of range"));
268
269        errors1.merge(errors2);
270        assert_eq!(errors1.len(), 2);
271        assert!(errors1.get("email").is_some());
272        assert!(errors1.get("age").is_some());
273    }
274}