Skip to main content

xdoc/core/
error.rs

1use std::fmt;
2
3/// Convenient result type for core XML operations.
4pub type XmlResult<T> = Result<T, XmlError>;
5
6/// High-level category for an XML engine error.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum ErrorKind {
9    InvalidName,
10    InvalidNamespace,
11    DuplicateNamespacePrefix,
12    UnknownNamespacePrefix,
13    InvalidOperation,
14    NotFound,
15    Parse,
16    Build,
17    Query,
18    Validation,
19    Signature,
20    Io,
21}
22
23/// Location in a source XML document, when known.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct Span {
26    line: Option<usize>,
27    column: Option<usize>,
28}
29
30impl Span {
31    /// Creates a span with a known one-based line and column.
32    pub fn new(line: usize, column: usize) -> Self {
33        Self {
34            line: Some(line),
35            column: Some(column),
36        }
37    }
38
39    /// Creates a span for cases where the exact location is unavailable.
40    pub fn unknown() -> Self {
41        Self {
42            line: None,
43            column: None,
44        }
45    }
46
47    pub fn line(&self) -> Option<usize> {
48        self.line
49    }
50
51    pub fn column(&self) -> Option<usize> {
52        self.column
53    }
54}
55
56/// Structured error used by the XML engine.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct XmlError {
59    kind: ErrorKind,
60    message: String,
61    span: Option<Span>,
62}
63
64impl XmlError {
65    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
66        Self {
67            kind,
68            message: message.into(),
69            span: None,
70        }
71    }
72
73    pub fn with_span(mut self, span: Span) -> Self {
74        self.span = Some(span);
75        self
76    }
77
78    pub fn invalid_name(name: impl AsRef<str>, reason: impl AsRef<str>) -> Self {
79        Self::new(
80            ErrorKind::InvalidName,
81            format!("invalid XML name `{}`: {}", name.as_ref(), reason.as_ref()),
82        )
83    }
84
85    pub fn kind(&self) -> &ErrorKind {
86        &self.kind
87    }
88
89    pub fn message(&self) -> &str {
90        &self.message
91    }
92
93    pub fn span(&self) -> Option<Span> {
94        self.span
95    }
96}
97
98impl fmt::Display for XmlError {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        match self.span {
101            Some(span) => match (span.line(), span.column()) {
102                (Some(line), Some(column)) => {
103                    write!(
104                        f,
105                        "{:?}: {} at line {}, column {}",
106                        self.kind, self.message, line, column
107                    )
108                }
109                _ => write!(f, "{:?}: {} at unknown location", self.kind, self.message),
110            },
111            None => write!(f, "{:?}: {}", self.kind, self.message),
112        }
113    }
114}
115
116impl std::error::Error for XmlError {}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn error_includes_kind_and_message() {
124        let error = XmlError::new(ErrorKind::InvalidName, "bad name");
125
126        assert_eq!(error.kind(), &ErrorKind::InvalidName);
127        assert_eq!(error.message(), "bad name");
128        assert_eq!(error.span(), None);
129    }
130
131    #[test]
132    fn error_can_include_span() {
133        let error = XmlError::new(ErrorKind::Parse, "unexpected token").with_span(Span::new(3, 9));
134
135        assert_eq!(error.span(), Some(Span::new(3, 9)));
136        assert!(error.to_string().contains("line 3, column 9"));
137    }
138}