1use std::fmt;
7
8#[derive(Debug, Clone)]
10pub enum ValidationError {
11 MissingRequiredField {
13 field_path: String,
15 },
16
17 InvalidType {
19 field_path: String,
21 expected: String,
23 found: String,
25 },
26
27 InvalidEnumValue {
29 field_path: String,
31 value: String,
33 allowed: Vec<String>,
35 },
36
37 OutOfRange {
39 field_path: String,
41 value: String,
43 min: Option<String>,
45 max: Option<String>,
47 },
48
49 InvalidArrayLength {
51 field_path: String,
53 length: usize,
55 min: Option<usize>,
57 max: Option<usize>,
59 },
60
61 PatternMismatch {
63 field_path: String,
65 pattern: String,
67 },
68
69 ConditionalRequirementFailed {
71 field_path: String,
73 condition: String,
75 },
76
77 SchemaParseError(String),
79
80 TomlParseError(String),
82
83 Multiple(Vec<ValidationError>),
85
86 UnexpectedTable {
88 table_path: String,
90 },
91
92 UnexpectedField {
94 field_path: String,
96 },
97}
98
99impl fmt::Display for ValidationError {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 match self {
102 ValidationError::MissingRequiredField { field_path } => {
103 write!(f, "Missing required field: {}", field_path)
104 }
105 ValidationError::InvalidType {
106 field_path,
107 expected,
108 found,
109 } => {
110 write!(
111 f,
112 "Invalid type for field '{}': expected {}, found {}",
113 field_path, expected, found
114 )
115 }
116 ValidationError::InvalidEnumValue {
117 field_path,
118 value,
119 allowed,
120 } => {
121 write!(
122 f,
123 "Invalid value '{}' for field '{}': must be one of [{}]",
124 value,
125 field_path,
126 allowed.join(", ")
127 )
128 }
129 ValidationError::OutOfRange {
130 field_path,
131 value,
132 min,
133 max,
134 } => {
135 let range = match (min, max) {
136 (Some(min), Some(max)) => format!("between {} and {}", min, max),
137 (Some(min), None) => format!("at least {}", min),
138 (None, Some(max)) => format!("at most {}", max),
139 (None, None) => "within valid range".to_string(),
140 };
141 write!(
142 f,
143 "Value '{}' for field '{}' is out of range: must be {}",
144 value, field_path, range
145 )
146 }
147 ValidationError::InvalidArrayLength {
148 field_path,
149 length,
150 min,
151 max,
152 } => {
153 let constraint = match (min, max) {
154 (Some(min), Some(max)) => format!("between {} and {} items", min, max),
155 (Some(min), None) => format!("at least {} items", min),
156 (None, Some(max)) => format!("at most {} items", max),
157 (None, None) => "valid length".to_string(),
158 };
159 write!(
160 f,
161 "Array '{}' has {} items, but must have {}",
162 field_path, length, constraint
163 )
164 }
165 ValidationError::PatternMismatch {
166 field_path,
167 pattern,
168 } => {
169 write!(
170 f,
171 "Field '{}' does not match required pattern: {}",
172 field_path, pattern
173 )
174 }
175 ValidationError::ConditionalRequirementFailed {
176 field_path,
177 condition,
178 } => {
179 write!(
180 f,
181 "Field '{}' is required when condition '{}' is met",
182 field_path, condition
183 )
184 }
185 ValidationError::SchemaParseError(msg) => {
186 write!(f, "Failed to parse schema: {}", msg)
187 }
188 ValidationError::TomlParseError(msg) => {
189 write!(f, "Failed to parse TOML: {}", msg)
190 }
191 ValidationError::Multiple(errors) => {
192 writeln!(f, "Multiple validation errors occurred:")?;
193 for (i, error) in errors.iter().enumerate() {
194 writeln!(f, " {}. {}", i + 1, error)?;
195 }
196 Ok(())
197 }
198 ValidationError::UnexpectedTable { table_path } => {
199 write!(f, "Unexpected table: {}", table_path)
200 }
201 ValidationError::UnexpectedField { field_path } => {
202 write!(f, "Unexpected field: {}", field_path)
203 }
204 }
205 }
206}
207
208impl std::error::Error for ValidationError {}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn test_missing_required_field_display() {
216 let error = ValidationError::MissingRequiredField {
217 field_path: "proxy.id".to_string(),
218 };
219 let message = error.to_string();
220 assert!(message.contains("proxy.id"));
221 assert!(message.contains("Missing required field"));
222 }
223
224 #[test]
225 fn test_invalid_type_display() {
226 let error = ValidationError::InvalidType {
227 field_path: "network.default.bind_address".to_string(),
228 expected: "string".to_string(),
229 found: "integer".to_string(),
230 };
231 let message = error.to_string();
232 assert!(message.contains("network.default.bind_address"));
233 assert!(message.contains("string"));
234 assert!(message.contains("integer"));
235 }
236
237 #[test]
238 fn test_invalid_enum_value_display() {
239 let error = ValidationError::InvalidEnumValue {
240 field_path: "proxy.log_level".to_string(),
241 value: "invalid".to_string(),
242 allowed: vec!["trace".to_string(), "debug".to_string(), "info".to_string()],
243 };
244 let message = error.to_string();
245 assert!(message.contains("proxy.log_level"));
246 assert!(message.contains("invalid"));
247 assert!(message.contains("trace"));
248 assert!(message.contains("debug"));
249 assert!(message.contains("info"));
250 }
251
252 #[test]
253 fn test_out_of_range_display() {
254 let error = ValidationError::OutOfRange {
255 field_path: "proxy.jwks_cache_duration_hours".to_string(),
256 value: "200".to_string(),
257 min: Some("1".to_string()),
258 max: Some("168".to_string()),
259 };
260 let message = error.to_string();
261 assert!(message.contains("proxy.jwks_cache_duration_hours"));
262 assert!(message.contains("200"));
263 assert!(message.contains("1"));
264 assert!(message.contains("168"));
265 }
266
267 #[test]
268 fn test_invalid_array_length_display() {
269 let error = ValidationError::InvalidArrayLength {
270 field_path: "pipelines.example.endpoints".to_string(),
271 length: 0,
272 min: Some(1),
273 max: None,
274 };
275 let message = error.to_string();
276 assert!(message.contains("pipelines.example.endpoints"));
277 assert!(message.contains("0 items"));
278 assert!(message.contains("at least 1"));
279 }
280
281 #[test]
282 fn test_pattern_mismatch_display() {
283 let error = ValidationError::PatternMismatch {
284 field_path: "network.invalid-name".to_string(),
285 pattern: "^[a-z0-9_-]+$".to_string(),
286 };
287 let message = error.to_string();
288 assert!(message.contains("network.invalid-name"));
289 assert!(message.contains("^[a-z0-9_-]+$"));
290 }
291
292 #[test]
293 fn test_conditional_requirement_failed_display() {
294 let error = ValidationError::ConditionalRequirementFailed {
295 field_path: "management.network".to_string(),
296 condition: "management.enabled == true".to_string(),
297 };
298 let message = error.to_string();
299 assert!(message.contains("management.network"));
300 assert!(message.contains("management.enabled == true"));
301 }
302
303 #[test]
304 fn test_multiple_errors_display() {
305 let errors = vec![
306 ValidationError::MissingRequiredField {
307 field_path: "proxy.id".to_string(),
308 },
309 ValidationError::InvalidType {
310 field_path: "proxy.port".to_string(),
311 expected: "integer".to_string(),
312 found: "string".to_string(),
313 },
314 ];
315 let error = ValidationError::Multiple(errors);
316 let message = error.to_string();
317 assert!(message.contains("Multiple validation errors"));
318 assert!(message.contains("proxy.id"));
319 assert!(message.contains("proxy.port"));
320 }
321}