use std::fmt;
pub type Result<T> = std::result::Result<T, XmlError>;
#[derive(Debug)]
pub enum XmlError {
NoAttribute,
WrongAttributeType,
Io(std::io::Error),
Parse {
kind: ParseErrorKind,
line: u32,
message: Option<String>,
},
EmptyDocument,
MismatchedElement {
line: u32,
expected: String,
found: String,
},
CanNotConvertText,
NoTextNode,
ElementDepthExceeded {
line: u32,
max_depth: u32,
},
InvalidNodeId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParseErrorKind {
Element,
Attribute,
Text,
Cdata,
Comment,
Declaration,
Unknown,
General,
}
impl XmlError {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::NoAttribute => "XML_NO_ATTRIBUTE",
Self::WrongAttributeType => "XML_WRONG_ATTRIBUTE_TYPE",
Self::Io(_) => "XML_ERROR_FILE_READ_ERROR",
Self::Parse { kind, .. } => kind.error_name(),
Self::EmptyDocument => "XML_ERROR_EMPTY_DOCUMENT",
Self::MismatchedElement { .. } => "XML_ERROR_MISMATCHED_ELEMENT",
Self::CanNotConvertText => "XML_CAN_NOT_CONVERT_TEXT",
Self::NoTextNode => "XML_NO_TEXT_NODE",
Self::ElementDepthExceeded { .. } => "XML_ELEMENT_DEPTH_EXCEEDED",
Self::InvalidNodeId => "XML_INVALID_NODE_ID",
}
}
#[must_use]
pub fn line(&self) -> Option<u32> {
match self {
Self::Parse { line, .. }
| Self::MismatchedElement { line, .. }
| Self::ElementDepthExceeded { line, .. } => Some(*line),
_ => None,
}
}
}
impl ParseErrorKind {
#[must_use]
pub const fn error_name(self) -> &'static str {
match self {
Self::Element => "XML_ERROR_PARSING_ELEMENT",
Self::Attribute => "XML_ERROR_PARSING_ATTRIBUTE",
Self::Text => "XML_ERROR_PARSING_TEXT",
Self::Cdata => "XML_ERROR_PARSING_CDATA",
Self::Comment => "XML_ERROR_PARSING_COMMENT",
Self::Declaration => "XML_ERROR_PARSING_DECLARATION",
Self::Unknown => "XML_ERROR_PARSING_UNKNOWN",
Self::General => "XML_ERROR_PARSING",
}
}
}
impl fmt::Display for XmlError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NoAttribute => write!(f, "attribute not found"),
Self::WrongAttributeType => write!(f, "wrong attribute type"),
Self::Io(err) => write!(f, "I/O error: {err}"),
Self::Parse {
kind,
line,
message,
} => {
write!(f, "parse error ({}) at line {line}", kind.error_name())?;
if let Some(msg) = message {
write!(f, ": {msg}")?;
}
Ok(())
}
Self::EmptyDocument => write!(f, "empty document"),
Self::MismatchedElement {
line,
expected,
found,
} => write!(
f,
"mismatched element at line {line}: expected </{expected}>, found </{found}>"
),
Self::CanNotConvertText => write!(f, "cannot convert text to requested type"),
Self::NoTextNode => write!(f, "no text node found"),
Self::ElementDepthExceeded { line, max_depth } => {
write!(
f,
"element depth exceeded maximum of {max_depth} at line {line}"
)
}
Self::InvalidNodeId => write!(f, "invalid node ID (node was deleted or never existed)"),
}
}
}
impl std::error::Error for XmlError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(err) => Some(err),
_ => None,
}
}
}
impl From<std::io::Error> for XmlError {
fn from(err: std::io::Error) -> Self {
Self::Io(err)
}
}
impl Clone for XmlError {
fn clone(&self) -> Self {
match self {
Self::NoAttribute => Self::NoAttribute,
Self::WrongAttributeType => Self::WrongAttributeType,
Self::Io(err) => Self::Io(std::io::Error::new(err.kind(), err.to_string())),
Self::Parse {
kind,
line,
message,
} => Self::Parse {
kind: *kind,
line: *line,
message: message.clone(),
},
Self::EmptyDocument => Self::EmptyDocument,
Self::MismatchedElement {
line,
expected,
found,
} => Self::MismatchedElement {
line: *line,
expected: expected.clone(),
found: found.clone(),
},
Self::CanNotConvertText => Self::CanNotConvertText,
Self::NoTextNode => Self::NoTextNode,
Self::ElementDepthExceeded { line, max_depth } => Self::ElementDepthExceeded {
line: *line,
max_depth: *max_depth,
},
Self::InvalidNodeId => Self::InvalidNodeId,
}
}
}
impl PartialEq for XmlError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::NoAttribute, Self::NoAttribute)
| (Self::WrongAttributeType, Self::WrongAttributeType)
| (Self::EmptyDocument, Self::EmptyDocument)
| (Self::CanNotConvertText, Self::CanNotConvertText)
| (Self::NoTextNode, Self::NoTextNode)
| (Self::InvalidNodeId, Self::InvalidNodeId) => true,
(
Self::Parse {
kind: k1, line: l1, ..
},
Self::Parse {
kind: k2, line: l2, ..
},
) => k1 == k2 && l1 == l2,
(
Self::MismatchedElement {
line: l1,
expected: e1,
found: f1,
},
Self::MismatchedElement {
line: l2,
expected: e2,
found: f2,
},
) => l1 == l2 && e1 == e2 && f1 == f2,
(
Self::ElementDepthExceeded {
line: l1,
max_depth: m1,
},
Self::ElementDepthExceeded {
line: l2,
max_depth: m2,
},
) => l1 == l2 && m1 == m2,
(Self::Io(a), Self::Io(b)) => a.kind() == b.kind(),
_ => false,
}
}
}
impl Eq for XmlError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_names_match_tinyxml2() {
assert_eq!(XmlError::NoAttribute.name(), "XML_NO_ATTRIBUTE");
assert_eq!(
XmlError::WrongAttributeType.name(),
"XML_WRONG_ATTRIBUTE_TYPE"
);
assert_eq!(XmlError::EmptyDocument.name(), "XML_ERROR_EMPTY_DOCUMENT");
assert_eq!(
XmlError::CanNotConvertText.name(),
"XML_CAN_NOT_CONVERT_TEXT"
);
assert_eq!(XmlError::NoTextNode.name(), "XML_NO_TEXT_NODE");
assert_eq!(
XmlError::ElementDepthExceeded {
line: 1,
max_depth: 500
}
.name(),
"XML_ELEMENT_DEPTH_EXCEEDED"
);
}
#[test]
fn parse_error_names() {
let cases = [
(ParseErrorKind::Element, "XML_ERROR_PARSING_ELEMENT"),
(ParseErrorKind::Attribute, "XML_ERROR_PARSING_ATTRIBUTE"),
(ParseErrorKind::Text, "XML_ERROR_PARSING_TEXT"),
(ParseErrorKind::Cdata, "XML_ERROR_PARSING_CDATA"),
(ParseErrorKind::Comment, "XML_ERROR_PARSING_COMMENT"),
(ParseErrorKind::Declaration, "XML_ERROR_PARSING_DECLARATION"),
(ParseErrorKind::Unknown, "XML_ERROR_PARSING_UNKNOWN"),
(ParseErrorKind::General, "XML_ERROR_PARSING"),
];
for (kind, expected_name) in cases {
let err = XmlError::Parse {
kind,
line: 1,
message: None,
};
assert_eq!(err.name(), expected_name);
}
}
#[test]
fn error_line_numbers() {
assert_eq!(XmlError::NoAttribute.line(), None);
assert_eq!(
XmlError::Parse {
kind: ParseErrorKind::Element,
line: 42,
message: None,
}
.line(),
Some(42)
);
assert_eq!(
XmlError::MismatchedElement {
line: 10,
expected: "foo".into(),
found: "bar".into(),
}
.line(),
Some(10)
);
assert_eq!(
XmlError::ElementDepthExceeded {
line: 500,
max_depth: 100,
}
.line(),
Some(500)
);
}
#[test]
fn display_formatting() {
assert_eq!(XmlError::NoAttribute.to_string(), "attribute not found");
assert_eq!(XmlError::EmptyDocument.to_string(), "empty document");
let parse_err = XmlError::Parse {
kind: ParseErrorKind::Element,
line: 5,
message: Some("unexpected character".into()),
};
assert_eq!(
parse_err.to_string(),
"parse error (XML_ERROR_PARSING_ELEMENT) at line 5: unexpected character"
);
let mismatch = XmlError::MismatchedElement {
line: 3,
expected: "div".into(),
found: "span".into(),
};
assert_eq!(
mismatch.to_string(),
"mismatched element at line 3: expected </div>, found </span>"
);
}
#[test]
fn error_is_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<XmlError>();
assert_sync::<XmlError>();
}
#[test]
fn io_error_conversion() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let xml_err: XmlError = io_err.into();
assert_eq!(xml_err.name(), "XML_ERROR_FILE_READ_ERROR");
assert!(xml_err.to_string().contains("file not found"));
}
#[test]
fn error_clone_and_eq() {
let err = XmlError::Parse {
kind: ParseErrorKind::Element,
line: 10,
message: Some("test".into()),
};
let cloned = err.clone();
assert_eq!(err, cloned);
}
#[test]
fn std_error_source() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
let xml_err = XmlError::Io(io_err);
assert!(std::error::Error::source(&xml_err).is_some());
assert!(std::error::Error::source(&XmlError::NoAttribute).is_none());
}
}