Skip to main content

ergo_sbe/xml/
error.rs

1//! Parse errors and internal fault staging.
2
3use std::ops::Range;
4use std::path::PathBuf;
5
6use roxmltree::Node;
7
8/// Why an `xi:include` failed.
9#[derive(Debug, thiserror::Error)]
10#[non_exhaustive]
11pub enum IncludeCause {
12    /// The include graph contains a cycle.
13    #[error("cyclic include: {}", cycle_display(chain))]
14    Cycle {
15        /// Canonical paths in visit order, ending at the repeated file.
16        chain: Vec<PathBuf>,
17    },
18    /// A candidate path existed but could not be read.
19    #[error("cannot read {}: {source}", path.display())]
20    Io {
21        /// Path that failed to read.
22        path: PathBuf,
23        /// Underlying I/O error.
24        #[source]
25        source: std::io::Error,
26    },
27    /// No candidate path existed.
28    #[error("include file not found")]
29    NotFound,
30}
31
32fn cycle_display(chain: &[PathBuf]) -> String {
33    chain
34        .iter()
35        .map(|p| p.display().to_string())
36        .collect::<Vec<_>>()
37        .join(" -> ")
38}
39
40/// Errors raised while parsing an SBE schema. Carries a [`miette`] source span
41/// so the offending XML element is highlighted in the rendered diagnostic.
42#[derive(Debug, thiserror::Error, miette::Diagnostic)]
43#[non_exhaustive]
44pub enum ParseError {
45    /// The XML document itself was malformed.
46    #[error("malformed XML: {message}")]
47    #[diagnostic(code(ergo_sbe::schema_parse::malformed_xml))]
48    MalformedXml {
49        /// What went wrong.
50        message: String,
51        /// The source document, for span rendering.
52        #[source_code]
53        source_code: miette::NamedSource<String>,
54        /// The offending location, when available.
55        #[label("here")]
56        span: Option<miette::SourceSpan>,
57    },
58    /// A required attribute or element was missing.
59    #[error("missing {what}")]
60    #[diagnostic(code(ergo_sbe::schema_parse::missing))]
61    Missing {
62        /// What was missing (element/attribute context).
63        what: String,
64        /// The source document, for span rendering.
65        #[source_code]
66        source_code: miette::NamedSource<String>,
67        /// The offending location, when a node was available.
68        #[label("missing here")]
69        span: Option<miette::SourceSpan>,
70    },
71    /// An attribute value was invalid.
72    #[error("invalid {what}: {value}")]
73    #[diagnostic(code(ergo_sbe::schema_parse::invalid))]
74    Invalid {
75        /// What was invalid.
76        what: String,
77        /// The offending value.
78        value: String,
79        /// The source document, for span rendering.
80        #[source_code]
81        source_code: miette::NamedSource<String>,
82        /// The offending location.
83        #[label("invalid here")]
84        span: Option<miette::SourceSpan>,
85    },
86    /// A schema resolution or validation error occurred.
87    #[error("resolution error: {error}")]
88    #[diagnostic(code(ergo_sbe::schema_parse::resolve))]
89    Resolve {
90        /// The source document, for span rendering.
91        #[source_code]
92        source_code: miette::NamedSource<String>,
93        /// The primary offending location.
94        #[label("here")]
95        span: Option<miette::SourceSpan>,
96        /// Secondary label (e.g. for duplicate definitions).
97        #[label("related")]
98        second_label: Option<miette::SourceSpan>,
99        /// The underlying resolution error.
100        #[source]
101        error: Box<crate::resolve::ResolveError>,
102    },
103    /// Root schema file could not be read.
104    #[error("cannot read {}: {source}", path.display())]
105    #[diagnostic(code(ergo_sbe::schema_parse::io))]
106    Io {
107        /// Path that failed to read.
108        path: PathBuf,
109        /// Underlying I/O error.
110        #[source]
111        source: std::io::Error,
112    },
113    /// An include (`xi:include`) resolution error occurred.
114    #[error("include error for '{href}': {cause}")]
115    #[diagnostic(code(ergo_sbe::schema_parse::include))]
116    Include {
117        /// The `href` attribute from the include element.
118        href: String,
119        /// Candidate paths that were tried, in order.
120        attempted: Vec<PathBuf>,
121        /// Machine-readable failure kind.
122        #[source]
123        cause: IncludeCause,
124        /// The source document, for span rendering.
125        #[source_code]
126        source_code: miette::NamedSource<String>,
127        /// The offending include element, when available.
128        #[label("include error here")]
129        span: Option<miette::SourceSpan>,
130    },
131}
132
133impl ParseError {
134    pub(crate) fn malformed_xml(name: &str, message: impl Into<String>, xml: &str) -> Self {
135        Self::MalformedXml {
136            message: message.into(),
137            source_code: named_source(name, xml),
138            span: None,
139        }
140    }
141
142    pub(crate) fn io(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
143        Self::Io {
144            path: path.into(),
145            source,
146        }
147    }
148
149    /// Lift an internal [`Fault`] into a span-bearing [`ParseError`], attaching
150    /// the parsed source so `miette` can render the highlight.
151    pub(crate) fn from_fault(name: &str, fault: Fault, input: &str) -> Self {
152        let source_code = named_source(name, input);
153        let span = fault.span.map(miette::SourceSpan::from);
154        match fault.kind {
155            FaultKind::Missing { what } => Self::Missing {
156                what,
157                source_code,
158                span,
159            },
160            FaultKind::Invalid { what, value } => Self::Invalid {
161                what,
162                value,
163                source_code,
164                span,
165            },
166            FaultKind::Include {
167                href,
168                attempted,
169                cause,
170            } => Self::Include {
171                href,
172                attempted,
173                cause,
174                source_code,
175                span,
176            },
177        }
178    }
179}
180
181impl From<crate::resolve::ResolveError> for ParseError {
182    fn from(mut e: crate::resolve::ResolveError) -> Self {
183        let source_code = e
184            .take_source_code()
185            .unwrap_or_else(|| miette::NamedSource::new("schema.xml", String::new()));
186        let (span, second_label) = e.take_spans();
187        Self::Resolve {
188            source_code,
189            span,
190            second_label,
191            error: Box::new(e),
192        }
193    }
194}
195
196/// Build a [`miette::NamedSource`] with the real source name, not the
197/// old hardcoded `"schema.xml"`. Callers thread the name from [`WarnState`]
198/// (set by the entry point — file path or `"<xml>"`).
199pub(crate) fn named_source(name: &str, xml: &str) -> miette::NamedSource<String> {
200    miette::NamedSource::new(name, xml.to_owned())
201}
202
203/// Internal, source-free error — converted to [`ParseError`] at the boundary,
204/// where the parsed source text is known. Keeps the recursive helpers cheap and
205/// free of source-cloning on the success path.
206#[derive(Debug)]
207pub(crate) struct Fault {
208    pub(crate) kind: FaultKind,
209    pub(crate) span: Option<Range<usize>>,
210}
211
212#[derive(Debug)]
213pub(crate) enum FaultKind {
214    Missing {
215        what: String,
216    },
217    Invalid {
218        what: String,
219        value: String,
220    },
221    Include {
222        href: String,
223        attempted: Vec<PathBuf>,
224        cause: IncludeCause,
225    },
226}
227
228impl Fault {
229    pub(crate) fn missing(node: Node<'_, '_>, what: impl Into<String>) -> Self {
230        Self {
231            kind: FaultKind::Missing { what: what.into() },
232            span: Some(node.range()),
233        }
234    }
235    pub(crate) fn missing_no_node(what: impl Into<String>) -> Self {
236        Self {
237            kind: FaultKind::Missing { what: what.into() },
238            span: None,
239        }
240    }
241
242    pub(crate) fn invalid(
243        node: Node<'_, '_>,
244        what: impl Into<String>,
245        value: impl Into<String>,
246    ) -> Self {
247        Self {
248            kind: FaultKind::Invalid {
249                what: what.into(),
250                value: value.into(),
251            },
252            span: Some(node.range()),
253        }
254    }
255
256    pub(crate) fn include(
257        href: impl Into<String>,
258        attempted: Vec<PathBuf>,
259        cause: IncludeCause,
260    ) -> Self {
261        Self {
262            kind: FaultKind::Include {
263                href: href.into(),
264                attempted,
265                cause,
266            },
267            span: None,
268        }
269    }
270}