1use serde_json::{json, Value};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ValidationErrorCode {
11 MissingField,
13 InvalidFormat,
15 OutOfRange,
17 NotAllowed,
19 TooShort,
21 TooLong,
23 TooFewItems,
25 TooManyItems,
27 PatternMismatch,
29 SecurityViolation,
31 TypeMismatch,
33 CustomValidation,
35}
36
37impl ValidationErrorCode {
38 pub fn as_str(&self) -> &'static str {
40 match self {
41 Self::MissingField => "missing_field",
42 Self::InvalidFormat => "invalid_format",
43 Self::OutOfRange => "out_of_range",
44 Self::NotAllowed => "not_allowed",
45 Self::TooShort => "too_short",
46 Self::TooLong => "too_long",
47 Self::TooFewItems => "too_few_items",
48 Self::TooManyItems => "too_many_items",
49 Self::PatternMismatch => "pattern_mismatch",
50 Self::SecurityViolation => "security_violation",
51 Self::TypeMismatch => "type_mismatch",
52 Self::CustomValidation => "custom_validation",
53 }
54 }
55
56 pub fn description(&self) -> &'static str {
58 match self {
59 Self::MissingField => "Required field is missing",
60 Self::InvalidFormat => "Field value has invalid format",
61 Self::OutOfRange => "Value is outside the allowed range",
62 Self::NotAllowed => "Value is not in the allowed set",
63 Self::TooShort => "Value is too short",
64 Self::TooLong => "Value is too long",
65 Self::TooFewItems => "Collection has too few items",
66 Self::TooManyItems => "Collection has too many items",
67 Self::PatternMismatch => "Value does not match the required pattern",
68 Self::SecurityViolation => "Security constraint violated",
69 Self::TypeMismatch => "Value has incorrect type",
70 Self::CustomValidation => "Custom validation failed",
71 }
72 }
73}
74
75impl std::fmt::Display for ValidationErrorCode {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 write!(f, "{}", self.as_str())
78 }
79}
80
81#[derive(Debug, Clone)]
83pub struct ValidationError {
84 pub code: ValidationErrorCode,
86 pub field: String,
88 pub message: String,
90 pub expected: Option<String>,
92 pub actual: Option<Value>,
94 pub context: Option<Value>,
96}
97
98impl ValidationError {
99 pub fn new(code: ValidationErrorCode, field: impl Into<String>) -> Self {
101 let field = field.into();
102 let message = format!("{} for field '{}'", code.description(), field);
103 Self {
104 code,
105 field,
106 message,
107 expected: None,
108 actual: None,
109 context: None,
110 }
111 }
112
113 pub fn expected(mut self, expected: impl Into<String>) -> Self {
115 self.expected = Some(expected.into());
116 self
117 }
118
119 pub fn actual(mut self, actual: impl Into<Value>) -> Self {
121 self.actual = Some(actual.into());
122 self
123 }
124
125 pub fn context(mut self, context: impl Into<Value>) -> Self {
127 self.context = Some(context.into());
128 self
129 }
130
131 pub fn message(mut self, message: impl Into<String>) -> Self {
133 self.message = message.into();
134 self
135 }
136
137 pub fn to_json(&self) -> Value {
139 let mut obj = json!({
140 "code": self.code.as_str(),
141 "field": self.field,
142 "message": self.message,
143 "elicit": true,
144 });
145
146 if let Some(expected) = &self.expected {
147 obj["expected"] = json!(expected);
148 }
149
150 if let Some(actual) = &self.actual {
151 obj["actual"] = actual.clone();
152 }
153
154 if let Some(context) = &self.context {
155 obj["context"] = context.clone();
156 }
157
158 obj
159 }
160
161 pub fn to_error(&self) -> crate::Error {
163 crate::Error::Validation(self.to_json().to_string())
164 }
165}
166
167pub trait IntoValidationError {
169 fn into_validation_error(self, code: ValidationErrorCode, field: &str) -> crate::Error;
171}
172
173impl<T, E> IntoValidationError for Result<T, E>
174where
175 E: std::fmt::Display,
176{
177 fn into_validation_error(self, code: ValidationErrorCode, field: &str) -> crate::Error {
178 match self {
179 Ok(_) => panic!("Cannot convert Ok result to validation error"),
180 Err(e) => ValidationError::new(code, field)
181 .message(format!("{}: {}", code.description(), e))
182 .to_error(),
183 }
184 }
185}
186
187pub fn missing_field(field: &str) -> crate::Error {
191 ValidationError::new(ValidationErrorCode::MissingField, field)
192 .expected("This field is required")
193 .to_error()
194}
195
196pub fn invalid_format(field: &str, expected: &str) -> crate::Error {
198 ValidationError::new(ValidationErrorCode::InvalidFormat, field)
199 .expected(expected)
200 .to_error()
201}
202
203pub fn out_of_range<T: std::fmt::Display>(
205 field: &str,
206 value: T,
207 min: Option<T>,
208 max: Option<T>,
209) -> crate::Error {
210 let expected = match (min, max) {
211 (Some(min), Some(max)) => format!("Value between {} and {}", min, max),
212 (Some(min), None) => format!("Value >= {}", min),
213 (None, Some(max)) => format!("Value <= {}", max),
214 (None, None) => "Value within valid range".to_string(),
215 };
216
217 ValidationError::new(ValidationErrorCode::OutOfRange, field)
218 .expected(expected)
219 .actual(json!(value.to_string()))
220 .to_error()
221}
222
223pub fn not_allowed<T: std::fmt::Display>(field: &str, value: T, allowed: &[T]) -> crate::Error {
225 let allowed_str = allowed
226 .iter()
227 .map(|v| v.to_string())
228 .collect::<Vec<_>>()
229 .join(", ");
230
231 ValidationError::new(ValidationErrorCode::NotAllowed, field)
232 .expected(format!("One of: {}", allowed_str))
233 .actual(json!(value.to_string()))
234 .to_error()
235}
236
237pub fn pattern_mismatch(field: &str, pattern: &str) -> crate::Error {
239 ValidationError::new(ValidationErrorCode::PatternMismatch, field)
240 .expected(format!("Match pattern: {}", pattern))
241 .to_error()
242}
243
244pub fn security_violation(field: &str, reason: &str) -> crate::Error {
246 ValidationError::new(ValidationErrorCode::SecurityViolation, field)
247 .message(format!("Security violation: {}", reason))
248 .to_error()
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn test_validation_error_json() {
257 let error = ValidationError::new(ValidationErrorCode::OutOfRange, "age")
258 .expected("18-120")
259 .actual(json!(150))
260 .message("Age must be between 18 and 120");
261
262 let json = error.to_json();
263 assert_eq!(json["code"], "out_of_range");
264 assert_eq!(json["field"], "age");
265 assert_eq!(json["expected"], "18-120");
266 assert_eq!(json["actual"], 150);
267 assert_eq!(json["elicit"], true);
268 }
269
270 #[test]
271 fn test_convenience_functions() {
272 let error = missing_field("email");
273 assert!(error.to_string().contains("missing_field"));
274
275 let error = invalid_format("email", "user@example.com");
276 assert!(error.to_string().contains("invalid_format"));
277
278 let error = out_of_range("age", 200, Some(18), Some(120));
279 assert!(error.to_string().contains("out_of_range"));
280 }
281}