1use crate::error::FacetError;
11use crate::parser::location::SourceLocation;
12use crate::types::validators::ValidationError as TypeValidationError;
13
14#[derive(Debug, Clone)]
16pub struct ValidationError {
17 pub constraint: &'static str,
19 pub message: String,
21 pub location: Option<SourceLocation>,
23 pub element_path: Option<String>,
25}
26
27impl std::fmt::Display for ValidationError {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 write!(f, "[{}] {}", self.constraint, self.message)?;
30 if let Some(loc) = &self.location {
31 write!(f, " at {}", loc)?;
32 }
33 if let Some(path) = &self.element_path {
34 write!(f, " ({})", path)?;
35 }
36 Ok(())
37 }
38}
39
40impl std::error::Error for ValidationError {}
41
42impl ValidationError {
43 pub fn with_location(mut self, location: SourceLocation) -> Self {
44 self.location = Some(location);
45 self
46 }
47
48 pub fn with_path(mut self, path: impl Into<String>) -> Self {
49 self.element_path = Some(path.into());
50 self
51 }
52}
53
54pub type ValidationResult<T> = Result<T, ValidationError>;
56
57pub fn error(
59 constraint: &'static str,
60 message: impl Into<String>,
61 location: Option<SourceLocation>,
62) -> ValidationError {
63 ValidationError {
64 constraint,
65 message: message.into(),
66 location,
67 element_path: None,
68 }
69}
70
71pub fn error_with_path(
73 constraint: &'static str,
74 message: impl Into<String>,
75 location: Option<SourceLocation>,
76 element_path: impl Into<String>,
77) -> ValidationError {
78 ValidationError {
79 constraint,
80 message: message.into(),
81 location,
82 element_path: Some(element_path.into()),
83 }
84}
85
86pub fn from_value_error(
91 constraint: &'static str,
92 err: TypeValidationError,
93 location: Option<SourceLocation>,
94) -> ValidationError {
95 ValidationError {
96 constraint,
97 message: err.to_string(),
98 location,
99 element_path: None,
100 }
101}
102
103pub fn from_facet_error(
108 constraint: &'static str,
109 err: FacetError,
110 location: Option<SourceLocation>,
111) -> ValidationError {
112 ValidationError {
113 constraint,
114 message: err.to_string(),
115 location,
116 element_path: None,
117 }
118}
119
120pub fn facet_constraint_code(err: &FacetError) -> &'static str {
147 match err {
148 FacetError::LengthViolation { .. } => "cvc-length-valid",
149 FacetError::MinLengthViolation { .. } => "cvc-minLength-valid",
150 FacetError::MaxLengthViolation { .. } => "cvc-maxLength-valid",
151 FacetError::PatternViolation { .. } => "cvc-pattern-valid",
152 FacetError::EnumerationViolation { .. } => "cvc-enumeration-valid",
153 FacetError::MinInclusiveViolation { .. } => "cvc-minInclusive-valid",
154 FacetError::MaxInclusiveViolation { .. } => "cvc-maxInclusive-valid",
155 FacetError::MinExclusiveViolation { .. } => "cvc-minExclusive-valid",
156 FacetError::MaxExclusiveViolation { .. } => "cvc-maxExclusive-valid",
157 FacetError::TotalDigitsViolation { .. } => "cvc-totalDigits-valid",
158 FacetError::FractionDigitsViolation { .. } => "cvc-fractionDigits-valid",
159 FacetError::ExplicitTimezoneViolation { .. } => "cvc-explicitTimezone-valid",
160 FacetError::InvalidPattern { .. } => "cvc-pattern-valid",
161 FacetError::DerivationRestriction { .. } => "cos-st-restricts",
162 FacetError::FixedFacetViolation { .. } => "cos-st-restricts",
163 FacetError::ConflictingFacets { .. } => "cos-st-restricts",
164 FacetError::NotApplicable { .. } => "cos-applicable-facets",
165 }
166}
167
168pub fn value_error_constraint_code(err: &TypeValidationError) -> &'static str {
184 match err {
185 TypeValidationError::InvalidLexical { .. } => "cvc-datatype-valid",
186 TypeValidationError::FacetViolation(facet_err) => facet_constraint_code(facet_err),
187 TypeValidationError::TypeError { .. } => "cvc-datatype-valid",
188 TypeValidationError::RangeError { .. } => "cvc-datatype-valid",
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn test_error_constructor() {
198 let err = error("cvc-elt", "Element is invalid", None);
199 assert_eq!(err.constraint, "cvc-elt");
200 assert_eq!(err.message, "Element is invalid");
201 assert!(err.location.is_none());
202 assert!(err.element_path.is_none());
203 }
204
205 #[test]
206 fn test_error_with_path() {
207 let err = error_with_path(
208 "cvc-complex-type",
209 "Missing required element",
210 None,
211 "/root/child",
212 );
213 assert_eq!(err.constraint, "cvc-complex-type");
214 assert_eq!(err.element_path.as_deref(), Some("/root/child"));
215 }
216
217 #[test]
218 fn test_error_display() {
219 let err = error("cvc-elt", "Invalid element", None);
220 assert_eq!(format!("{}", err), "[cvc-elt] Invalid element");
221
222 let err_with_path = error_with_path("cvc-type", "Type mismatch", None, "/root");
223 assert_eq!(
224 format!("{}", err_with_path),
225 "[cvc-type] Type mismatch (/root)"
226 );
227 }
228
229 #[test]
230 fn test_facet_constraint_code_mapping() {
231 assert_eq!(
233 facet_constraint_code(&FacetError::LengthViolation {
234 message: "test".to_string()
235 }),
236 "cvc-length-valid"
237 );
238 assert_eq!(
239 facet_constraint_code(&FacetError::MinLengthViolation { actual: 1, min: 5 }),
240 "cvc-minLength-valid"
241 );
242 assert_eq!(
243 facet_constraint_code(&FacetError::MaxLengthViolation { actual: 10, max: 5 }),
244 "cvc-maxLength-valid"
245 );
246 assert_eq!(
247 facet_constraint_code(&FacetError::PatternViolation {
248 value: "abc".to_string(),
249 pattern: "[0-9]+".to_string()
250 }),
251 "cvc-pattern-valid"
252 );
253 assert_eq!(
254 facet_constraint_code(&FacetError::EnumerationViolation {
255 value: "x".to_string()
256 }),
257 "cvc-enumeration-valid"
258 );
259 assert_eq!(
260 facet_constraint_code(&FacetError::MinInclusiveViolation {
261 value: "1".to_string(),
262 min: "5".to_string()
263 }),
264 "cvc-minInclusive-valid"
265 );
266 assert_eq!(
267 facet_constraint_code(&FacetError::MaxInclusiveViolation {
268 value: "10".to_string(),
269 max: "5".to_string()
270 }),
271 "cvc-maxInclusive-valid"
272 );
273 assert_eq!(
274 facet_constraint_code(&FacetError::MinExclusiveViolation {
275 value: "5".to_string(),
276 min: "5".to_string()
277 }),
278 "cvc-minExclusive-valid"
279 );
280 assert_eq!(
281 facet_constraint_code(&FacetError::MaxExclusiveViolation {
282 value: "5".to_string(),
283 max: "5".to_string()
284 }),
285 "cvc-maxExclusive-valid"
286 );
287 assert_eq!(
288 facet_constraint_code(&FacetError::TotalDigitsViolation { actual: 10, max: 5 }),
289 "cvc-totalDigits-valid"
290 );
291 assert_eq!(
292 facet_constraint_code(&FacetError::FractionDigitsViolation { actual: 5, max: 2 }),
293 "cvc-fractionDigits-valid"
294 );
295 assert_eq!(
296 facet_constraint_code(&FacetError::ExplicitTimezoneViolation {
297 message: "test".to_string()
298 }),
299 "cvc-explicitTimezone-valid"
300 );
301 assert_eq!(
302 facet_constraint_code(&FacetError::InvalidPattern {
303 pattern: "[".to_string(),
304 message: "invalid".to_string()
305 }),
306 "cvc-pattern-valid"
307 );
308 assert_eq!(
309 facet_constraint_code(&FacetError::DerivationRestriction {
310 message: "test".to_string()
311 }),
312 "cos-st-restricts"
313 );
314 assert_eq!(
315 facet_constraint_code(&FacetError::FixedFacetViolation {
316 facet_name: "length".to_string(),
317 base_value: "5".to_string(),
318 derived_value: "10".to_string()
319 }),
320 "cos-st-restricts"
321 );
322 assert_eq!(
323 facet_constraint_code(&FacetError::ConflictingFacets {
324 message: "test".to_string()
325 }),
326 "cos-st-restricts"
327 );
328 assert_eq!(
329 facet_constraint_code(&FacetError::NotApplicable {
330 facet: "length".to_string(),
331 type_name: "integer".to_string()
332 }),
333 "cos-applicable-facets"
334 );
335 }
336
337 #[test]
338 fn test_from_facet_error() {
339 let facet_err = FacetError::MinLengthViolation { actual: 2, min: 5 };
340 let code = facet_constraint_code(&facet_err);
341 let val_err = from_facet_error(code, facet_err, None);
342 assert_eq!(val_err.constraint, "cvc-minLength-valid");
343 assert!(val_err.message.contains("minLength"));
344 }
345
346 #[test]
347 fn test_value_error_constraint_code_invalid_lexical() {
348 let err = TypeValidationError::InvalidLexical {
349 value: "abc".to_string(),
350 type_name: "integer",
351 message: "not a valid integer".to_string(),
352 };
353 assert_eq!(value_error_constraint_code(&err), "cvc-datatype-valid");
354 }
355
356 #[test]
357 fn test_value_error_constraint_code_facet_violation() {
358 let err = TypeValidationError::FacetViolation(FacetError::PatternViolation {
359 value: "abc".to_string(),
360 pattern: "[0-9]+".to_string(),
361 });
362 assert_eq!(value_error_constraint_code(&err), "cvc-pattern-valid");
363 }
364
365 #[test]
366 fn test_value_error_constraint_code_type_error() {
367 use crate::types::XmlTypeCode;
368 let err = TypeValidationError::TypeError {
369 expected: XmlTypeCode::String,
370 actual: XmlTypeCode::Integer,
371 };
372 assert_eq!(value_error_constraint_code(&err), "cvc-datatype-valid");
373 }
374
375 #[test]
376 fn test_value_error_constraint_code_range_error() {
377 let err = TypeValidationError::RangeError {
378 value: "999999".to_string(),
379 type_name: "short",
380 };
381 assert_eq!(value_error_constraint_code(&err), "cvc-datatype-valid");
382 }
383
384 #[test]
385 fn test_with_location() {
386 let loc = SourceLocation {
387 base_uri: "test.xsd".to_string(),
388 line: 10,
389 column: 5,
390 };
391 let err = error("cvc-elt", "test", None).with_location(loc.clone());
392 assert_eq!(err.location, Some(loc));
393 }
394
395 #[test]
396 fn test_with_path() {
397 let err = error("cvc-elt", "test", None).with_path("/root/child");
398 assert_eq!(err.element_path.as_deref(), Some("/root/child"));
399 }
400
401 #[test]
402 fn test_builder_chaining() {
403 let loc = SourceLocation {
404 base_uri: "test.xsd".to_string(),
405 line: 3,
406 column: 1,
407 };
408 let err = error("cvc-type", "Type mismatch", None)
409 .with_location(loc)
410 .with_path("/root/elem[2]");
411 assert_eq!(err.constraint, "cvc-type");
412 assert_eq!(err.location.as_ref().unwrap().line, 3);
413 assert_eq!(err.element_path.as_deref(), Some("/root/elem[2]"));
414 let display = format!("{}", err);
415 assert!(display.contains("[cvc-type]"));
416 assert!(display.contains("Type mismatch"));
417 assert!(display.contains("/root/elem[2]"));
418 }
419
420 #[test]
421 fn test_from_value_error_with_auto_code() {
422 let type_err = TypeValidationError::InvalidLexical {
423 value: "not-a-number".to_string(),
424 type_name: "decimal",
425 message: "invalid decimal".to_string(),
426 };
427 let code = value_error_constraint_code(&type_err);
428 let val_err = from_value_error(code, type_err, None);
429 assert_eq!(val_err.constraint, "cvc-datatype-valid");
430 assert!(val_err.message.contains("invalid decimal"));
431 }
432}