tardis_cli/
errors.rs

1//! Centralised error and exit‑handling utilities for the **TARDIS** CLI.
2//!
3//! This module provides a single [`Error`] enum that groups together all
4//! *user* and *system* failures plus two convenience macros for constructing
5//! those errors ergonomically.  It also offers the [`Failure`] trait, allowing
6//! any error value to map itself to an appropriate process exit.  All public
7//! items live behind concise documentation so that generated docs.rs output
8//! remains immediately useful without excessive inline comments.
9
10/// Any fatal error that can terminate the application.
11///
12/// Implementations must emit an error message to *stderr* and terminate the
13/// process with a meaningful exit code.
14pub trait Failable: std::error::Error + Send + Sync + 'static {
15    /// Print a diagnostic message and stop the program.
16    fn exit(self) -> !;
17}
18
19/// All possible failures surfaced by the CLI.
20#[derive(thiserror::Error, Debug, PartialEq, Eq)]
21pub enum Error {
22    /// Problems attributable to the user (bad flags, invalid input, …).
23    #[error(transparent)]
24    UserInput(#[from] UserInputError),
25    /// Issues the user cannot fix without changing the environment
26    /// (config corruption, I/O failures, …).
27    #[error(transparent)]
28    System(#[from] SystemError),
29}
30
31/// Human‑error variants.
32#[derive(thiserror::Error, Debug, PartialEq, Eq)]
33pub enum UserInputError {
34    #[error("Invalid date format: {0}")]
35    InvalidDateFormat(String),
36    #[error("Unsupported format: {0}")]
37    UnsupportedFormat(#[from] std::fmt::Error),
38    #[error("Unsupported timezone: {0}")]
39    UnsupportedTimezone(String),
40    #[error("Invalid 'now' argument: {0}")]
41    InvalidNow(String),
42    #[error("Missing required argument: {0}")]
43    MissingArgument(String),
44}
45
46/// Failures that stem from the operating environment or runtime.
47#[derive(thiserror::Error, Debug)]
48pub enum SystemError {
49    #[error("Configuration error: {0}")]
50    Config(String),
51    #[error("IO error: {0}")]
52    Io(#[from] std::io::Error),
53}
54
55/// Crate‑wide `Result` alias that uses the consolidated [`Error`] type.
56pub type Result<T> = std::result::Result<T, Error>;
57
58impl Failable for Error {
59    fn exit(self) -> ! {
60        match self {
61            Error::UserInput(err) => {
62                eprintln!("{}", err);
63                std::process::exit(exitcode::USAGE);
64            }
65            Error::System(err) => {
66                eprintln!("System error: {}", err);
67
68                match err {
69                    SystemError::Config(_) => std::process::exit(exitcode::CONFIG),
70                    SystemError::Io(_) => std::process::exit(exitcode::IOERR),
71                }
72            }
73        }
74    }
75}
76
77impl PartialEq for SystemError {
78    fn eq(&self, other: &Self) -> bool {
79        use SystemError::*;
80        match (self, other) {
81            (Config(a), Config(b)) => a == b,
82            (Io(a), Io(b)) => a.kind() == b.kind(),
83            _ => false,
84        }
85    }
86}
87
88impl Eq for SystemError {}
89
90/// Create an [`Error::UserInput`] of the requested variant with minimal boilerplate.
91#[macro_export]
92macro_rules! user_input_error {
93    ($err_type:ident, $msg:expr) => {
94        $crate::errors::Error::UserInput($crate::errors::UserInputError::$err_type($msg.to_string()))
95    };
96
97    ($err_type:ident, $($arg:tt)*) => {
98        $crate::errors::Error::UserInput($crate::errors::UserInputError::$err_type(format!($($arg)*)))
99    };
100
101    ($err_type:ident) => {
102        $crate::errors::Error::UserInput($crate::errors::UserInputError::$err_type(String::new()))
103    };
104}
105
106/// Create an [`Error::System`] of the requested variant with minimal boilerplate.
107#[macro_export]
108macro_rules! system_error {
109    ($err_type:ident, $msg:expr) => {
110        $crate::errors::Error::System($crate::errors::SystemError::$err_type($msg.to_string()))
111    };
112    ($err_type:ident, $($arg:tt)*) => {
113        $crate::errors::Error::System($crate::errors::SystemError::$err_type(format!($($arg)*)))
114    };
115    ($err_type:ident) => {
116        $crate::errors::Error::System($crate::errors::SystemError::$err_type(String::new()))
117    };
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use std::fmt;
124
125    #[test]
126    fn user_input_macro_literal() {
127        let err = user_input_error!(InvalidDateFormat, "foo");
128        assert!(matches!(
129            err,
130            Error::UserInput(UserInputError::InvalidDateFormat(ref s)) if s == "foo"
131        ));
132    }
133
134    #[test]
135    fn user_input_macro_formatted() {
136        let err = user_input_error!(MissingArgument, "missing {}", "--format");
137        assert!(matches!(
138            err,
139            Error::UserInput(UserInputError::MissingArgument(ref s)) if s == "missing --format"
140        ));
141    }
142
143    #[test]
144    fn user_input_macro_empty() {
145        let err = user_input_error!(InvalidNow);
146        assert!(matches!(
147            err,
148            Error::UserInput(UserInputError::InvalidNow(ref s)) if s.is_empty()
149        ));
150    }
151
152    #[test]
153    fn system_error_macro_literal() {
154        let err = system_error!(Config, "invalid field");
155        assert!(matches!(
156            err,
157            Error::System(SystemError::Config(ref s)) if s == "invalid field"
158        ));
159    }
160
161    #[test]
162    fn system_error_macro_formatted() {
163        let err = system_error!(Config, "failed to read {}", "/tmp/foo");
164        assert!(matches!(
165            err,
166            Error::System(SystemError::Config(ref s)) if s == "failed to read /tmp/foo"
167        ));
168    }
169
170    #[test]
171    fn system_error_macro_empty() {
172        let err = system_error!(Config);
173        assert!(matches!(
174            err,
175            Error::System(SystemError::Config(ref s)) if s.is_empty()
176        ));
177    }
178
179    #[test]
180    fn conversion_from_fmt_error() {
181        let err: Error = fmt::Error.into();
182        assert!(matches!(
183            err,
184            Error::UserInput(UserInputError::UnsupportedFormat(_))
185        ));
186    }
187
188    #[test]
189    fn conversion_from_io_error() {
190        let err: Error = std::io::Error::from(std::io::ErrorKind::PermissionDenied).into();
191        assert!(matches!(err, Error::System(SystemError::Io(_))));
192    }
193}