1use domainstack::{ValidationError, Violation};
50use error_envelope::Error;
51
52pub trait IntoEnvelopeError {
53 fn into_envelope_error(self) -> Error;
54}
55
56impl IntoEnvelopeError for ValidationError {
57 fn into_envelope_error(self) -> Error {
58 let violation_count = self.violations.len();
59
60 let message = if violation_count == 1 {
61 format!("Validation failed: {}", self.violations[0].message)
62 } else {
63 format!("Validation failed with {} errors", violation_count)
64 };
65
66 let details = create_field_details(&self);
67
68 Error::validation(message)
69 .with_details(details)
70 .with_retryable(false)
71 }
72}
73
74fn create_field_details(validation_error: &ValidationError) -> serde_json::Value {
75 let field_map = validation_error.field_violations_map();
76
77 let mut fields = serde_json::Map::new();
78
79 for (path, violations) in field_map {
80 let violations_json: Vec<serde_json::Value> =
81 violations.into_iter().map(violation_to_json).collect();
82
83 fields.insert(path, serde_json::Value::Array(violations_json));
84 }
85
86 serde_json::json!({
87 "fields": fields
88 })
89}
90
91fn violation_to_json(violation: &Violation) -> serde_json::Value {
92 let mut obj = serde_json::Map::new();
93 obj.insert(
94 "code".to_string(),
95 serde_json::Value::String(violation.code.to_string()),
96 );
97 obj.insert(
98 "message".to_string(),
99 serde_json::Value::String(violation.message.clone()),
100 );
101
102 if !violation.meta.is_empty() {
103 let mut meta = serde_json::Map::new();
104 for (key, value) in violation.meta.iter() {
105 meta.insert(
106 key.to_string(),
107 serde_json::Value::String(value.to_string()),
108 );
109 }
110 obj.insert("meta".to_string(), serde_json::Value::Object(meta));
111 }
112
113 serde_json::Value::Object(obj)
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use domainstack::{Path, ValidationError};
120
121 #[test]
122 fn test_single_violation_conversion() {
123 let mut err = ValidationError::new();
124 err.push("email", "invalid_email", "Invalid email format");
125
126 let envelope = err.into_envelope_error();
127
128 assert_eq!(envelope.status, 400);
129 assert_eq!(envelope.message, "Validation failed: Invalid email format");
130 assert!(!envelope.retryable);
131
132 let details = envelope.details.expect("Should have details");
133 let fields = details["fields"]
134 .as_object()
135 .expect("Should have fields object");
136
137 assert!(fields.contains_key("email"));
138 let email_violations = fields["email"].as_array().expect("Should be array");
139 assert_eq!(email_violations.len(), 1);
140 assert_eq!(email_violations[0]["code"], "invalid_email");
141 assert_eq!(email_violations[0]["message"], "Invalid email format");
142 }
143
144 #[test]
145 fn test_multiple_violations_conversion() {
146 let mut err = ValidationError::new();
147 err.push("name", "min_length", "Must be at least 1 characters");
148 err.push("age", "out_of_range", "Must be between 18 and 120");
149
150 let envelope = err.into_envelope_error();
151
152 assert_eq!(envelope.status, 400);
153 assert_eq!(envelope.message, "Validation failed with 2 errors");
154
155 let details = envelope.details.expect("Should have details");
156 let fields = details["fields"]
157 .as_object()
158 .expect("Should have fields object");
159
160 assert_eq!(fields.len(), 2);
161 assert!(fields.contains_key("name"));
162 assert!(fields.contains_key("age"));
163 }
164
165 #[test]
166 fn test_nested_path_preservation() {
167 let mut err = ValidationError::new();
168 err.push(
169 Path::root().field("guest").field("email"),
170 "invalid_email",
171 "Invalid email format",
172 );
173
174 let envelope = err.into_envelope_error();
175
176 let details = envelope.details.expect("Should have details");
177 let fields = details["fields"]
178 .as_object()
179 .expect("Should have fields object");
180
181 assert!(fields.contains_key("guest.email"));
182 }
183
184 #[test]
185 fn test_collection_path_with_index() {
186 let mut err = ValidationError::new();
187 err.push(
188 Path::root().field("rooms").index(0).field("adults"),
189 "out_of_range",
190 "Must be between 1 and 4",
191 );
192 err.push(
193 Path::root().field("rooms").index(1).field("children"),
194 "out_of_range",
195 "Must be between 0 and 3",
196 );
197
198 let envelope = err.into_envelope_error();
199
200 let details = envelope.details.expect("Should have details");
201 let fields = details["fields"]
202 .as_object()
203 .expect("Should have fields object");
204
205 assert!(fields.contains_key("rooms[0].adults"));
206 assert!(fields.contains_key("rooms[1].children"));
207 }
208
209 #[test]
210 fn test_meta_field_inclusion() {
211 let mut err = ValidationError::new();
212 let mut violation = domainstack::Violation {
213 path: Path::from("age"),
214 code: "out_of_range",
215 message: "Must be between 18 and 120".to_string(),
216 meta: domainstack::Meta::new(),
217 };
218 violation.meta.insert("min", 18);
219 violation.meta.insert("max", 120);
220 err.violations.push(violation);
221
222 let envelope = err.into_envelope_error();
223
224 let details = envelope.details.expect("Should have details");
225 let fields = details["fields"]
226 .as_object()
227 .expect("Should have fields object");
228 let age_violations = fields["age"].as_array().expect("Should be array");
229
230 assert_eq!(age_violations[0]["meta"]["min"], "18");
231 assert_eq!(age_violations[0]["meta"]["max"], "120");
232 }
233
234 #[test]
235 fn test_multiple_violations_same_field() {
236 let mut err = ValidationError::new();
237 err.push("password", "no_uppercase", "Must contain uppercase letter");
238 err.push("password", "no_digit", "Must contain digit");
239
240 let envelope = err.into_envelope_error();
241
242 let details = envelope.details.expect("Should have details");
243 let fields = details["fields"]
244 .as_object()
245 .expect("Should have fields object");
246
247 assert_eq!(fields.len(), 1);
248 let password_violations = fields["password"].as_array().expect("Should be array");
249 assert_eq!(password_violations.len(), 2);
250 }
251
252 #[test]
253 fn test_deeply_nested_path() {
254 let mut err = ValidationError::new();
255 err.push(
256 Path::root()
257 .field("order")
258 .field("items")
259 .index(0)
260 .field("product")
261 .field("variants")
262 .index(2)
263 .field("sku"),
264 "invalid_sku",
265 "SKU format is invalid",
266 );
267
268 let envelope = err.into_envelope_error();
269
270 let details = envelope.details.expect("Should have details");
271 let fields = details["fields"]
272 .as_object()
273 .expect("Should have fields object");
274
275 assert!(fields.contains_key("order.items[0].product.variants[2].sku"));
276 }
277
278 #[test]
279 fn test_empty_message_string() {
280 let mut err = ValidationError::new();
281 err.push("field", "error_code", "");
282
283 let envelope = err.into_envelope_error();
284
285 let details = envelope.details.expect("Should have details");
286 let violations = details["fields"]["field"]
287 .as_array()
288 .expect("Should be array");
289 assert_eq!(violations[0]["message"], "");
290 }
291
292 #[test]
293 fn test_special_characters_in_message() {
294 let mut err = ValidationError::new();
295 err.push(
296 "field",
297 "error",
298 r#"Message with "quotes", 'apostrophes', and \backslash"#,
299 );
300
301 let envelope = err.into_envelope_error();
302
303 let details = envelope.details.expect("Should have details");
304 let violations = details["fields"]["field"]
305 .as_array()
306 .expect("Should be array");
307 assert_eq!(
308 violations[0]["message"],
309 r#"Message with "quotes", 'apostrophes', and \backslash"#
310 );
311 }
312
313 #[test]
314 fn test_meta_with_special_characters() {
315 let mut err = ValidationError::new();
316 let mut violation = domainstack::Violation {
317 path: Path::from("field"),
318 code: "error",
319 message: "Error".to_string(),
320 meta: domainstack::Meta::new(),
321 };
322 violation.meta.insert("key_with:colon", "value");
323 violation.meta.insert("pattern", r"^[\w]+$");
324 err.violations.push(violation);
325
326 let envelope = err.into_envelope_error();
327
328 let details = envelope.details.expect("Should have details");
329 let violations = details["fields"]["field"]
330 .as_array()
331 .expect("Should be array");
332 let meta = violations[0]["meta"].as_object().expect("Should have meta");
333 assert_eq!(meta["key_with:colon"], "value");
334 assert_eq!(meta["pattern"], r"^[\w]+$");
335 }
336
337 #[test]
338 fn test_three_violations_message_format() {
339 let mut err = ValidationError::new();
340 err.push("a", "err", "Error A");
341 err.push("b", "err", "Error B");
342 err.push("c", "err", "Error C");
343
344 let envelope = err.into_envelope_error();
345
346 assert_eq!(envelope.message, "Validation failed with 3 errors");
347 }
348
349 #[test]
350 fn test_empty_meta_not_included() {
351 let mut err = ValidationError::new();
352 err.push("field", "error_code", "Error message");
353
354 let envelope = err.into_envelope_error();
355
356 let details = envelope.details.expect("Should have details");
357 let violations = details["fields"]["field"]
358 .as_array()
359 .expect("Should be array");
360 assert!(violations[0].get("meta").is_none());
362 }
363
364 #[test]
365 fn test_large_number_of_violations() {
366 let mut err = ValidationError::new();
367 for i in 0..100 {
368 err.push(format!("field{}", i), "error", format!("Error {}", i));
369 }
370
371 let envelope = err.into_envelope_error();
372
373 assert_eq!(envelope.message, "Validation failed with 100 errors");
374 let details = envelope.details.expect("Should have details");
375 let fields = details["fields"]
376 .as_object()
377 .expect("Should have fields object");
378 assert_eq!(fields.len(), 100);
379 }
380
381 #[test]
382 fn test_root_path_violation() {
383 let mut err = ValidationError::new();
384 let violation = domainstack::Violation {
385 path: Path::root(),
386 code: "invalid_object",
387 message: "Object is invalid".to_string(),
388 meta: domainstack::Meta::new(),
389 };
390 err.violations.push(violation);
391
392 let envelope = err.into_envelope_error();
393
394 let details = envelope.details.expect("Should have details");
395 let fields = details["fields"]
396 .as_object()
397 .expect("Should have fields object");
398 assert!(fields.contains_key(""));
400 }
401}