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_with_locale(&self, locale: Option<&str>) -> String {
55 let msg = crate::v2::i18n::translate(&self.message, locale);
56 let mut result = msg;
57
58 for (key, value) in &self.params {
59 let placeholder = format!("{{{}}}", key);
60 let p_placeholder = format!("%{{{}}}", key); let replacement = match value {
63 serde_json::Value::String(s) => s.clone(),
64 serde_json::Value::Number(n) => n.to_string(),
65 serde_json::Value::Bool(b) => b.to_string(),
66 _ => value.to_string(),
67 };
68 result = result.replace(&p_placeholder, &replacement);
70 result = result.replace(&placeholder, &replacement);
71 }
72 result
73 }
74
75 pub fn interpolate_message(&self) -> String {
77 self.interpolate_with_locale(None)
78 }
79}
80
81impl fmt::Display for RuleError {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 write!(f, "[{}] {}", self.code, self.interpolate_message())
84 }
85}
86
87impl std::error::Error for RuleError {}
88
89#[derive(Debug, Clone, Default, Serialize, Deserialize)]
91pub struct ValidationErrors {
92 #[serde(flatten)]
94 pub fields: HashMap<String, Vec<RuleError>>,
95}
96
97impl ValidationErrors {
98 pub fn new() -> Self {
100 Self {
101 fields: HashMap::new(),
102 }
103 }
104
105 pub fn add(&mut self, field: impl Into<String>, error: RuleError) {
107 self.fields.entry(field.into()).or_default().push(error);
108 }
109
110 pub fn add_all(&mut self, field: impl Into<String>, errors: Vec<RuleError>) {
112 let field = field.into();
113 for error in errors {
114 self.add(field.clone(), error);
115 }
116 }
117
118 pub fn merge(&mut self, other: ValidationErrors) {
120 for (field, errors) in other.fields {
121 self.add_all(field, errors);
122 }
123 }
124
125 pub fn is_empty(&self) -> bool {
127 self.fields.is_empty()
128 }
129
130 pub fn len(&self) -> usize {
132 self.fields.values().map(|v| v.len()).sum()
133 }
134
135 pub fn get(&self, field: &str) -> Option<&Vec<RuleError>> {
137 self.fields.get(field)
138 }
139
140 pub fn into_result(self) -> Result<(), Self> {
142 if self.is_empty() {
143 Ok(())
144 } else {
145 Err(self)
146 }
147 }
148
149 pub fn field_names(&self) -> Vec<&str> {
151 self.fields.keys().map(|s| s.as_str()).collect()
152 }
153
154 pub fn to_api_error_with_locale(&self, locale: Option<&str>) -> ApiValidationError {
156 let fields: Vec<FieldErrorResponse> = self
157 .fields
158 .iter()
159 .flat_map(|(field, errors)| {
160 errors.iter().map(move |e| FieldErrorResponse {
161 field: field.clone(),
162 code: e.code.clone(),
163 message: e.interpolate_with_locale(locale),
164 params: if e.params.is_empty() {
165 None
166 } else {
167 Some(e.params.clone())
168 },
169 })
170 })
171 .collect();
172
173 ApiValidationError {
174 error: ErrorBody {
175 error_type: "validation_error".to_string(),
176 message: "Validation failed".to_string(),
177 fields,
178 },
179 }
180 }
181
182 pub fn to_api_error(&self) -> ApiValidationError {
184 self.to_api_error_with_locale(None)
185 }
186}
187
188impl fmt::Display for ValidationErrors {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 write!(f, "Validation failed: {} error(s)", self.len())
191 }
192}
193
194impl std::error::Error for ValidationErrors {}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct ApiValidationError {
199 pub error: ErrorBody,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct ErrorBody {
205 #[serde(rename = "type")]
206 pub error_type: String,
207 pub message: String,
208 pub fields: Vec<FieldErrorResponse>,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct FieldErrorResponse {
214 pub field: String,
215 pub code: String,
216 pub message: String,
217 #[serde(skip_serializing_if = "Option::is_none")]
218 pub params: Option<HashMap<String, serde_json::Value>>,
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn rule_error_creation() {
227 let error = RuleError::new("email", "Invalid email format");
228 assert_eq!(error.code, "email");
229 assert_eq!(error.message, "Invalid email format");
230 assert!(error.params.is_empty());
231 }
232
233 #[test]
234 fn rule_error_with_params() {
235 let error = RuleError::new("length", "Must be between {min} and {max} characters")
236 .param("min", 3)
237 .param("max", 50);
238
239 assert_eq!(
240 error.interpolate_message(),
241 "Must be between 3 and 50 characters"
242 );
243 }
244
245 #[test]
246 fn validation_errors_add_and_get() {
247 let mut errors = ValidationErrors::new();
248 errors.add("email", RuleError::new("email", "Invalid email"));
249 errors.add("email", RuleError::new("required", "Email is required"));
250 errors.add("age", RuleError::new("range", "Age out of range"));
251
252 assert_eq!(errors.len(), 3);
253 assert_eq!(errors.get("email").unwrap().len(), 2);
254 assert_eq!(errors.get("age").unwrap().len(), 1);
255 }
256
257 #[test]
258 fn validation_errors_into_result() {
259 let errors = ValidationErrors::new();
260 assert!(errors.into_result().is_ok());
261
262 let mut errors = ValidationErrors::new();
263 errors.add("field", RuleError::new("code", "message"));
264 assert!(errors.into_result().is_err());
265 }
266
267 #[test]
268 fn validation_errors_to_api_error() {
269 let mut errors = ValidationErrors::new();
270 errors.add("email", RuleError::new("email", "Invalid email format"));
271
272 let api_error = errors.to_api_error();
273 assert_eq!(api_error.error.error_type, "validation_error");
274 assert_eq!(api_error.error.fields.len(), 1);
275 assert_eq!(api_error.error.fields[0].field, "email");
276 }
277
278 #[test]
279 fn validation_errors_merge() {
280 let mut errors1 = ValidationErrors::new();
281 errors1.add("email", RuleError::new("email", "Invalid"));
282
283 let mut errors2 = ValidationErrors::new();
284 errors2.add("age", RuleError::new("range", "Out of range"));
285
286 errors1.merge(errors2);
287 assert_eq!(errors1.len(), 2);
288 assert!(errors1.get("email").is_some());
289 assert!(errors1.get("age").is_some());
290 }
291}