1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
//! Error type for conversion.
use std::fmt;
use crate::format::InputFormat;
/// Anything that can go wrong while loading or converting a source document.
#[derive(Debug)]
pub enum ConversionError {
/// Reading the input from disk failed.
Io(std::io::Error),
/// The file extension (or content) did not map to a known format.
UnknownFormat { hint: String },
/// The format is known but no backend is wired up for it yet.
UnsupportedFormat(InputFormat),
/// The backend recognized the format but failed to parse the content.
Parse(String),
/// The requested streaming conversion is not supported (e.g. JSON, or the
/// referenced image mode, which both need the whole document up front).
Streaming(String),
/// The headless-browser pre-render (`--use-web-browser`) failed, or the crate
/// was built without the `web-browser` feature.
Browser(String),
/// A dependency failed during conversion. Unlike [`ConversionError::Parse`]
/// the underlying error is kept alive (not flattened into a string), so
/// callers can walk [`std::error::Error::source`] and downcast to the
/// original type — e.g. to tell a truncated archive from malformed XML.
WithSource {
/// What was being converted when the failure happened (backend prefix,
/// mirroring the `Parse` message style: "xlsx", "docling-json", …).
context: String,
/// The error that caused the failure, preserved on the chain.
source: Box<dyn std::error::Error + Send + Sync>,
},
}
impl ConversionError {
/// Wrap a dependency error, keeping it reachable via
/// [`std::error::Error::source`] instead of stringifying it.
pub fn with_source(
context: impl Into<String>,
source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
) -> Self {
ConversionError::WithSource {
context: context.into(),
source: source.into(),
}
}
}
impl fmt::Display for ConversionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConversionError::Io(e) => write!(f, "i/o error: {e}"),
ConversionError::UnknownFormat { hint } => {
write!(f, "could not determine input format (hint: {hint})")
}
ConversionError::UnsupportedFormat(fmt) => {
write!(
f,
"no backend implemented yet for format '{}'",
fmt.as_str()
)
}
ConversionError::Parse(msg) => write!(f, "parse error: {msg}"),
ConversionError::Streaming(msg) => write!(f, "streaming not supported: {msg}"),
ConversionError::Browser(msg) => write!(f, "web-browser render error: {msg}"),
ConversionError::WithSource { context, source } => {
write!(f, "parse error: {context}: {source}")
}
}
}
}
impl std::error::Error for ConversionError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ConversionError::Io(e) => Some(e),
ConversionError::WithSource { source, .. } => Some(source.as_ref()),
_ => None,
}
}
}
impl From<std::io::Error> for ConversionError {
fn from(e: std::io::Error) -> Self {
ConversionError::Io(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error;
#[test]
fn with_source_keeps_the_cause_on_the_chain() {
let cause = serde_json::from_str::<i32>("boom").unwrap_err();
let err = ConversionError::with_source("docling-json", cause);
assert!(err.to_string().starts_with("parse error: docling-json: "));
let source = err.source().expect("source is chained");
assert!(
source.downcast_ref::<serde_json::Error>().is_some(),
"chained source downcasts to the original type"
);
}
#[test]
fn stringly_variants_have_no_source() {
assert!(ConversionError::Parse("x".into()).source().is_none());
}
}