Skip to main content

sup_xml_core/
error.rs

1#![forbid(unsafe_code)]  // see CONTRIBUTING.md § "Unsafe policy"
2
3use std::fmt;
4
5/// Which subsystem raised the error.
6///
7/// Mirrors `xmlErrorDomain` from libxml2 (`include/libxml/xmlerror.h`) so that
8/// errors can be categorised without inspecting the message string, AND so
9/// that `domain as i32` produces the exact numeric value a C caller sees
10/// through `xmlError::domain` after going through the [`crates/compat`] FFI
11/// shim.  See `thoughts/c_abi_implementation_plan.md` § "Unified Rust error
12/// type, free conversion to libxml2 layout."
13///
14/// Variant discriminants are pinned — **do not renumber** — they are part
15/// of the C ABI surface.
16#[repr(i32)]
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ErrorDomain {
19    /// No domain — used by libxml2 as the default value for fields it hasn't
20    /// initialised.  We rarely emit this from Rust; included for round-trip.
21    None       =  0,    // XML_FROM_NONE
22    /// XML parser (well-formedness violations, unexpected tokens, etc.).
23    Parser     =  1,    // XML_FROM_PARSER
24    /// Document tree manipulation.
25    Tree       =  2,    // XML_FROM_TREE
26    /// Namespace resolution (undeclared prefix, duplicate declarations, etc.).
27    Namespace  =  3,    // XML_FROM_NAMESPACE
28    /// DTD processing and entity expansion.
29    Dtd        =  4,    // XML_FROM_DTD
30    /// HTML parsing errors (malformed tag soup, recovered errors from the
31    /// HTML5 tree-construction algorithm).
32    Html       =  5,    // XML_FROM_HTML
33    /// I/O errors (file not found, read failure, etc.).
34    Io         =  8,    // XML_FROM_IO
35    /// XPath expression parsing or evaluation.
36    XPath      = 12,    // XML_FROM_XPATH
37    /// XSLT processing — stylesheet compile, transform errors.
38    Xslt       = 22,    // XML_FROM_XSLT
39    /// W3C XML Schema (XSD) validation.  lxml's `error.domain_name` →
40    /// `"SCHEMASV"`.
41    SchemasValidate = 17, // XML_FROM_SCHEMASV
42    /// RELAX NG validation.  lxml's `error.domain_name` → `"RELAXNGV"`.
43    RelaxNGValidate = 19, // XML_FROM_RELAXNGV
44    /// Schematron validation.  lxml's `error.domain_name` → `"SCHEMATRONV"`.
45    SchematronValidate = 28, // XML_FROM_SCHEMATRONV
46    /// Schema or DTD validation.
47    Validation = 23,    // XML_FROM_VALID
48    /// Character encoding errors (invalid UTF-8, unsupported encoding).
49    /// libxml2 calls this domain "I18N" but it's the encoding/charset
50    /// error bucket.
51    Encoding   = 27,    // XML_FROM_I18N
52}
53
54/// Severity of the error.
55///
56/// `Warning < Error < Fatal`.  Fatal errors abort processing; warnings and
57/// errors may still produce a partial document.
58///
59/// Discriminants match libxml2's `xmlErrorLevel`.
60#[repr(i32)]
61#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
62pub enum ErrorLevel {
63    /// `XML_ERR_NONE` (0) is a no-error sentinel libxml2 sometimes emits;
64    /// callers should treat it as "not actually an error."  We don't
65    /// construct it from Rust.
66    None    = 0,
67    Warning = 1,    // XML_ERR_WARNING
68    Error   = 2,    // XML_ERR_ERROR
69    Fatal   = 3,    // XML_ERR_FATAL
70}
71
72/// One of libxml2's well-known numeric error codes.
73///
74/// libxml2's `xmlParserErrors` enum has ~800 variants; this type covers
75/// the ~40 we actually emit from the parser, validator, encoder, etc.
76/// Everything else lands at [`ErrorCode::InternalError`] (`= 1`), which
77/// is what libxml2 itself uses for unmapped cases.
78///
79/// Discriminants are pinned to match libxml2's `include/libxml/xmlerror.h`
80/// exactly — **do not renumber.**  A C caller doing
81/// `if (err->code == XML_ERR_INVALID_CHAR) { ... }` sees `9` here too,
82/// because `ErrorCode::InvalidChar as i32 == 9`.
83///
84/// # When to add a new variant
85///
86/// When you have a new error-construction site that maps to a specific
87/// libxml2 code that callers genuinely check for.  When in doubt, default
88/// to [`ErrorCode::InternalError`].  Adding a variant is additive (callers
89/// reading numeric codes are unaffected) but renumbering an existing
90/// variant is a breaking ABI change.
91#[repr(i32)]
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93#[non_exhaustive]
94pub enum ErrorCode {
95    // ── general (xmlParserErrors) ────────────────────────────────────────
96    /// No error — round-trip sentinel.  We don't emit this from Rust.
97    Ok                    =    0,   // XML_ERR_OK
98    /// Default for any error we can't classify more specifically.
99    /// libxml2 itself uses this for unhandled cases, so callers
100    /// branching on specific codes will already have a "default" arm
101    /// that handles us.
102    InternalError         =    1,   // XML_ERR_INTERNAL_ERROR
103    /// Out-of-memory at parse / build time.  Not currently emitted (we
104    /// panic on OOM today), reserved for when we add fallible alloc.
105    NoMemory              =    2,   // XML_ERR_NO_MEMORY
106    /// Document is empty (no root element, no prolog).
107    DocumentEmpty         =    4,   // XML_ERR_DOCUMENT_EMPTY
108
109    // ── well-formedness violations (the ones consumers check) ───────────
110    /// `&#x...;` malformed — empty hex, non-hex digit, etc.
111    InvalidHexCharRef     =    6,   // XML_ERR_INVALID_HEX_CHARREF
112    /// `&#...;` malformed — empty decimal, non-digit, etc.
113    InvalidDecCharRef     =    7,   // XML_ERR_INVALID_DEC_CHARREF
114    /// Character outside XML 1.0 § 2.2 char range.
115    InvalidChar           =    9,   // XML_ERR_INVALID_CHAR
116    /// Entity reference to an undeclared entity name.
117    UndeclaredEntity      =   26,   // XML_ERR_UNDECLARED_ENTITY
118    /// Unknown encoding label on `<?xml encoding="..."?>` or BOM mismatch.
119    UnknownEncoding       =   31,   // XML_ERR_UNKNOWN_ENCODING
120    /// Encoding declared but not supported by the build.
121    UnsupportedEncoding   =   32,   // XML_ERR_UNSUPPORTED_ENCODING
122
123    // ── attribute / element structure ───────────────────────────────────
124    /// Two attributes with the same expanded name on one element.
125    AttributeRedefined    =   42,   // XML_ERR_ATTRIBUTE_REDEFINED
126    /// Comment didn't terminate before EOF.
127    CommentNotFinished    =   45,   // XML_ERR_COMMENT_NOT_FINISHED
128    /// Bad `<?xml ... ?>` declaration syntax.
129    XmlDeclNotStarted     =   56,   // XML_ERR_XMLDECL_NOT_STARTED
130    XmlDeclNotFinished    =   57,   // XML_ERR_XMLDECL_NOT_FINISHED
131    /// `]]>` in text content, where it's reserved for CDATA close.
132    MisplacedCdataEnd     =   62,   // XML_ERR_MISPLACED_CDATA_END
133    /// CDATA section didn't terminate before EOF.
134    CdataNotFinished      =   63,   // XML_ERR_CDATA_NOT_FINISHED
135    /// Expected an XML name (start tag, attribute name, entity name, etc.).
136    NameRequired          =   68,   // XML_ERR_NAME_REQUIRED
137    /// Expected `=` after attribute name.
138    EqualRequired         =   75,   // XML_ERR_EQUAL_REQUIRED
139    /// `</X>` end-tag name doesn't match the open `<Y>`.
140    TagNameMismatch       =   76,   // XML_ERR_TAG_NAME_MISMATCH
141    /// Start tag never closed before EOF.
142    TagNotFinished        =   77,   // XML_ERR_TAG_NOT_FINISHED
143    /// Document is not well-balanced (open tags don't match close tags
144    /// at EOF).  General catch-all when we can't say which tag.
145    NotWellBalanced       =   85,   // XML_ERR_NOT_WELL_BALANCED
146    /// Content after the root element's close.
147    ExtraContent          =   86,   // XML_ERR_EXTRA_CONTENT
148
149    // ── namespace ────────────────────────────────────────────────────────
150    /// Prefix used but never declared via `xmlns:prefix=...`.
151    NsErrUndefinedNamespace = 201, // XML_NS_ERR_UNDEFINED_NAMESPACE
152    /// Malformed QName (e.g. multiple colons).
153    NsErrQname              = 202, // XML_NS_ERR_QNAME
154
155    // ── I/O (xmlErrorDomain::Io) ────────────────────────────────────────
156    /// A document input (the main file or an external entity) could not
157    /// be opened or read.  libxml2's `__xmlLoaderErr` reports exactly
158    /// this code with domain [`ErrorDomain::Io`]; consumers that key on
159    /// the domain (e.g. lxml's `_raiseParseError`) then raise an I/O
160    /// error rather than a syntax error.
161    IoLoadError             = 1549, // XML_IO_LOAD_ERROR
162
163    // ── XSD schema validation (xmlSchemaValidError) ─────────────────────
164    // Codes consumers (e.g. lxml's `error_log.filter_types`) match on to
165    // classify a validation failure.
166    /// Attribute/element value invalid against its datatype.
167    SchemavCvcDatatypeValid121 = 1824, // XML_SCHEMAV_CVC_DATATYPE_VALID_1_2_1
168    /// A value rejected by a facet (pattern, length, range, …).
169    SchemavCvcFacetValid       = 1829, // XML_SCHEMAV_CVC_FACET_VALID
170    /// An attribute not permitted by the element's complex type.
171    SchemavCvcComplexType322   = 1867, // XML_SCHEMAV_CVC_COMPLEX_TYPE_3_2_2
172    /// A required attribute is missing.
173    SchemavCvcComplexType4     = 1868, // XML_SCHEMAV_CVC_COMPLEX_TYPE_4
174    /// Element content doesn't match the declared content model
175    /// (unexpected child element, or a required one absent).
176    SchemavElementContent      = 1871, // XML_SCHEMAV_ELEMENT_CONTENT
177
178    // ── DTD validation (xmlParserErrors) ────────────────────────────────
179    /// An element declared `EMPTY` has child content.
180    DtdNotEmpty                = 528,  // XML_DTD_NOT_EMPTY
181
182    // ── RELAX NG validation (xmlRelaxNGValidErr) ────────────────────────
183    /// An element appeared where the pattern did not expect it.
184    RelaxngErrElemwrong        = 38,   // XML_RELAXNG_ERR_ELEMWRONG
185
186    // ── encoding (xmlErrorDomain::I18n) ─────────────────────────────────
187    /// Encoding handler couldn't decode the input bytes.
188    EncodingConvFailed    = 6003,   // XML_I18N_CONV_FAILED
189}
190
191/// A structured XML processing error.
192///
193/// SupXML returns `XmlError` through `Result<_, XmlError>` instead of
194/// relying on a global error variable like libxml2 does.  The `domain`,
195/// `level`, and `code` fields let callers react without parsing the
196/// human-readable `message`.
197///
198/// `code` is an [`ErrorCode`] enum whose discriminants match libxml2's
199/// `xmlParserErrors` numeric values.  Callers can match on the enum
200/// (idiomatic Rust); the [`crates/compat`] cdylib converts to libxml2's
201/// `xmlError::code: i32` via `err.code as i32` — zero cost.  See
202/// `thoughts/c_abi_implementation_plan.md` for the design.
203#[derive(Debug, Clone)]
204pub struct XmlError {
205    /// Which subsystem produced the error.
206    pub domain: ErrorDomain,
207    /// Severity.
208    pub level: ErrorLevel,
209    /// Specific error category (libxml2-compatible numeric code on
210    /// the wire side).  When in doubt, [`ErrorCode::InternalError`].
211    pub code: ErrorCode,
212    /// Human-readable description of the problem.
213    pub message: String,
214    /// Source file name, if available (e.g. for file-based parsing).
215    pub file: Option<String>,
216    /// 1-based line number where the error occurred, if known.
217    pub line: Option<u32>,
218    /// 1-based column number where the error occurred, if known.
219    pub column: Option<u32>,
220    /// 0-based byte offset into the parser's input buffer where the
221    /// error occurred, if known.
222    ///
223    /// Reported alongside [`line`](Self::line) / [`column`](Self::column)
224    /// because the three answer different questions: line/col is what
225    /// a human reads, byte offset is what tools (editors, LSP servers,
226    /// `dd if=… bs=1 skip=…`) act on without re-walking the input.
227    /// Byte offset also survives line-ending normalization (XML 1.0
228    /// § 2.11) and is the only useful coordinate for binary
229    /// pipelines — gzipped XML, network captures, mmap'd files.
230    ///
231    /// `u64` (not `usize`) so that the ABI surface in `crates/compat`
232    /// is stable across 32- and 64-bit targets and survives documents
233    /// larger than 4 GB on the streaming reader.
234    ///
235    /// # Coordinate system
236    ///
237    /// The offset is measured in the parser's **internal UTF-8
238    /// buffer**, which is the same as the caller's input byte slice
239    /// in the common case (input was already UTF-8).  If
240    /// [`ParseOptions::auto_transcode`](crate::options::ParseOptions)
241    /// converted UTF-16 or another encoding to UTF-8 first, the
242    /// offset is relative to the post-transcode buffer and does
243    /// **not** point at the user's original bytes; the user-facing
244    /// offset would require a transcoder back-map we don't have
245    /// today.  Callers operating on already-UTF-8 input — which is
246    /// the overwhelming majority of XML on the wire — can use this
247    /// directly.
248    pub byte_offset: Option<u64>,
249    /// XPath/XQuery/XSLT error code as a local name in the standard
250    /// `err:` namespace (`http://www.w3.org/2005/xqt-errors`) — e.g.
251    /// `"FOAR0001"` for division by zero, `"FORG0001"` for an invalid
252    /// cast.  Distinct from [`code`](Self::code), which is the
253    /// libxml2-numeric category; this is the spec-defined dynamic
254    /// error a stylesheet's `xsl:catch` / `try/catch` matches on and
255    /// exposes through `$err:code`.  `None` when the error has no
256    /// specific spec code (it then projects as the generic
257    /// `err:FOER0000`).
258    pub xpath_code: Option<String>,
259}
260
261impl XmlError {
262    /// Construct an error with the catch-all
263    /// [`ErrorCode::InternalError`] code.  Add a more specific code
264    /// via [`with_code`](Self::with_code) when there's a libxml2
265    /// numeric value that fits the case.
266    pub fn new(domain: ErrorDomain, level: ErrorLevel, message: impl Into<String>) -> Self {
267        Self {
268            domain,
269            level,
270            code: ErrorCode::InternalError,
271            message: message.into(),
272            file: None,
273            line: None,
274            column: None,
275            byte_offset: None,
276            xpath_code: None,
277        }
278    }
279
280    /// Attach a specific [`ErrorCode`] (libxml2-numeric).  Builder-style;
281    /// returns `self` for chaining with [`at`](Self::at).
282    pub fn with_code(mut self, code: ErrorCode) -> Self {
283        self.code = code;
284        self
285    }
286
287    /// Attach the spec-defined XPath/XSLT error code (an `err:` local
288    /// name such as `"FOAR0001"`).  Builder-style; see
289    /// [`xpath_code`](Self::xpath_code).
290    pub fn with_xpath_code(mut self, code: impl Into<String>) -> Self {
291        self.xpath_code = Some(code.into());
292        self
293    }
294
295    /// Attach `code` only if no spec code is already present.  Used at
296    /// outer choke points (e.g. `document()` retrieval) that want to
297    /// label an otherwise-uncoded error without overwriting a more
298    /// specific code an inner layer already set.
299    pub fn or_xpath_code(mut self, code: impl Into<String>) -> Self {
300        if self.xpath_code.is_none() {
301            self.xpath_code = Some(code.into());
302        }
303        self
304    }
305
306    /// Attach source position.  All three coordinates are taken
307    /// together because the scanner derives them from a single byte
308    /// offset and any error that knows one knows all three;
309    /// `byte_offset` is documented on [`Self::byte_offset`].
310    pub fn at(
311        mut self,
312        file:        impl Into<String>,
313        line:        u32,
314        column:      u32,
315        byte_offset: u64,
316    ) -> Self {
317        self.file        = Some(file.into());
318        self.line        = Some(line);
319        self.column      = Some(column);
320        self.byte_offset = Some(byte_offset);
321        self
322    }
323}
324
325impl fmt::Display for XmlError {
326    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327        // `file:line:col:` is the conventional editor-clickable
328        // prefix.  Byte offset goes in a trailing `@N` because it
329        // breaks that convention if injected in the middle.
330        match (&self.file, self.line, self.column) {
331            (Some(file), Some(line), Some(col)) => write!(f, "{file}:{line}:{col}: ")?,
332            (Some(file), Some(line), None)      => write!(f, "{file}:{line}: ")?,
333            _ => {}
334        }
335        write!(f, "[{:?}/{:?}] {}", self.domain, self.level, self.message)?;
336        if let Some(ofs) = self.byte_offset {
337            write!(f, " @ byte {ofs}")?;
338        }
339        Ok(())
340    }
341}
342
343impl std::error::Error for XmlError {}
344
345/// Convenience alias used throughout SupXML — equivalent to
346/// `std::result::Result<T, XmlError>`.
347pub type Result<T> = std::result::Result<T, XmlError>;
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    #[test]
354    fn error_display() {
355        let e = XmlError::new(ErrorDomain::Parser, ErrorLevel::Fatal, "unexpected EOF")
356            .at("doc.xml", 42, 7, 1024);
357        let s = e.to_string();
358        assert!(s.contains("doc.xml"));
359        assert!(s.contains("42"));
360        assert!(s.contains("7"));
361        assert!(s.contains("unexpected EOF"));
362        assert!(s.contains("byte 1024"));
363    }
364
365    #[test]
366    fn error_level_ordering() {
367        assert!(ErrorLevel::Warning < ErrorLevel::Error);
368        assert!(ErrorLevel::Error < ErrorLevel::Fatal);
369    }
370
371    /// Verifies that `ErrorCode` discriminants match libxml2's
372    /// `xmlParserErrors` numeric values exactly — the contract that
373    /// makes `err.code as i32` a zero-cost FFI conversion.  Drift here
374    /// breaks libsupxml2's caller-facing codes silently.
375    ///
376    /// Spot-check the codes most consumers care about; the full set
377    /// is documented inline in the enum.
378    /// Pinned discriminants — one row per variant so a renumbering bug
379    /// (the kind that caused [`NsErrUndefinedNamespace`] to ship at 502
380    /// instead of 201, silently breaking C consumers comparing
381    /// `err->code == XML_NS_ERR_UNDEFINED_NAMESPACE`) lights up here
382    /// instead of leaking through the FFI surface.  Numbers checked
383    /// against `xmlParserErrors` in
384    /// `/opt/homebrew/Cellar/libxml2/<version>/include/libxml2/libxml/xmlerror.h`.
385    #[test]
386    fn error_code_libxml2_values_match() {
387        assert_eq!(ErrorCode::Ok                      as i32,    0);
388        assert_eq!(ErrorCode::InternalError           as i32,    1);
389        assert_eq!(ErrorCode::NoMemory                as i32,    2);
390        assert_eq!(ErrorCode::DocumentEmpty           as i32,    4);
391        assert_eq!(ErrorCode::InvalidHexCharRef       as i32,    6);
392        assert_eq!(ErrorCode::InvalidDecCharRef       as i32,    7);
393        assert_eq!(ErrorCode::InvalidChar             as i32,    9);
394        assert_eq!(ErrorCode::UndeclaredEntity        as i32,   26);
395        assert_eq!(ErrorCode::UnknownEncoding         as i32,   31);
396        assert_eq!(ErrorCode::UnsupportedEncoding     as i32,   32);
397        assert_eq!(ErrorCode::AttributeRedefined      as i32,   42);
398        assert_eq!(ErrorCode::CommentNotFinished      as i32,   45);
399        assert_eq!(ErrorCode::XmlDeclNotStarted       as i32,   56);
400        assert_eq!(ErrorCode::XmlDeclNotFinished      as i32,   57);
401        assert_eq!(ErrorCode::MisplacedCdataEnd       as i32,   62);
402        assert_eq!(ErrorCode::CdataNotFinished        as i32,   63);
403        assert_eq!(ErrorCode::NameRequired            as i32,   68);
404        assert_eq!(ErrorCode::EqualRequired           as i32,   75);
405        assert_eq!(ErrorCode::TagNameMismatch         as i32,   76);
406        assert_eq!(ErrorCode::TagNotFinished          as i32,   77);
407        assert_eq!(ErrorCode::NotWellBalanced         as i32,   85);
408        assert_eq!(ErrorCode::ExtraContent            as i32,   86);
409        assert_eq!(ErrorCode::NsErrUndefinedNamespace as i32,  201);
410        assert_eq!(ErrorCode::NsErrQname              as i32,  202);
411        assert_eq!(ErrorCode::IoLoadError             as i32, 1549);
412        assert_eq!(ErrorCode::EncodingConvFailed      as i32, 6003);
413    }
414
415    /// Pins every [`ErrorDomain`] discriminant against libxml2's
416    /// `xmlErrorDomain` (xmlerror.h).  Same drift-protection role as
417    /// [`error_code_libxml2_values_match`].
418    ///
419    /// We model **11 of 31** libxml2 domains.  The 20 absent values
420    /// stay free at their libxml2 numbers so adding them later
421    /// requires no renumbering:
422    ///
423    /// |  # | libxml2 name           | rationale to add |
424    /// |---:|------------------------|------------------|
425    /// |  6 | XML_FROM_MEMORY        | alloc failures   |
426    /// |  7 | XML_FROM_OUTPUT        | serializer I/O   |
427    /// |  9 | XML_FROM_FTP           | (deprecated)     |
428    /// | 10 | XML_FROM_HTTP          | network fetch    |
429    /// | 11 | XML_FROM_XINCLUDE      | XInclude         |
430    /// | 13 | XML_FROM_XPOINTER      | XPointer         |
431    /// | 14 | XML_FROM_REGEXP        | XSD §F regex     |
432    /// | 15 | XML_FROM_DATATYPE      | XSD types        |
433    /// | 16 | XML_FROM_SCHEMASP      | schema parse     |
434    /// | 17 | XML_FROM_SCHEMASV      | schema validate  |
435    /// | 18 | XML_FROM_RELAXNGP      | RelaxNG parse    |
436    /// | 19 | XML_FROM_RELAXNGV      | RelaxNG validate |
437    /// | 20 | XML_FROM_CATALOG       | XML Catalogs     |
438    /// | 21 | XML_FROM_C14N          | canonicalisation |
439    /// | 24 | XML_FROM_CHECK         | tree integrity   |
440    /// | 25 | XML_FROM_WRITER        | xmlTextWriter    |
441    /// | 26 | XML_FROM_MODULE        | xmlModule        |
442    /// | 28 | XML_FROM_SCHEMATRONV   | Schematron       |
443    /// | 29 | XML_FROM_BUFFER        | xmlBuffer        |
444    /// | 30 | XML_FROM_URI           | URI parse        |
445    ///
446    /// Numbers verified against
447    /// `/opt/homebrew/Cellar/libxml2/<version>/include/libxml2/libxml/xmlerror.h`.
448    #[test]
449    fn error_domain_libxml2_values_match() {
450        assert_eq!(ErrorDomain::None       as i32,  0);
451        assert_eq!(ErrorDomain::Parser     as i32,  1);
452        assert_eq!(ErrorDomain::Tree       as i32,  2);
453        assert_eq!(ErrorDomain::Namespace  as i32,  3);
454        assert_eq!(ErrorDomain::Dtd        as i32,  4);
455        assert_eq!(ErrorDomain::Html       as i32,  5);
456        assert_eq!(ErrorDomain::Io         as i32,  8);
457        assert_eq!(ErrorDomain::XPath      as i32, 12);
458        assert_eq!(ErrorDomain::Xslt       as i32, 22);
459        assert_eq!(ErrorDomain::Validation as i32, 23);
460        assert_eq!(ErrorDomain::Encoding   as i32, 27);
461    }
462
463    #[test]
464    fn xml_error_carries_default_code() {
465        let e = XmlError::new(ErrorDomain::Parser, ErrorLevel::Fatal, "boom");
466        assert_eq!(e.code, ErrorCode::InternalError);
467    }
468
469    #[test]
470    fn xml_error_with_code_chains() {
471        let e = XmlError::new(ErrorDomain::Parser, ErrorLevel::Fatal, "bad char")
472            .with_code(ErrorCode::InvalidChar)
473            .at("doc.xml", 1, 2, 5);
474        assert_eq!(e.code, ErrorCode::InvalidChar);
475        assert_eq!(e.code as i32, 9);
476        assert_eq!(e.line,        Some(1));
477        assert_eq!(e.column,      Some(2));
478        assert_eq!(e.byte_offset, Some(5));
479    }
480}