use std::fmt;
#[derive(Debug)]
#[non_exhaustive]
pub enum ConvertError {
Unsupported(String),
NeedsOcr {
pages: Vec<u32>,
page_count: u32,
},
Malformed {
part: Option<String>,
detail: String,
},
Encrypted,
ResourceLimit {
limit: &'static str,
detail: String,
},
MissingPart {
part: String,
},
Io(std::io::Error),
}
impl fmt::Display for ConvertError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConvertError::Unsupported(what) => write!(f, "unsupported input: {what}"),
ConvertError::NeedsOcr { pages, page_count } => match pages.as_slice() {
[page] => write!(f, "page {page} of {page_count} needs OCR"),
_ if pages.len() as u32 >= *page_count => {
write!(f, "all {page_count} pages need OCR")
}
_ => write!(f, "pages {} of {page_count} need OCR", page_ranges(pages)),
},
ConvertError::Malformed { part: Some(part), detail } => {
write!(f, "malformed document ({part}): {detail}")
}
ConvertError::Malformed { part: None, detail } => {
write!(f, "malformed document: {detail}")
}
ConvertError::Encrypted => write!(f, "document is encrypted"),
ConvertError::ResourceLimit { limit, detail } => {
write!(f, "resource limit exceeded ({limit}): {detail}")
}
ConvertError::MissingPart { part } => write!(f, "missing required part: {part}"),
ConvertError::Io(e) => write!(f, "io error: {e}"),
}
}
}
impl std::error::Error for ConvertError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ConvertError::Io(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for ConvertError {
fn from(e: std::io::Error) -> Self {
ConvertError::Io(e)
}
}
impl ConvertError {
pub fn code(&self) -> &'static str {
match self {
ConvertError::Unsupported(_) => "unsupported",
ConvertError::NeedsOcr { .. } => "needsOcr",
ConvertError::Malformed { .. } => "malformed",
ConvertError::Encrypted => "encrypted",
ConvertError::ResourceLimit { .. } => "resourceLimit",
ConvertError::MissingPart { .. } => "missingPart",
ConvertError::Io(_) => "io",
}
}
pub(crate) fn malformed(detail: impl Into<String>) -> Self {
ConvertError::Malformed { part: None, detail: detail.into() }
}
pub(crate) fn malformed_part(part: impl Into<String>, detail: impl Into<String>) -> Self {
ConvertError::Malformed { part: Some(part.into()), detail: detail.into() }
}
pub(crate) fn is_fatal(&self) -> bool {
matches!(self, ConvertError::ResourceLimit { .. })
}
}
fn page_ranges(pages: &[u32]) -> String {
let mut ranges = Vec::new();
let mut pages = pages.iter().copied().peekable();
while let Some(start) = pages.next() {
let mut end = start;
while pages.next_if_eq(&(end + 1)).is_some() {
end += 1;
}
ranges.push(if end > start { format!("{start}-{end}") } else { start.to_string() });
}
ranges.join(", ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn needs_ocr_names_the_pages() {
let scattered = ConvertError::NeedsOcr { pages: vec![2, 5, 6, 7, 12], page_count: 20 };
assert_eq!(scattered.to_string(), "pages 2, 5-7, 12 of 20 need OCR");
let all = ConvertError::NeedsOcr { pages: vec![1, 2], page_count: 2 };
assert_eq!(all.to_string(), "all 2 pages need OCR");
}
#[test]
fn codes_name_every_variant() {
assert_eq!(ConvertError::Unsupported(String::new()).code(), "unsupported");
assert_eq!(ConvertError::NeedsOcr { pages: vec![1], page_count: 1 }.code(), "needsOcr");
assert_eq!(ConvertError::malformed("").code(), "malformed");
assert_eq!(ConvertError::malformed_part("word/document.xml", "").code(), "malformed");
assert_eq!(ConvertError::Encrypted.code(), "encrypted");
let limit = ConvertError::ResourceLimit { limit: "max_entry_bytes", detail: String::new() };
assert_eq!(limit.code(), "resourceLimit");
assert_eq!(ConvertError::MissingPart { part: String::new() }.code(), "missingPart");
assert_eq!(ConvertError::Io(std::io::ErrorKind::NotFound.into()).code(), "io");
}
}