Skip to main content

fastxml/
error.rs

1//! Error types for fastxml.
2
3use std::fmt;
4use std::io;
5
6use crate::namespace::error::NamespaceError;
7use crate::node::error::NodeError;
8use crate::parser::error::ParseError;
9use crate::schema::error::SchemaError;
10use crate::schema::fetcher::error::FetchError;
11use crate::schema::xsd::error::XsdParseError;
12use crate::xpath::error::{XPathEvalError, XPathSyntaxError};
13
14/// Location information for errors, providing line, column, byte offset, and optional XPath.
15///
16/// This struct provides a lightweight way to attach location information to any error.
17/// It can be used across various modules (transform, parser, validator) to provide
18/// consistent error location reporting.
19///
20/// # Examples
21///
22/// ```
23/// use fastxml::error::ErrorLocation;
24///
25/// // Create from byte offset with line/column calculation
26/// let input = "line1\nline2\nline3";
27/// let loc = ErrorLocation::from_offset_with_input(6, input);
28/// assert_eq!(loc.line, Some(2));
29/// assert_eq!(loc.column, Some(1));
30///
31/// // Multi-byte UTF-8 characters are counted as single columns
32/// let input = "あいう\nえお";
33/// // "あいう" is 9 bytes (3 bytes each), "\n" is 1 byte, "え" starts at byte 10
34/// let loc = ErrorLocation::from_offset_with_input(10, input);
35/// assert_eq!(loc.line, Some(2));
36/// assert_eq!(loc.column, Some(1)); // First char of line 2
37///
38/// // Add XPath information
39/// let loc = loc.with_xpath("/root/item[1]".to_string());
40/// assert!(loc.to_string().contains("/root/item[1]"));
41/// ```
42#[derive(Debug, Clone, Default)]
43pub struct ErrorLocation {
44    /// Line number (1-indexed)
45    pub line: Option<usize>,
46    /// Column number (1-indexed)
47    pub column: Option<usize>,
48    /// Byte offset from the beginning of the input
49    pub byte_offset: Option<usize>,
50    /// XPath-like path to the error location
51    pub xpath: Option<String>,
52}
53
54impl ErrorLocation {
55    /// Creates an empty error location.
56    pub fn new() -> Self {
57        Self::default()
58    }
59
60    /// Creates an error location with only byte offset.
61    pub fn from_offset(byte_offset: usize) -> Self {
62        Self {
63            byte_offset: Some(byte_offset),
64            ..Default::default()
65        }
66    }
67
68    /// Creates an error location with line and column (calculated from byte offset).
69    pub fn from_offset_with_input(byte_offset: usize, input: &str) -> Self {
70        let (line, column) = Self::calculate_line_column(input, byte_offset);
71        Self {
72            line: Some(line),
73            column: Some(column),
74            byte_offset: Some(byte_offset),
75            xpath: None,
76        }
77    }
78
79    /// Creates an error location with line and column directly.
80    pub fn from_line_column(line: usize, column: usize) -> Self {
81        Self {
82            line: Some(line),
83            column: Some(column),
84            byte_offset: None,
85            xpath: None,
86        }
87    }
88
89    /// Sets the XPath-like path.
90    pub fn with_xpath(mut self, xpath: String) -> Self {
91        self.xpath = Some(xpath);
92        self
93    }
94
95    /// Sets the byte offset.
96    pub fn with_offset(mut self, offset: usize) -> Self {
97        self.byte_offset = Some(offset);
98        self
99    }
100
101    /// Returns true if this location has any position information.
102    pub fn has_position(&self) -> bool {
103        self.line.is_some() || self.byte_offset.is_some()
104    }
105
106    /// Calculates line and column from byte offset in the input string.
107    ///
108    /// Returns (line, column) where both are 1-indexed.
109    /// Column is counted in Unicode characters (not bytes), so multi-byte
110    /// characters like Japanese are counted as single columns.
111    pub fn calculate_line_column(input: &str, byte_offset: usize) -> (usize, usize) {
112        let mut line = 1;
113        let mut column = 1;
114
115        for (pos, ch) in input.char_indices() {
116            if pos >= byte_offset {
117                break;
118            }
119            if ch == '\n' {
120                line += 1;
121                column = 1;
122            } else {
123                column += 1;
124            }
125        }
126
127        (line, column)
128    }
129}
130
131impl fmt::Display for ErrorLocation {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        let mut parts = Vec::new();
134
135        if let (Some(line), Some(col)) = (self.line, self.column) {
136            parts.push(format!("line {}:{}", line, col));
137        } else if let Some(offset) = self.byte_offset {
138            parts.push(format!("position {}", offset));
139        }
140
141        if let Some(xpath) = &self.xpath {
142            parts.push(format!("at {}", xpath));
143        }
144
145        write!(f, "{}", parts.join(", "))
146    }
147}
148
149/// Main error type for fastxml operations.
150#[derive(Debug, thiserror::Error)]
151pub enum Error {
152    /// XML parsing error
153    #[error("parse error: {0}")]
154    Parse(#[from] ParseError),
155
156    /// IO error
157    #[error("io error: {0}")]
158    Io(#[from] io::Error),
159
160    /// XPath syntax error
161    #[error("xpath syntax error: {0}")]
162    XPathSyntax(#[from] XPathSyntaxError),
163
164    /// XPath evaluation error
165    #[error("xpath evaluation error: {0}")]
166    XPathEval(#[from] XPathEvalError),
167
168    /// Schema error
169    #[error("schema error: {0}")]
170    Schema(#[from] SchemaError),
171
172    /// Validation error
173    #[error("validation error: {message}")]
174    Validation {
175        /// Error message
176        message: String,
177        /// Line number where the error occurred
178        line: Option<usize>,
179        /// Column number where the error occurred
180        column: Option<usize>,
181    },
182
183    /// Namespace error
184    #[error("namespace error: {0}")]
185    Namespace(#[from] NamespaceError),
186
187    /// Node-related error
188    #[error("node error: {0}")]
189    Node(#[from] NodeError),
190
191    /// Invalid operation
192    #[error("invalid operation: {0}")]
193    InvalidOperation(String),
194
195    /// Network/fetch error
196    #[error("fetch error: {0}")]
197    Fetch(#[from] FetchError),
198
199    /// UTF-8 encoding error
200    #[error("utf8 error: {0}")]
201    Utf8(#[from] std::str::Utf8Error),
202
203    /// String UTF-8 error
204    #[error("string utf8 error: {0}")]
205    FromUtf8(#[from] std::string::FromUtf8Error),
206
207    /// XSD parsing error
208    #[error("xsd parse error: {0}")]
209    XsdParse(#[from] XsdParseError),
210}
211
212impl From<quick_xml::Error> for Error {
213    fn from(err: quick_xml::Error) -> Self {
214        ParseError::Generic {
215            message: err.to_string(),
216        }
217        .into()
218    }
219}
220
221impl From<quick_xml::events::attributes::AttrError> for Error {
222    fn from(err: quick_xml::events::attributes::AttrError) -> Self {
223        ParseError::AttributeError {
224            message: err.to_string(),
225        }
226        .into()
227    }
228}
229
230/// Result type alias for fastxml operations.
231pub type Result<T> = std::result::Result<T, Error>;
232
233/// Error severity level.
234#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
235pub enum ErrorLevel {
236    /// Warning - validation can continue
237    Warning,
238    /// Error - validation issue but can continue
239    #[default]
240    Error,
241    /// Fatal - validation cannot continue
242    Fatal,
243}
244
245impl std::fmt::Display for ErrorLevel {
246    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247        match self {
248            ErrorLevel::Warning => write!(f, "warning"),
249            ErrorLevel::Error => write!(f, "error"),
250            ErrorLevel::Fatal => write!(f, "fatal"),
251        }
252    }
253}
254
255/// Structured error for schema validation, compatible with libxml's StructuredError.
256#[derive(Debug, Clone)]
257pub struct StructuredError {
258    /// Error message
259    pub message: String,
260    /// Location information (line, column, byte_offset, xpath)
261    pub location: ErrorLocation,
262    /// Error type classification
263    pub error_type: ValidationErrorType,
264    /// Error severity level
265    pub level: ErrorLevel,
266    /// Name of the element or attribute that caused the error
267    pub node_name: Option<String>,
268    /// Expected value or type (for type mismatch errors)
269    pub expected: Option<String>,
270    /// Actual value found (for type mismatch errors)
271    pub found: Option<String>,
272}
273
274impl Default for StructuredError {
275    fn default() -> Self {
276        Self {
277            message: String::new(),
278            location: ErrorLocation::default(),
279            error_type: ValidationErrorType::Other,
280            level: ErrorLevel::Error,
281            node_name: None,
282            expected: None,
283            found: None,
284        }
285    }
286}
287
288impl StructuredError {
289    /// Creates a new error with the given message and type.
290    pub fn new(message: impl Into<String>, error_type: ValidationErrorType) -> Self {
291        Self {
292            message: message.into(),
293            error_type,
294            ..Default::default()
295        }
296    }
297
298    /// Sets the line number.
299    pub fn with_line(mut self, line: usize) -> Self {
300        self.location.line = Some(line);
301        self
302    }
303
304    /// Sets the column number.
305    pub fn with_column(mut self, column: usize) -> Self {
306        self.location.column = Some(column);
307        self
308    }
309
310    /// Sets the byte offset.
311    pub fn with_byte_offset(mut self, offset: usize) -> Self {
312        self.location.byte_offset = Some(offset);
313        self
314    }
315
316    /// Sets the error level.
317    pub fn with_level(mut self, level: ErrorLevel) -> Self {
318        self.level = level;
319        self
320    }
321
322    /// Sets the element path (stored in location.xpath).
323    pub fn with_element_path(mut self, path: impl Into<String>) -> Self {
324        self.location.xpath = Some(path.into());
325        self
326    }
327
328    /// Sets the node name.
329    pub fn with_node_name(mut self, name: impl Into<String>) -> Self {
330        self.node_name = Some(name.into());
331        self
332    }
333
334    /// Sets the expected value.
335    pub fn with_expected(mut self, expected: impl Into<String>) -> Self {
336        self.expected = Some(expected.into());
337        self
338    }
339
340    /// Sets the found value.
341    pub fn with_found(mut self, found: impl Into<String>) -> Self {
342        self.found = Some(found.into());
343        self
344    }
345
346    /// Returns true if this is a warning.
347    pub fn is_warning(&self) -> bool {
348        self.level == ErrorLevel::Warning
349    }
350
351    /// Returns true if this is an error or fatal.
352    pub fn is_error(&self) -> bool {
353        self.level >= ErrorLevel::Error
354    }
355
356    /// Sets location information from an ErrorLocation (merges non-None fields).
357    pub fn with_location(mut self, location: &ErrorLocation) -> Self {
358        if let Some(line) = location.line {
359            self.location.line = Some(line);
360        }
361        if let Some(column) = location.column {
362            self.location.column = Some(column);
363        }
364        if let Some(offset) = location.byte_offset {
365            self.location.byte_offset = Some(offset);
366        }
367        if let Some(ref xpath) = location.xpath {
368            self.location.xpath = Some(xpath.clone());
369        }
370        self
371    }
372
373    /// Sets the entire location, replacing any existing location.
374    pub fn set_location(mut self, location: ErrorLocation) -> Self {
375        self.location = location;
376        self
377    }
378
379    /// Returns the line number (convenience accessor).
380    pub fn line(&self) -> Option<usize> {
381        self.location.line
382    }
383
384    /// Returns the column number (convenience accessor).
385    pub fn column(&self) -> Option<usize> {
386        self.location.column
387    }
388
389    /// Returns the byte offset (convenience accessor).
390    pub fn byte_offset(&self) -> Option<usize> {
391        self.location.byte_offset
392    }
393
394    /// Returns the element path (convenience accessor).
395    pub fn element_path(&self) -> Option<&str> {
396        self.location.xpath.as_deref()
397    }
398
399    /// Calculates and sets line/column from byte_offset using the given input.
400    ///
401    /// This is useful when you have a byte offset but need to display line/column.
402    pub fn calculate_line_column(mut self, input: &str) -> Self {
403        if let Some(offset) = self.location.byte_offset {
404            let (line, column) = ErrorLocation::calculate_line_column(input, offset);
405            self.location.line = Some(line);
406            self.location.column = Some(column);
407        }
408        self
409    }
410}
411
412impl From<&StructuredError> for ErrorLocation {
413    fn from(err: &StructuredError) -> Self {
414        err.location.clone()
415    }
416}
417
418impl std::fmt::Display for StructuredError {
419    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
420        // Format: [level] location: message
421        write!(f, "[{}] ", self.level)?;
422
423        if let Some(ref path) = self.location.xpath {
424            write!(f, "{}", path)?;
425            if let Some(line) = self.location.line {
426                write!(f, " (line {})", line)?;
427            }
428            write!(f, ": ")?;
429        } else if let (Some(line), Some(col)) = (self.location.line, self.location.column) {
430            write!(f, "{}:{}: ", line, col)?;
431        } else if let Some(line) = self.location.line {
432            write!(f, "line {}: ", line)?;
433        } else if let Some(offset) = self.location.byte_offset {
434            write!(f, "offset {}: ", offset)?;
435        }
436
437        write!(f, "{}", self.message)?;
438
439        if let (Some(expected), Some(found)) = (&self.expected, &self.found) {
440            write!(f, " (expected: {}, found: {})", expected, found)?;
441        }
442
443        Ok(())
444    }
445}
446
447/// Classification of validation errors.
448#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub enum ValidationErrorType {
450    /// Unknown or unrecognized element
451    UnknownElement,
452    /// Unknown or unrecognized attribute
453    UnknownAttribute,
454    /// Missing required element
455    MissingRequiredElement,
456    /// Missing required attribute
457    MissingRequiredAttribute,
458    /// Invalid attribute value
459    InvalidAttributeValue,
460    /// Invalid element content
461    InvalidContent,
462    /// Invalid text content (type mismatch)
463    InvalidTextContent,
464    /// Element appears too many times
465    TooManyOccurrences,
466    /// Element appears too few times
467    TooFewOccurrences,
468    /// Element out of order (sequence violation)
469    ElementOutOfOrder,
470    /// Unexpected element in choice/sequence
471    UnexpectedElement,
472    /// Namespace mismatch
473    NamespaceMismatch,
474    /// Schema not found
475    SchemaNotFound,
476    /// Identity constraint violation (unique, key, keyref)
477    IdentityConstraint,
478    /// Type definition not found
479    TypeNotFound,
480    /// Facet constraint violation
481    FacetViolation,
482    /// Content model violation
483    ContentModelViolation,
484    /// Unclosed element at end of document
485    UnclosedElement,
486    /// Generic validation error
487    Other,
488}