use std::fmt;
use std::process::ExitCode;
#[derive(Debug)]
pub enum AppError {
BadInput(anyhow::Error),
NoResult(anyhow::Error),
Internal(anyhow::Error),
}
impl AppError {
pub fn no_result(e: impl Into<anyhow::Error>) -> Self {
Self::NoResult(e.into())
}
pub fn internal(e: impl Into<anyhow::Error>) -> Self {
Self::Internal(e.into())
}
pub fn exit_code(&self) -> ExitCode {
match self {
Self::BadInput(_) => ExitCode::from(1),
Self::NoResult(_) => ExitCode::from(2),
Self::Internal(_) => ExitCode::from(3),
}
}
fn inner(&self) -> &anyhow::Error {
match self {
Self::BadInput(e) | Self::NoResult(e) | Self::Internal(e) => e,
}
}
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:#}", self.inner())
}
}
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self.inner().as_ref())
}
}
impl From<anyhow::Error> for AppError {
fn from(e: anyhow::Error) -> Self {
Self::BadInput(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_anyhow_defaults_to_bad_input() {
let err: AppError = anyhow::anyhow!("bad path").into();
assert!(matches!(err, AppError::BadInput(_)));
}
#[test]
fn no_result_constructor_categorises_correctly() {
let err = AppError::no_result(anyhow::anyhow!("empty map"));
assert!(matches!(err, AppError::NoResult(_)));
}
#[test]
fn internal_constructor_categorises_correctly() {
let err = AppError::internal(anyhow::anyhow!("invariant broken"));
assert!(matches!(err, AppError::Internal(_)));
}
#[test]
fn display_walks_anyhow_source_chain() {
let inner = anyhow::anyhow!("root cause")
.context("middle")
.context("top");
let err: AppError = inner.into();
let s = format!("{err}");
assert!(s.contains("top"));
assert!(s.contains("root cause"));
}
}