xdoc-rs 0.1.1

Declarative XML engine for Rust
Documentation
use std::fmt;

/// Convenient result type for core XML operations.
pub type XmlResult<T> = Result<T, XmlError>;

/// High-level category for an XML engine error.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorKind {
    InvalidName,
    InvalidNamespace,
    DuplicateNamespacePrefix,
    UnknownNamespacePrefix,
    InvalidOperation,
    NotFound,
    Parse,
    Build,
    Query,
    Validation,
    Signature,
    Io,
}

/// Location in a source XML document, when known.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
    line: Option<usize>,
    column: Option<usize>,
}

impl Span {
    /// Creates a span with a known one-based line and column.
    pub fn new(line: usize, column: usize) -> Self {
        Self {
            line: Some(line),
            column: Some(column),
        }
    }

    /// Creates a span for cases where the exact location is unavailable.
    pub fn unknown() -> Self {
        Self {
            line: None,
            column: None,
        }
    }

    pub fn line(&self) -> Option<usize> {
        self.line
    }

    pub fn column(&self) -> Option<usize> {
        self.column
    }
}

/// Structured error used by the XML engine.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XmlError {
    kind: ErrorKind,
    message: String,
    span: Option<Span>,
}

impl XmlError {
    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
        Self {
            kind,
            message: message.into(),
            span: None,
        }
    }

    pub fn with_span(mut self, span: Span) -> Self {
        self.span = Some(span);
        self
    }

    pub fn invalid_name(name: impl AsRef<str>, reason: impl AsRef<str>) -> Self {
        Self::new(
            ErrorKind::InvalidName,
            format!("invalid XML name `{}`: {}", name.as_ref(), reason.as_ref()),
        )
    }

    pub fn kind(&self) -> &ErrorKind {
        &self.kind
    }

    pub fn message(&self) -> &str {
        &self.message
    }

    pub fn span(&self) -> Option<Span> {
        self.span
    }
}

impl fmt::Display for XmlError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.span {
            Some(span) => match (span.line(), span.column()) {
                (Some(line), Some(column)) => {
                    write!(
                        f,
                        "{:?}: {} at line {}, column {}",
                        self.kind, self.message, line, column
                    )
                }
                _ => write!(f, "{:?}: {} at unknown location", self.kind, self.message),
            },
            None => write!(f, "{:?}: {}", self.kind, self.message),
        }
    }
}

impl std::error::Error for XmlError {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn error_includes_kind_and_message() {
        let error = XmlError::new(ErrorKind::InvalidName, "bad name");

        assert_eq!(error.kind(), &ErrorKind::InvalidName);
        assert_eq!(error.message(), "bad name");
        assert_eq!(error.span(), None);
    }

    #[test]
    fn error_can_include_span() {
        let error = XmlError::new(ErrorKind::Parse, "unexpected token").with_span(Span::new(3, 9));

        assert_eq!(error.span(), Some(Span::new(3, 9)));
        assert!(error.to_string().contains("line 3, column 9"));
    }
}