Skip to main content

xsd_schema/
error.rs

1//! Error types for XSD parsing and validation
2//!
3//! All errors include source locations when available for developer-friendly messages.
4
5use crate::parser::location::SourceLocation;
6use thiserror::Error;
7
8/// Result type for schema operations
9pub type SchemaResult<T> = Result<T, SchemaError>;
10
11/// Result type for facet operations
12pub type FacetResult<T> = Result<T, FacetError>;
13
14/// Facet-related errors (validation and derivation)
15#[derive(Error, Debug, Clone)]
16pub enum FacetError {
17    /// Value violates length constraint
18    #[error("length constraint violation: {message}")]
19    LengthViolation { message: String },
20
21    /// Value violates minLength constraint
22    #[error("minLength constraint violation: value length {actual} is less than minimum {min}")]
23    MinLengthViolation { actual: u64, min: u64 },
24
25    /// Value violates maxLength constraint
26    #[error("maxLength constraint violation: value length {actual} exceeds maximum {max}")]
27    MaxLengthViolation { actual: u64, max: u64 },
28
29    /// Value doesn't match pattern
30    #[error("pattern constraint violation: value '{value}' does not match pattern '{pattern}'")]
31    PatternViolation { value: String, pattern: String },
32
33    /// Value not in enumeration
34    #[error("enumeration constraint violation: value '{value}' is not in the allowed set")]
35    EnumerationViolation { value: String },
36
37    /// Value violates minInclusive constraint
38    #[error("minInclusive constraint violation: value '{value}' is less than minimum '{min}'")]
39    MinInclusiveViolation { value: String, min: String },
40
41    /// Value violates maxInclusive constraint
42    #[error("maxInclusive constraint violation: value '{value}' is greater than maximum '{max}'")]
43    MaxInclusiveViolation { value: String, max: String },
44
45    /// Value violates minExclusive constraint
46    #[error("minExclusive constraint violation: value '{value}' is not greater than '{min}'")]
47    MinExclusiveViolation { value: String, min: String },
48
49    /// Value violates maxExclusive constraint
50    #[error("maxExclusive constraint violation: value '{value}' is not less than '{max}'")]
51    MaxExclusiveViolation { value: String, max: String },
52
53    /// Value violates totalDigits constraint
54    #[error("totalDigits constraint violation: value has {actual} digits, maximum is {max}")]
55    TotalDigitsViolation { actual: u32, max: u32 },
56
57    /// Value violates fractionDigits constraint
58    #[error(
59        "fractionDigits constraint violation: value has {actual} fraction digits, maximum is {max}"
60    )]
61    FractionDigitsViolation { actual: u32, max: u32 },
62
63    /// Value violates explicitTimezone constraint
64    #[error("explicitTimezone constraint violation: {message}")]
65    ExplicitTimezoneViolation { message: String },
66
67    /// Invalid pattern regex
68    #[error("invalid pattern regex '{pattern}': {message}")]
69    InvalidPattern { pattern: String, message: String },
70
71    /// Facet derivation error - derived type is not more restrictive
72    #[error("derivation restriction violation: {message}")]
73    DerivationRestriction { message: String },
74
75    /// Facet derivation error - fixed facet cannot be overridden
76    #[error("fixed facet violation: cannot override fixed {facet_name} value '{base_value}' with '{derived_value}'")]
77    FixedFacetViolation {
78        facet_name: String,
79        base_value: String,
80        derived_value: String,
81    },
82
83    /// Facet derivation error - conflicting facets
84    #[error("conflicting facets: {message}")]
85    ConflictingFacets { message: String },
86
87    /// Facet not applicable to this type
88    #[error("facet '{facet}' is not applicable to type '{type_name}'")]
89    NotApplicable { facet: String, type_name: String },
90}
91
92impl FacetError {
93    /// Create a length violation error
94    pub fn length(message: impl Into<String>) -> Self {
95        FacetError::LengthViolation {
96            message: message.into(),
97        }
98    }
99
100    /// Create a pattern violation error
101    pub fn pattern(value: impl Into<String>, pattern: impl Into<String>) -> Self {
102        FacetError::PatternViolation {
103            value: value.into(),
104            pattern: pattern.into(),
105        }
106    }
107
108    /// Create an enumeration violation error
109    pub fn enumeration(value: impl Into<String>) -> Self {
110        FacetError::EnumerationViolation {
111            value: value.into(),
112        }
113    }
114
115    /// Create a derivation restriction error
116    pub fn derivation(message: impl Into<String>) -> Self {
117        FacetError::DerivationRestriction {
118            message: message.into(),
119        }
120    }
121
122    /// Create a fixed facet violation error
123    pub fn fixed_violation(
124        facet_name: impl Into<String>,
125        base_value: impl Into<String>,
126        derived_value: impl Into<String>,
127    ) -> Self {
128        FacetError::FixedFacetViolation {
129            facet_name: facet_name.into(),
130            base_value: base_value.into(),
131            derived_value: derived_value.into(),
132        }
133    }
134
135    /// Create a conflicting facets error
136    pub fn conflicting(message: impl Into<String>) -> Self {
137        FacetError::ConflictingFacets {
138            message: message.into(),
139        }
140    }
141}
142
143/// XSD schema error with source location
144#[derive(Error, Debug)]
145pub enum SchemaError {
146    /// XML parsing error from quick-xml
147    #[error("XML parse error{}: {message}", location_str(.location))]
148    XmlError {
149        message: String,
150        location: Option<SourceLocation>,
151    },
152
153    /// Structural error (invalid child element, wrong attributes, etc.)
154    #[error("Schema structural error{}: {message} (constraint: {constraint})", location_str(.location))]
155    StructuralError {
156        constraint: &'static str,
157        message: String,
158        location: Option<SourceLocation>,
159    },
160
161    /// Namespace error (undefined prefix, invalid QName, etc.)
162    #[error("Namespace error{}: {message}", location_str(.location))]
163    NamespaceError {
164        message: String,
165        location: Option<SourceLocation>,
166    },
167
168    /// Feature gate error (XSD 1.1 feature used in 1.0 mode)
169    #[error("Feature not supported{}: {message}", location_str(.location))]
170    FeatureError {
171        message: String,
172        location: Option<SourceLocation>,
173    },
174
175    /// Schema resolution error (include/import failed)
176    #[error("Schema resolution error: {message}")]
177    ResolutionError { message: String },
178
179    /// I/O error (file not found, permission denied, etc.)
180    #[error("I/O error: {0}")]
181    IoError(#[from] std::io::Error),
182
183    /// Internal error (should not happen, indicates a bug)
184    #[error("Internal error: {0}")]
185    Internal(String),
186}
187
188impl SchemaError {
189    /// Create a structural error with constraint ID
190    pub fn structural(
191        constraint: &'static str,
192        message: impl Into<String>,
193        location: Option<SourceLocation>,
194    ) -> Self {
195        SchemaError::StructuralError {
196            constraint,
197            message: message.into(),
198            location,
199        }
200    }
201
202    /// Create a namespace error
203    pub fn namespace(message: impl Into<String>, location: Option<SourceLocation>) -> Self {
204        SchemaError::NamespaceError {
205            message: message.into(),
206            location,
207        }
208    }
209
210    /// Create a feature gate error
211    pub fn feature(message: impl Into<String>, location: Option<SourceLocation>) -> Self {
212        SchemaError::FeatureError {
213            message: message.into(),
214            location,
215        }
216    }
217
218    /// Create a resolution error
219    pub fn resolution(message: impl Into<String>) -> Self {
220        SchemaError::ResolutionError {
221            message: message.into(),
222        }
223    }
224
225    /// Create an XML parse error
226    pub fn xml(message: impl Into<String>, location: Option<SourceLocation>) -> Self {
227        SchemaError::XmlError {
228            message: message.into(),
229            location,
230        }
231    }
232
233    /// Create an internal error
234    pub fn internal(message: impl Into<String>) -> Self {
235        SchemaError::Internal(message.into())
236    }
237
238    /// Add source location to error if it doesn't already have one
239    pub fn with_location(self, location: SourceLocation) -> Self {
240        match self {
241            SchemaError::XmlError {
242                message,
243                location: None,
244            } => SchemaError::XmlError {
245                message,
246                location: Some(location),
247            },
248            SchemaError::StructuralError {
249                constraint,
250                message,
251                location: None,
252            } => SchemaError::StructuralError {
253                constraint,
254                message,
255                location: Some(location),
256            },
257            SchemaError::NamespaceError {
258                message,
259                location: None,
260            } => SchemaError::NamespaceError {
261                message,
262                location: Some(location),
263            },
264            SchemaError::FeatureError {
265                message,
266                location: None,
267            } => SchemaError::FeatureError {
268                message,
269                location: Some(location),
270            },
271            // Already has location or doesn't support location - return unchanged
272            other => other,
273        }
274    }
275
276    /// Returns `true` for errors that indicate the schema *content* is invalid
277    /// (structural, namespace, XML parse, feature-gate errors).
278    ///
279    /// Returns `false` for resolution/IO errors, which mean the schema could
280    /// not be *located* — these are non-fatal during import processing because
281    /// `xs:import` schema locations are hints, not requirements.
282    pub fn is_schema_content_error(&self) -> bool {
283        matches!(
284            self,
285            SchemaError::StructuralError { .. }
286                | SchemaError::NamespaceError { .. }
287                | SchemaError::XmlError { .. }
288                | SchemaError::FeatureError { .. }
289        )
290    }
291}
292
293/// Conversion from quick-xml errors
294impl From<quick_xml::Error> for SchemaError {
295    fn from(err: quick_xml::Error) -> Self {
296        SchemaError::XmlError {
297            message: err.to_string(),
298            location: None,
299        }
300    }
301}
302
303/// Helper function for formatting optional location
304fn location_str(loc: &Option<SourceLocation>) -> String {
305    match loc {
306        Some(l) => format!(" at {}", l),
307        None => String::new(),
308    }
309}