Skip to main content

serde_xml/
error.rs

1//! Error types for XML serialization and deserialization.
2
3use std::fmt::{self, Display};
4use std::io;
5
6/// Result type alias for serde_xml operations.
7pub type Result<T> = std::result::Result<T, Error>;
8
9/// Error type for XML serialization and deserialization.
10#[derive(Debug)]
11pub struct Error {
12    kind: Box<ErrorKind>,
13    position: Option<Position>,
14}
15
16/// Position information for error reporting.
17#[derive(Debug, Clone, Copy)]
18pub struct Position {
19    /// Line number (1-indexed).
20    pub line: usize,
21    /// Column number (1-indexed).
22    pub column: usize,
23    /// Byte offset from start.
24    pub offset: usize,
25}
26
27/// The kind of error that occurred.
28///
29/// Variants marked *Reserved* are intentionally part of the stable public API
30/// for downstream constructors; this crate's own parsing never emits them.
31#[derive(Debug)]
32pub enum ErrorKind {
33    /// An I/O error occurred.
34    Io(io::Error),
35    /// Unexpected end of input.
36    UnexpectedEof,
37    /// Invalid XML syntax.
38    Syntax(String),
39    /// Invalid XML name.
40    InvalidName(String),
41    /// Missing required attribute. Reserved.
42    MissingAttribute(String),
43    /// Unexpected element. Reserved.
44    UnexpectedElement(String),
45    /// Unexpected attribute. Reserved.
46    UnexpectedAttribute(String),
47    /// Invalid value for type.
48    InvalidValue(String),
49    /// Unclosed tag.
50    UnclosedTag(String),
51    /// Mismatched closing tag.
52    MismatchedTag {
53        /// The expected tag name.
54        expected: String,
55        /// The actual tag name found.
56        found: String,
57    },
58    /// Invalid escape sequence.
59    InvalidEscape(String),
60    /// Invalid UTF-8.
61    InvalidUtf8,
62    /// Custom error message.
63    Custom(String),
64    /// Unsupported operation.
65    Unsupported(String),
66}
67
68impl Error {
69    /// Creates a new error with the given kind.
70    #[inline]
71    pub fn new(kind: ErrorKind) -> Self {
72        Self { kind: Box::new(kind), position: None }
73    }
74
75    /// Creates a new error with position information.
76    #[inline]
77    pub fn with_position(mut self, position: Position) -> Self {
78        self.position = Some(position);
79        self
80    }
81
82    /// Returns the error kind.
83    #[inline]
84    pub fn kind(&self) -> &ErrorKind {
85        &self.kind
86    }
87
88    /// Returns the position where the error occurred.
89    #[inline]
90    pub fn position(&self) -> Option<Position> {
91        self.position
92    }
93
94    /// Creates an unexpected EOF error.
95    #[inline]
96    pub fn unexpected_eof() -> Self {
97        Self::new(ErrorKind::UnexpectedEof)
98    }
99
100    /// Creates a syntax error.
101    #[inline]
102    pub fn syntax<S: Into<String>>(msg: S) -> Self {
103        Self::new(ErrorKind::Syntax(msg.into()))
104    }
105
106    /// Creates an invalid name error.
107    #[inline]
108    pub fn invalid_name<S: Into<String>>(name: S) -> Self {
109        Self::new(ErrorKind::InvalidName(name.into()))
110    }
111
112    /// Creates an invalid value error.
113    #[inline]
114    pub fn invalid_value<S: Into<String>>(msg: S) -> Self {
115        Self::new(ErrorKind::InvalidValue(msg.into()))
116    }
117
118    /// Creates an unclosed tag error.
119    #[inline]
120    pub fn unclosed_tag<S: Into<String>>(tag: S) -> Self {
121        Self::new(ErrorKind::UnclosedTag(tag.into()))
122    }
123
124    /// Creates a mismatched tag error.
125    #[inline]
126    pub fn mismatched_tag<S: Into<String>>(expected: S, found: S) -> Self {
127        Self::new(ErrorKind::MismatchedTag {
128            expected: expected.into(),
129            found: found.into(),
130        })
131    }
132
133    /// Creates an invalid escape error.
134    #[inline]
135    pub fn invalid_escape<S: Into<String>>(seq: S) -> Self {
136        Self::new(ErrorKind::InvalidEscape(seq.into()))
137    }
138
139    /// Creates a custom error.
140    #[inline]
141    pub fn custom<S: Into<String>>(msg: S) -> Self {
142        Self::new(ErrorKind::Custom(msg.into()))
143    }
144
145    /// Creates an unsupported operation error.
146    #[inline]
147    pub fn unsupported<S: Into<String>>(msg: S) -> Self {
148        Self::new(ErrorKind::Unsupported(msg.into()))
149    }
150}
151
152impl Display for Error {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        match &*self.kind {
155            ErrorKind::Io(e) => write!(f, "I/O error: {}", e),
156            ErrorKind::UnexpectedEof => write!(f, "unexpected end of input"),
157            ErrorKind::Syntax(msg) => write!(f, "syntax error: {}", msg),
158            ErrorKind::InvalidName(name) => write!(f, "invalid XML name: {}", name),
159            ErrorKind::MissingAttribute(name) => write!(f, "missing required attribute: {}", name),
160            ErrorKind::UnexpectedElement(name) => write!(f, "unexpected element: {}", name),
161            ErrorKind::UnexpectedAttribute(name) => write!(f, "unexpected attribute: {}", name),
162            ErrorKind::InvalidValue(msg) => write!(f, "invalid value: {}", msg),
163            ErrorKind::UnclosedTag(tag) => write!(f, "unclosed tag: <{}>", tag),
164            ErrorKind::MismatchedTag { expected, found } => {
165                write!(f, "mismatched closing tag: expected </{}>, found </{}>", expected, found)
166            }
167            ErrorKind::InvalidEscape(seq) => write!(f, "invalid escape sequence: {}", seq),
168            ErrorKind::InvalidUtf8 => write!(f, "invalid UTF-8"),
169            ErrorKind::Custom(msg) => write!(f, "{}", msg),
170            ErrorKind::Unsupported(msg) => write!(f, "unsupported: {}", msg),
171        }?;
172
173        if let Some(pos) = self.position {
174            write!(f, " at line {}, column {} (offset {})", pos.line, pos.column, pos.offset)?;
175        }
176
177        Ok(())
178    }
179}
180
181impl std::error::Error for Error {
182    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
183        match &*self.kind {
184            ErrorKind::Io(e) => Some(e),
185            _ => None,
186        }
187    }
188}
189
190impl From<io::Error> for Error {
191    fn from(e: io::Error) -> Self {
192        Self::new(ErrorKind::Io(e))
193    }
194}
195
196impl serde::de::Error for Error {
197    fn custom<T: Display>(msg: T) -> Self {
198        Self::custom(msg.to_string())
199    }
200}
201
202impl serde::ser::Error for Error {
203    fn custom<T: Display>(msg: T) -> Self {
204        Self::custom(msg.to_string())
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_error_display() {
214        let err = Error::syntax("expected '>'");
215        assert_eq!(err.to_string(), "syntax error: expected '>'");
216    }
217
218    #[test]
219    fn test_error_with_position() {
220        let err = Error::syntax("expected '>'")
221            .with_position(Position { line: 5, column: 10, offset: 42 });
222        assert_eq!(
223            err.to_string(),
224            "syntax error: expected '>' at line 5, column 10 (offset 42)"
225        );
226    }
227
228    #[test]
229    fn test_mismatched_tag_error() {
230        let err = Error::mismatched_tag("foo", "bar");
231        assert_eq!(
232            err.to_string(),
233            "mismatched closing tag: expected </foo>, found </bar>"
234        );
235    }
236
237    #[test]
238    fn test_io_error() {
239        let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
240        let err = Error::from(io_err);
241        assert!(err.to_string().contains("I/O error"));
242    }
243
244    #[test]
245    fn test_custom_error() {
246        let err = Error::custom("something went wrong");
247        assert_eq!(err.to_string(), "something went wrong");
248    }
249}