1const EX_USAGE: i32 = 64;
12const EX_IOERR: i32 = 74;
13const EX_CONFIG: i32 = 78;
14
15#[non_exhaustive]
17#[derive(thiserror::Error, Debug, PartialEq, Eq)]
18pub enum Error {
19 #[error(transparent)]
21 UserInput(#[from] UserInputError),
22 #[error(transparent)]
25 System(#[from] SystemError),
26}
27
28#[non_exhaustive]
30#[derive(thiserror::Error, Debug, PartialEq, Eq)]
31pub enum UserInputError {
32 #[error("Invalid date format: {0}")]
33 InvalidDateFormat(String),
34 #[error("Unsupported format: {0}")]
35 UnsupportedFormat(String),
36 #[error("Invalid date: {0}")]
37 InvalidDate(String),
38 #[error("Ambiguous datetime: {0}")]
39 AmbiguousDateTime(String),
40 #[error("Unsupported timezone: {0}")]
41 UnsupportedTimezone(String),
42 #[error("Invalid 'now' argument: {0}")]
43 InvalidNow(String),
44 #[error("Missing required argument: {0}")]
45 MissingArgument(String),
46}
47
48#[non_exhaustive]
50#[derive(thiserror::Error, Debug)]
51pub enum SystemError {
52 #[error("Configuration error: {0}")]
53 Config(String),
54 #[error("IO error: {0}")]
55 Io(#[from] std::io::Error),
56}
57
58pub type Result<T> = std::result::Result<T, Error>;
60
61impl Error {
62 pub fn exit(self) -> ! {
64 match self {
65 Error::UserInput(err) => {
66 eprintln!("{}", colorize_suggestion(&format!("{err}")));
67 std::process::exit(EX_USAGE);
68 }
69 Error::System(err) => {
70 eprintln!("System error: {}", err);
71
72 match err {
73 SystemError::Config(_) => std::process::exit(EX_CONFIG),
74 SystemError::Io(_) => std::process::exit(EX_IOERR),
75 }
76 }
77 }
78 }
79}
80
81fn colorize_suggestion(msg: &str) -> String {
84 use std::io::IsTerminal;
85
86 if !std::io::stderr().is_terminal() || std::env::var("NO_COLOR").is_ok() {
87 return msg.to_string();
88 }
89
90 if let Some(start) = msg.find("Did you mean '") {
91 let prefix_end = start + "Did you mean '".len();
92 if let Some(end) = msg[prefix_end..].find("'?") {
93 let word = &msg[prefix_end..prefix_end + end];
94 return format!(
95 "{}Did you mean '\x1b[33m{}\x1b[0m'?{}",
96 &msg[..start],
97 word,
98 &msg[prefix_end + end + 2..],
99 );
100 }
101 }
102 msg.to_string()
103}
104
105impl PartialEq for SystemError {
106 fn eq(&self, other: &Self) -> bool {
107 use SystemError::*;
108 match (self, other) {
109 (Config(a), Config(b)) => a == b,
110 (Io(a), Io(b)) => a.kind() == b.kind(),
111 _ => false,
112 }
113 }
114}
115
116impl Eq for SystemError {}
117
118#[macro_export]
120macro_rules! user_input_error {
121 ($err_type:ident, $msg:expr) => {
122 $crate::errors::Error::UserInput($crate::errors::UserInputError::$err_type($msg.to_string()))
123 };
124
125 ($err_type:ident, $($arg:tt)*) => {
126 $crate::errors::Error::UserInput($crate::errors::UserInputError::$err_type(format!($($arg)*)))
127 };
128
129 ($err_type:ident) => {
130 $crate::errors::Error::UserInput($crate::errors::UserInputError::$err_type(String::new()))
131 };
132}
133
134#[macro_export]
136macro_rules! system_error {
137 ($err_type:ident, $msg:expr) => {
138 $crate::errors::Error::System($crate::errors::SystemError::$err_type($msg.to_string()))
139 };
140 ($err_type:ident, $($arg:tt)*) => {
141 $crate::errors::Error::System($crate::errors::SystemError::$err_type(format!($($arg)*)))
142 };
143 ($err_type:ident) => {
144 $crate::errors::Error::System($crate::errors::SystemError::$err_type(String::new()))
145 };
146}
147
148#[cfg(test)]
149mod tests {
150 #![allow(clippy::unwrap_used, clippy::expect_used)]
151
152 use super::*;
153
154 #[test]
155 fn user_input_macro_literal() {
156 let err = user_input_error!(InvalidDateFormat, "foo");
157 assert!(matches!(
158 err,
159 Error::UserInput(UserInputError::InvalidDateFormat(ref s)) if s == "foo"
160 ));
161 }
162
163 #[test]
164 fn user_input_macro_formatted() {
165 let err = user_input_error!(MissingArgument, "missing {}", "--format");
166 assert!(matches!(
167 err,
168 Error::UserInput(UserInputError::MissingArgument(ref s)) if s == "missing --format"
169 ));
170 }
171
172 #[test]
173 fn user_input_macro_empty() {
174 let err = user_input_error!(InvalidNow);
175 assert!(matches!(
176 err,
177 Error::UserInput(UserInputError::InvalidNow(ref s)) if s.is_empty()
178 ));
179 }
180
181 #[test]
182 fn system_error_macro_literal() {
183 let err = system_error!(Config, "invalid field");
184 assert!(matches!(
185 err,
186 Error::System(SystemError::Config(ref s)) if s == "invalid field"
187 ));
188 }
189
190 #[test]
191 fn system_error_macro_formatted() {
192 let err = system_error!(Config, "failed to read {}", "/tmp/foo");
193 assert!(matches!(
194 err,
195 Error::System(SystemError::Config(ref s)) if s == "failed to read /tmp/foo"
196 ));
197 }
198
199 #[test]
200 fn system_error_macro_empty() {
201 let err = system_error!(Config);
202 assert!(matches!(
203 err,
204 Error::System(SystemError::Config(ref s)) if s.is_empty()
205 ));
206 }
207
208 #[test]
209 fn unsupported_format_error() {
210 let err = user_input_error!(UnsupportedFormat, "bad format");
211 assert!(matches!(
212 err,
213 Error::UserInput(UserInputError::UnsupportedFormat(ref s)) if s == "bad format"
214 ));
215 }
216
217 #[test]
218 fn conversion_from_io_error() {
219 let err: Error = std::io::Error::from(std::io::ErrorKind::PermissionDenied).into();
220 assert!(matches!(err, Error::System(SystemError::Io(_))));
221 }
222
223 #[test]
224 fn system_error_partial_eq_config() {
225 let a = SystemError::Config("x".into());
226 let b = SystemError::Config("x".into());
227 let c = SystemError::Config("y".into());
228 assert_eq!(a, b);
229 assert_ne!(a, c);
230 }
231
232 #[test]
233 fn system_error_partial_eq_io() {
234 let a = SystemError::Io(std::io::Error::from(std::io::ErrorKind::NotFound));
235 let b = SystemError::Io(std::io::Error::from(std::io::ErrorKind::NotFound));
236 let c = SystemError::Io(std::io::Error::from(std::io::ErrorKind::PermissionDenied));
237 assert_eq!(a, b);
238 assert_ne!(a, c);
239 }
240
241 #[test]
242 fn system_error_partial_eq_different_variants() {
243 let a = SystemError::Config("x".into());
244 let b = SystemError::Io(std::io::Error::from(std::io::ErrorKind::NotFound));
245 assert_ne!(a, b);
246 }
247
248 #[test]
249 fn error_display_user_input() {
250 let err = user_input_error!(InvalidDateFormat, "bad date");
251 assert_eq!(format!("{err}"), "Invalid date format: bad date");
252 }
253
254 #[test]
255 fn error_display_system() {
256 let err = system_error!(Config, "broken");
257 assert_eq!(format!("{err}"), "Configuration error: broken");
258 }
259
260 #[test]
261 fn new_error_variants_display() {
262 let err = user_input_error!(InvalidDate, "bad");
263 assert_eq!(format!("{err}"), "Invalid date: bad");
264
265 let err = user_input_error!(AmbiguousDateTime, "ambiguous");
266 assert_eq!(format!("{err}"), "Ambiguous datetime: ambiguous");
267 }
268}