#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorKind {
SourceUnavailable,
PermissionDenied,
DecodeFailed,
QuicklookUnavailable,
ModelUnavailable,
LibraryBusy,
LibrarySchema,
Database,
}
impl ErrorKind {
pub const ALL: [ErrorKind; 8] = [
ErrorKind::SourceUnavailable,
ErrorKind::PermissionDenied,
ErrorKind::DecodeFailed,
ErrorKind::QuicklookUnavailable,
ErrorKind::ModelUnavailable,
ErrorKind::LibraryBusy,
ErrorKind::LibrarySchema,
ErrorKind::Database,
];
pub fn code(self) -> &'static str {
match self {
ErrorKind::SourceUnavailable => "source_unavailable",
ErrorKind::PermissionDenied => "permission_denied",
ErrorKind::DecodeFailed => "decode_failed",
ErrorKind::QuicklookUnavailable => "quicklook_unavailable",
ErrorKind::ModelUnavailable => "model_unavailable",
ErrorKind::LibraryBusy => "library_busy",
ErrorKind::LibrarySchema => "library_schema",
ErrorKind::Database => "database",
}
}
pub fn remediation(self) -> Option<&'static str> {
match self {
ErrorKind::SourceUnavailable => {
Some("Reconnect the drive holding the library, then run the command again.")
}
ErrorKind::PermissionDenied => Some("Grant read access to the file or folder."),
ErrorKind::DecodeFailed => {
Some("The file is unreadable or unsupported; videre skips it after two attempts.")
}
ErrorKind::QuicklookUnavailable => Some(
"HEIC and video need macOS QuickLook; these files are skipped on this platform.",
),
ErrorKind::ModelUnavailable => {
Some("Check network access for the first run, or the Hugging Face cache location.")
}
ErrorKind::LibraryBusy => {
Some("Another videre command is using this library; retry when it finishes.")
}
ErrorKind::LibrarySchema | ErrorKind::Database => None,
}
}
pub fn in_chain(err: &anyhow::Error) -> Option<ErrorKind> {
err.downcast_ref::<ErrorKind>().copied().or_else(|| {
err.chain()
.any(|e| e.is::<rusqlite::Error>())
.then_some(ErrorKind::Database)
})
}
}
impl std::fmt::Display for ErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
ErrorKind::SourceUnavailable => "the file could not be read from its drive",
ErrorKind::PermissionDenied => "permission denied",
ErrorKind::DecodeFailed => "the file could not be decoded",
ErrorKind::QuicklookUnavailable => "QuickLook is unavailable",
ErrorKind::ModelUnavailable => "the model could not be loaded",
ErrorKind::LibraryBusy => "the library is busy",
ErrorKind::LibrarySchema => "the library database needs attention",
ErrorKind::Database => "database error",
})
}
}
impl std::error::Error for ErrorKind {}
pub fn from_io(e: std::io::Error) -> anyhow::Error {
const EIO: i32 = 5;
let kind = match e.kind() {
std::io::ErrorKind::TimedOut | std::io::ErrorKind::NotFound => {
Some(ErrorKind::SourceUnavailable)
}
std::io::ErrorKind::PermissionDenied => Some(ErrorKind::PermissionDenied),
_ if e.raw_os_error() == Some(EIO) => Some(ErrorKind::SourceUnavailable),
_ => None,
};
let err = anyhow::Error::new(e);
match kind {
Some(kind) => err.context(kind),
None => err,
}
}
pub fn from_image(e: image::ImageError) -> anyhow::Error {
match e {
image::ImageError::IoError(io) => from_io(io),
other => anyhow::Error::new(other).context(ErrorKind::DecodeFailed),
}
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Context;
#[test]
fn codes_are_unique_and_snake_case() {
let mut seen = std::collections::HashSet::new();
for kind in ErrorKind::ALL {
let code = kind.code();
assert!(seen.insert(code), "duplicate code {code}");
assert!(
code.chars().all(|c| c.is_ascii_lowercase() || c == '_'),
"{code}"
);
}
}
#[test]
fn a_kind_is_found_anywhere_in_the_context_chain() {
let err = Err::<(), _>(std::io::Error::other("read timed out"))
.context(ErrorKind::SourceUnavailable)
.context("hash /Volumes/Fotoğraflar/İstanbul/Çağla_2019.jpg")
.unwrap_err();
assert_eq!(
ErrorKind::in_chain(&err),
Some(ErrorKind::SourceUnavailable)
);
}
#[test]
fn io_errors_carry_the_kind_their_cause_implies() {
use std::io::{Error, ErrorKind as Io};
let timed_out = from_io(Error::new(Io::TimedOut, "read timed out"));
assert_eq!(
ErrorKind::in_chain(&timed_out),
Some(ErrorKind::SourceUnavailable)
);
let denied = from_io(Error::new(Io::PermissionDenied, "denied"));
assert_eq!(
ErrorKind::in_chain(&denied),
Some(ErrorKind::PermissionDenied)
);
let other = from_io(Error::new(Io::InvalidData, "bad bytes"));
assert_eq!(ErrorKind::in_chain(&other), None);
assert!(format!("{other:#}").contains("bad bytes"));
let missing = from_io(Error::new(Io::NotFound, "gone"));
assert_eq!(
ErrorKind::in_chain(&missing),
Some(ErrorKind::SourceUnavailable)
);
let eio = from_io(Error::from_raw_os_error(5));
assert_eq!(
ErrorKind::in_chain(&eio),
Some(ErrorKind::SourceUnavailable)
);
}
#[test]
fn image_errors_are_classified_by_their_cause() {
let io = image::ImageError::IoError(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"denied",
));
assert_eq!(
ErrorKind::in_chain(&from_image(io)),
Some(ErrorKind::PermissionDenied)
);
let bad =
image::ImageError::Unsupported(image::error::UnsupportedError::from_format_and_kind(
image::error::ImageFormatHint::Unknown,
image::error::UnsupportedErrorKind::Format(image::error::ImageFormatHint::Unknown),
));
assert_eq!(
ErrorKind::in_chain(&from_image(bad)),
Some(ErrorKind::DecodeFailed)
);
}
#[test]
fn a_database_error_without_an_explicit_kind_is_the_database_kind() {
let err = anyhow::Error::new(rusqlite::Error::InvalidQuery).context("load rows");
assert_eq!(ErrorKind::in_chain(&err), Some(ErrorKind::Database));
let explicit =
anyhow::Error::new(rusqlite::Error::InvalidQuery).context(ErrorKind::LibraryBusy);
assert_eq!(ErrorKind::in_chain(&explicit), Some(ErrorKind::LibraryBusy));
}
#[test]
fn an_error_without_a_kind_has_none() {
let err = anyhow::anyhow!("plain failure").context("outer");
assert_eq!(ErrorKind::in_chain(&err), None);
}
#[test]
fn the_label_reads_as_part_of_a_sentence() {
let err = anyhow::anyhow!("timed out").context(ErrorKind::SourceUnavailable);
assert_eq!(
format!("{err:#}"),
"the file could not be read from its drive: timed out"
);
}
}