Skip to main content

sup_xml_xslt/
error.rs

1//! `XsltError` — single error type covering compile + apply + serialise.
2//!
3//! Wraps the underlying `XmlError` from `sup-xml-core` for any
4//! XPath evaluation failures hit during a transformation (an
5//! `<xsl:value-of select="bad-xpath()"/>` surfaces as `Xpath`),
6//! and exposes engine-specific variants for stylesheet structure
7//! problems and runtime termination.
8
9use sup_xml_core::error::XmlError;
10
11#[derive(Debug)]
12pub enum XsltError {
13    /// Stylesheet structure is malformed — e.g. an `xsl:template`
14    /// with neither `match=` nor `name=`, a top-level element in
15    /// the XSLT namespace that isn't a declaration.
16    InvalidStylesheet(String),
17
18    /// A template body referenced something the static analysis
19    /// couldn't resolve — e.g. `xsl:call-template name="X"` where
20    /// no template `X` exists.
21    UnresolvedReference(String),
22
23    /// An XPath expression failed during transformation.
24    Xpath(XmlError),
25
26    /// `<xsl:message terminate="yes">` fired.  Carries the
27    /// message-element's stringified content.
28    Terminated(String),
29}
30
31impl std::fmt::Display for XsltError {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            XsltError::InvalidStylesheet(msg)  => write!(f, "invalid stylesheet: {msg}"),
35            XsltError::UnresolvedReference(msg) => write!(f, "unresolved reference: {msg}"),
36            // When the error carries a source position (file:line:col,
37            // and the ground-truth byte offset), lead with the
38            // editor-clickable prefix; otherwise fall back to the bare
39            // "xpath error" label.
40            XsltError::Xpath(e) => match (&e.file, e.line, e.column) {
41                (Some(file), Some(line), Some(col)) => {
42                    write!(f, "{file}:{line}:{col}: {}", e.message)?;
43                    if let Some(ofs) = e.byte_offset {
44                        write!(f, " @ byte {ofs}")?;
45                    }
46                    Ok(())
47                }
48                _ => write!(f, "xpath error: {}", e.message),
49            },
50            XsltError::Terminated(msg)         => write!(f, "xsl:message terminate=yes: {msg}"),
51        }
52    }
53}
54
55impl std::error::Error for XsltError {}
56
57impl From<XmlError> for XsltError {
58    fn from(e: XmlError) -> Self { XsltError::Xpath(e) }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use sup_xml_core::error::{ErrorDomain, ErrorLevel};
65
66    #[test]
67    fn display_invalid_stylesheet() {
68        let e = XsltError::InvalidStylesheet("bad <foo>".into());
69        assert_eq!(format!("{e}"), "invalid stylesheet: bad <foo>");
70    }
71
72    #[test]
73    fn display_unresolved_reference() {
74        let e = XsltError::UnresolvedReference("template X".into());
75        assert_eq!(format!("{e}"), "unresolved reference: template X");
76    }
77
78    #[test]
79    fn display_xpath_wraps_inner_message() {
80        let inner = XmlError::new(ErrorDomain::XPath, ErrorLevel::Error, "syntax");
81        let e = XsltError::Xpath(inner);
82        assert_eq!(format!("{e}"), "xpath error: syntax");
83    }
84
85    #[test]
86    fn display_terminated() {
87        let e = XsltError::Terminated("user-requested halt".into());
88        assert_eq!(format!("{e}"), "xsl:message terminate=yes: user-requested halt");
89    }
90
91    #[test]
92    fn from_xml_error_wraps_as_xpath() {
93        let inner = XmlError::new(ErrorDomain::XPath, ErrorLevel::Error, "div by zero");
94        let e: XsltError = inner.into();
95        assert!(matches!(e, XsltError::Xpath(_)));
96        assert_eq!(format!("{e}"), "xpath error: div by zero");
97    }
98
99    #[test]
100    fn implements_std_error_trait() {
101        // Smoke test: XsltError must be usable as `&dyn std::error::Error`.
102        let e = XsltError::InvalidStylesheet("x".into());
103        let err: &dyn std::error::Error = &e;
104        assert!(err.to_string().contains("invalid stylesheet"));
105    }
106
107    #[test]
108    fn debug_format_works() {
109        // The #[derive(Debug)] impl — exercise it so it isn't listed as
110        // an uncovered function.
111        let e = XsltError::Terminated("x".into());
112        let s = format!("{e:?}");
113        assert!(s.contains("Terminated"));
114    }
115}