use std::{borrow::Cow, error::Error, fmt::Display, io, num::ParseIntError};
use thiserror::Error;
pub trait MaybeFatal {
fn is_fatal(&self) -> bool {
false
}
}
pub trait EmitNonFatal<E> {
fn emit_non_fatal(self) -> Result<(), E>;
fn emit_unconditional(self);
}
impl<E: MaybeFatal + Display> EmitNonFatal<E> for E {
fn emit_non_fatal(self) -> Result<(), E> {
if self.is_fatal() {
Err(self)
} else {
eprintln!("WARNING: {self}");
Ok(())
}
}
fn emit_unconditional(self) {
if self.is_fatal() {
panic!("emit_unconditional called on fatal error: {self}");
} else {
eprintln!("WARNING: {self}");
}
}
}
impl<E: MaybeFatal + Display> EmitNonFatal<E> for Result<(), E> {
fn emit_non_fatal(self) -> Result<(), E> {
match self {
Ok(()) => Ok(()),
Err(e) => {
if e.is_fatal() {
Err(e)
} else {
eprintln!("WARNING: {e}");
Ok(())
}
},
}
}
fn emit_unconditional(self) {
if let Err(e) = self {
if e.is_fatal() {
panic!("emit_unconditional called on fatal error: {e}");
} else {
eprintln!("WARNING: {e}");
}
}
}
}
#[derive(Debug, Error)]
pub enum FinalError {
#[error("invalid commandline argument: {0}")]
Args(#[from] ArgsError),
#[error(transparent)]
Interaction(#[from] InteractivityError),
#[error(transparent)]
ApiKey(#[from] ApiKeyError), #[error(transparent)]
Request(#[from] RequestError), #[error("no search results :(")]
NoSearchResults,
#[error("failed to format output as requested: {0}")]
FormatOutput(Box<dyn Error>),
}
impl FinalError {
pub fn error_code(&self) -> i32 {
use FinalError::*;
match self {
NoSearchResults => 0,
Args(_) => 1,
ApiKey(_) | Request(_) | FormatOutput(_) => 2,
Interaction(inner) => (inner.is_fatal() as i32) * 2,
}
}
}
impl MaybeFatal for FinalError {
fn is_fatal(&self) -> bool {
use FinalError::*;
match self {
Interaction(inner) => inner.is_fatal(),
_ => true,
}
}
}
impl From<serde_json::Error> for FinalError {
fn from(err: serde_json::Error) -> Self {
FinalError::FormatOutput(Box::new(err))
}
}
#[cfg(feature = "yaml")]
impl From<serde_yaml::Error> for FinalError {
fn from(err: serde_yaml::Error) -> Self {
FinalError::FormatOutput(Box::new(err))
}
}
#[derive(Debug, Error)]
pub enum ArgsError {
#[error("bad number of results: {0}")]
NumberOfResults(#[from] ParseIntError),
#[error("bad year: {0}")]
NotYear(#[from] YearParseError),
#[error("bad output format: {0}")]
OutputFormat(#[from] OutputFormatParseError),
#[error(transparent)]
MediaType(#[from] MediaTypeParseError),
#[error(transparent)]
SearchTerm(#[from] InteractivityError),
}
#[cfg(test)]
impl PartialEq for ArgsError {
fn eq(&self, other: &Self) -> bool {
use ArgsError::*;
match (self, other) {
(NumberOfResults(a), NumberOfResults(b)) => a == b,
(NotYear(a), NotYear(b)) => a == b,
(OutputFormat(a), OutputFormat(b)) => a == b,
(MediaType(a), MediaType(b)) => a == b,
(SearchTerm(_), SearchTerm(_)) => true,
_ => false,
}
}
}
#[derive(Debug, Error)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub enum OutputFormatParseError {
#[error(
"this format isn't supported because you didn't enable it at compile \
time.\nYou can 'enable' this by running `cargo install imdb-id \
--force --features {0}`"
)]
NotInstalled(String),
#[error("{0:?} is not a recognised output format")]
Unrecognised(String),
}
#[derive(Debug, Error)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub enum YearParseError {
#[error(transparent)]
InvalidInt(#[from] ParseIntError),
#[error("no year was specified at either end of the range")]
NoYearsSpecified,
#[error("start of date range is in the future")]
StartInFuture,
}
#[derive(Debug, Error)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[error("unrecognised media type {0:?}")]
pub struct MediaTypeParseError(pub String);
#[derive(Debug, Error)]
pub enum InteractivityError {
#[error("user aborted operation")]
Cancel,
#[error(
"unexpected CLI error: {0}\nIf you were just trying to stop running \
the program, please create an issue about this"
)]
Dialoguer(dialoguer::Error),
#[error("unexpected crossterm error: {0}")]
Crossterm(io::Error),
#[error("unexpected TUI error: {0}")]
Tui(io::Error),
}
impl MaybeFatal for InteractivityError {
fn is_fatal(&self) -> bool {
!matches!(self, InteractivityError::Cancel)
}
}
impl From<dialoguer::Error> for InteractivityError {
fn from(err: dialoguer::Error) -> Self {
use InteractivityError::*;
match err {
dialoguer::Error::IO(io_err)
if io_err.kind() == io::ErrorKind::NotConnected =>
{
Cancel
},
_ => Dialoguer(err),
}
}
}
#[derive(Debug, Error)]
pub enum RequestError {
#[error("issue with request: {0}")]
Web(#[from] minreq::Error),
#[error(
"Failed to parse response from OMDb, please raise an issue including \
the following text:\nSerde error: {0}\nJSON: \n```json\n{1}\n```"
)]
Deserialisation(serde_json::Error, String),
#[error("OMDb gave us an error: {0}")]
Omdb(String),
}
impl MaybeFatal for RequestError {
fn is_fatal(&self) -> bool {
use RequestError::*;
!matches!(self, Deserialisation(_, _))
}
}
#[derive(Debug, Error)]
pub enum SignUpError {
#[error(transparent)]
Interactivity(#[from] InteractivityError),
#[error(transparent)]
MinReq(#[from] minreq::Error),
#[error("response didn't indicate success")]
NeedleNotFound,
}
impl MaybeFatal for SignUpError {
fn is_fatal(&self) -> bool {
use SignUpError::*;
match self {
Interactivity(inner) => inner.is_fatal(),
MinReq(_) => true,
NeedleNotFound => false,
}
}
}
#[derive(Debug, Error)]
pub enum ApiKeyError {
#[error("invalid API key format")]
InvalidFormat,
#[error("issue with web request: {0}")]
RequestFailed(#[from] minreq::Error),
#[error("unauthorised API key")]
Unauthorised,
#[error("unexpected response to API key, status {0}")]
UnexpectedStatus(i32),
}
impl MaybeFatal for ApiKeyError {
fn is_fatal(&self) -> bool {
use ApiKeyError::*;
match self {
InvalidFormat | Unauthorised => false,
RequestFailed(_) | UnexpectedStatus(_) => true,
}
}
}
#[derive(Debug, Error)]
pub enum DiskError {
#[error("config file does not exist at {0}")] NotFound(Cow<'static, str>), #[error("failed to read saved config: {0}")]
Read(io::Error),
#[error("failed to interpret saved config at {1}: {0}")]
Deserialise(#[source] serde_json::Error, Cow<'static, str>),
#[error("failed to save config: {0}")]
Write(io::Error),
#[error("failed to convert config to JSON for writing: {0}")]
Serialise(serde_json::Error),
}
impl MaybeFatal for DiskError {}