Skip to main content

xsd_schema/validation/
errors.rs

1//! Validation error types for instance validation
2//!
3//! This module provides spec-aligned error codes for XML instance validation
4//! against XSD schemas. Error codes follow the XSD specification anchors:
5//!
6//! - `cvc-*` - Instance validation constraints (e.g., `cvc-elt`, `cvc-type`)
7//! - `cos-*` - Component constraints (e.g., `cos-valid-default`)
8//! - `src-*` - Schema representation constraints (e.g., `src-element`)
9
10use crate::error::FacetError;
11use crate::parser::location::SourceLocation;
12use crate::types::validators::ValidationError as TypeValidationError;
13
14/// Instance validation error with spec-aligned constraint code
15#[derive(Debug, Clone)]
16pub struct ValidationError {
17    /// Spec constraint code (cvc-*, cos-*, src-*)
18    pub constraint: &'static str,
19    /// Human-readable error message
20    pub message: String,
21    /// Source location in the instance document
22    pub location: Option<SourceLocation>,
23    /// XPath-like path to the element (e.g., "/root/child[1]")
24    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
54/// Result type for instance validation operations
55pub type ValidationResult<T> = Result<T, ValidationError>;
56
57/// Create a validation error with the given constraint code and message
58pub 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
71/// Create a validation error with element path information
72pub 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
86/// Convert a type validation error to an instance validation error
87///
88/// Use this when a `types::validators::ValidationError` needs to be reported
89/// with a specific cvc-* constraint code.
90pub 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
103/// Convert a facet error to an instance validation error
104///
105/// Use this when a `FacetError` needs to be reported with a specific cvc-* code.
106/// Consider using `facet_constraint_code()` to get the appropriate code.
107pub 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
120/// Map a FacetError variant to its specific cvc-* constraint code
121///
122/// This function returns the most specific constraint code for each facet type,
123/// preferring codes like `cvc-pattern-valid` over generic `cvc-facet-valid`.
124///
125/// # Mappings
126///
127/// | FacetError Variant | Constraint Code |
128/// |--------------------|-----------------|
129/// | LengthViolation | cvc-length-valid |
130/// | MinLengthViolation | cvc-minLength-valid |
131/// | MaxLengthViolation | cvc-maxLength-valid |
132/// | PatternViolation | cvc-pattern-valid |
133/// | EnumerationViolation | cvc-enumeration-valid |
134/// | MinInclusiveViolation | cvc-minInclusive-valid |
135/// | MaxInclusiveViolation | cvc-maxInclusive-valid |
136/// | MinExclusiveViolation | cvc-minExclusive-valid |
137/// | MaxExclusiveViolation | cvc-maxExclusive-valid |
138/// | TotalDigitsViolation | cvc-totalDigits-valid |
139/// | FractionDigitsViolation | cvc-fractionDigits-valid |
140/// | ExplicitTimezoneViolation | cvc-explicitTimezone-valid |
141/// | InvalidPattern | cvc-pattern-valid |
142/// | DerivationRestriction | cos-st-restricts |
143/// | FixedFacetViolation | cos-st-restricts |
144/// | ConflictingFacets | cos-st-restricts |
145/// | NotApplicable | cos-applicable-facets |
146pub 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
168/// Map a TypeValidationError variant to its default cvc-* constraint code
169///
170/// Returns `cvc-datatype-valid` for lexical/type/range errors, and delegates
171/// to `facet_constraint_code()` for facet violations.
172///
173/// # Constraint code context
174///
175/// - `cvc-datatype-valid` (datatypes.html) is the default for lexical/type/range
176///   errors. Callers validating at the structures.html level (e.g., element or
177///   attribute value checks) should override to `cvc-simple-type` instead.
178/// - `FacetViolation` delegates to `facet_constraint_code()`, which returns
179///   `cvc-*-valid` for runtime facet checks. Schema-level `FacetError` variants
180///   (`DerivationRestriction`, `FixedFacetViolation`, `ConflictingFacets`,
181///   `NotApplicable`) map to `cos-*` codes and should not appear during instance
182///   validation — they are schema compilation errors.
183pub 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        // Test all facet error variants map to correct codes
232        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}