1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fmt;
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9pub struct RuleError {
10 pub code: String,
12 pub message: String,
14 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
16 pub params: HashMap<String, serde_json::Value>,
17}
18
19impl RuleError {
20 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 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 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 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
79pub struct ValidationErrors {
80 #[serde(flatten)]
82 pub fields: HashMap<String, Vec<RuleError>>,
83}
84
85impl ValidationErrors {
86 pub fn new() -> Self {
88 Self {
89 fields: HashMap::new(),
90 }
91 }
92
93 pub fn add(&mut self, field: impl Into<String>, error: RuleError) {
95 self.fields.entry(field.into()).or_default().push(error);
96 }
97
98 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 pub fn merge(&mut self, other: ValidationErrors) {
108 for (field, errors) in other.fields {
109 self.add_all(field, errors);
110 }
111 }
112
113 pub fn is_empty(&self) -> bool {
115 self.fields.is_empty()
116 }
117
118 pub fn len(&self) -> usize {
120 self.fields.values().map(|v| v.len()).sum()
121 }
122
123 pub fn get(&self, field: &str) -> Option<&Vec<RuleError>> {
125 self.fields.get(field)
126 }
127
128 pub fn into_result(self) -> Result<(), Self> {
130 if self.is_empty() {
131 Ok(())
132 } else {
133 Err(self)
134 }
135 }
136
137 pub fn field_names(&self) -> Vec<&str> {
139 self.fields.keys().map(|s| s.as_str()).collect()
140 }
141
142 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#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct ApiValidationError {
182 pub error: ErrorBody,
183}
184
185#[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#[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}