1pub trait Failable: std::error::Error + Send + Sync + 'static {
15 fn exit(self) -> !;
17}
18
19#[derive(thiserror::Error, Debug, PartialEq, Eq)]
21pub enum Error {
22 #[error(transparent)]
24 UserInput(#[from] UserInputError),
25 #[error(transparent)]
28 System(#[from] SystemError),
29}
30
31#[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#[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
55pub 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#[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#[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}