const EX_USAGE: i32 = 64;
const EX_IOERR: i32 = 74;
const EX_CONFIG: i32 = 78;
#[non_exhaustive]
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum Error {
#[error(transparent)]
UserInput(#[from] UserInputError),
#[error(transparent)]
System(#[from] SystemError),
}
#[non_exhaustive]
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum UserInputError {
#[error("Invalid date format: {0}")]
InvalidDateFormat(String),
#[error("Unsupported format: {0}")]
UnsupportedFormat(String),
#[error("Invalid date: {0}")]
InvalidDate(String),
#[error("Ambiguous datetime: {0}")]
AmbiguousDateTime(String),
#[error("Unsupported timezone: {0}")]
UnsupportedTimezone(String),
#[error("Invalid 'now' argument: {0}")]
InvalidNow(String),
#[error("Missing required argument: {0}")]
MissingArgument(String),
}
#[non_exhaustive]
#[derive(thiserror::Error, Debug)]
pub enum SystemError {
#[error("Configuration error: {0}")]
Config(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
impl Error {
pub fn exit(self) -> ! {
match self {
Error::UserInput(err) => {
eprintln!("{}", colorize_suggestion(&format!("{err}")));
std::process::exit(EX_USAGE);
}
Error::System(err) => {
eprintln!("System error: {}", err);
match err {
SystemError::Config(_) => std::process::exit(EX_CONFIG),
SystemError::Io(_) => std::process::exit(EX_IOERR),
}
}
}
}
}
fn colorize_suggestion(msg: &str) -> String {
use std::io::IsTerminal;
if !std::io::stderr().is_terminal() || std::env::var("NO_COLOR").is_ok() {
return msg.to_string();
}
if let Some(start) = msg.find("Did you mean '") {
let prefix_end = start + "Did you mean '".len();
if let Some(end) = msg[prefix_end..].find("'?") {
let word = &msg[prefix_end..prefix_end + end];
return format!(
"{}Did you mean '\x1b[33m{}\x1b[0m'?{}",
&msg[..start],
word,
&msg[prefix_end + end + 2..],
);
}
}
msg.to_string()
}
impl PartialEq for SystemError {
fn eq(&self, other: &Self) -> bool {
use SystemError::*;
match (self, other) {
(Config(a), Config(b)) => a == b,
(Io(a), Io(b)) => a.kind() == b.kind(),
_ => false,
}
}
}
impl Eq for SystemError {}
#[macro_export]
macro_rules! user_input_error {
($err_type:ident, $msg:expr) => {
$crate::errors::Error::UserInput($crate::errors::UserInputError::$err_type($msg.to_string()))
};
($err_type:ident, $($arg:tt)*) => {
$crate::errors::Error::UserInput($crate::errors::UserInputError::$err_type(format!($($arg)*)))
};
($err_type:ident) => {
$crate::errors::Error::UserInput($crate::errors::UserInputError::$err_type(String::new()))
};
}
#[macro_export]
macro_rules! system_error {
($err_type:ident, $msg:expr) => {
$crate::errors::Error::System($crate::errors::SystemError::$err_type($msg.to_string()))
};
($err_type:ident, $($arg:tt)*) => {
$crate::errors::Error::System($crate::errors::SystemError::$err_type(format!($($arg)*)))
};
($err_type:ident) => {
$crate::errors::Error::System($crate::errors::SystemError::$err_type(String::new()))
};
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
#[test]
fn user_input_macro_literal() {
let err = user_input_error!(InvalidDateFormat, "foo");
assert!(matches!(
err,
Error::UserInput(UserInputError::InvalidDateFormat(ref s)) if s == "foo"
));
}
#[test]
fn user_input_macro_formatted() {
let err = user_input_error!(MissingArgument, "missing {}", "--format");
assert!(matches!(
err,
Error::UserInput(UserInputError::MissingArgument(ref s)) if s == "missing --format"
));
}
#[test]
fn user_input_macro_empty() {
let err = user_input_error!(InvalidNow);
assert!(matches!(
err,
Error::UserInput(UserInputError::InvalidNow(ref s)) if s.is_empty()
));
}
#[test]
fn system_error_macro_literal() {
let err = system_error!(Config, "invalid field");
assert!(matches!(
err,
Error::System(SystemError::Config(ref s)) if s == "invalid field"
));
}
#[test]
fn system_error_macro_formatted() {
let err = system_error!(Config, "failed to read {}", "/tmp/foo");
assert!(matches!(
err,
Error::System(SystemError::Config(ref s)) if s == "failed to read /tmp/foo"
));
}
#[test]
fn system_error_macro_empty() {
let err = system_error!(Config);
assert!(matches!(
err,
Error::System(SystemError::Config(ref s)) if s.is_empty()
));
}
#[test]
fn unsupported_format_error() {
let err = user_input_error!(UnsupportedFormat, "bad format");
assert!(matches!(
err,
Error::UserInput(UserInputError::UnsupportedFormat(ref s)) if s == "bad format"
));
}
#[test]
fn conversion_from_io_error() {
let err: Error = std::io::Error::from(std::io::ErrorKind::PermissionDenied).into();
assert!(matches!(err, Error::System(SystemError::Io(_))));
}
#[test]
fn system_error_partial_eq_config() {
let a = SystemError::Config("x".into());
let b = SystemError::Config("x".into());
let c = SystemError::Config("y".into());
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn system_error_partial_eq_io() {
let a = SystemError::Io(std::io::Error::from(std::io::ErrorKind::NotFound));
let b = SystemError::Io(std::io::Error::from(std::io::ErrorKind::NotFound));
let c = SystemError::Io(std::io::Error::from(std::io::ErrorKind::PermissionDenied));
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn system_error_partial_eq_different_variants() {
let a = SystemError::Config("x".into());
let b = SystemError::Io(std::io::Error::from(std::io::ErrorKind::NotFound));
assert_ne!(a, b);
}
#[test]
fn error_display_user_input() {
let err = user_input_error!(InvalidDateFormat, "bad date");
assert_eq!(format!("{err}"), "Invalid date format: bad date");
}
#[test]
fn error_display_system() {
let err = system_error!(Config, "broken");
assert_eq!(format!("{err}"), "Configuration error: broken");
}
#[test]
fn new_error_variants_display() {
let err = user_input_error!(InvalidDate, "bad");
assert_eq!(format!("{err}"), "Invalid date: bad");
let err = user_input_error!(AmbiguousDateTime, "ambiguous");
assert_eq!(format!("{err}"), "Ambiguous datetime: ambiguous");
}
}