winbrew_cli\commands/
error.rs1use std::process::ExitCode;
2use thiserror::Error;
3
4#[derive(Debug, Error)]
5pub enum CommandError {
6 #[error("{message}")]
7 Reported {
8 message: String,
9 exit_code: i32,
10 hint: Option<String>,
11 #[source]
12 source: Option<anyhow::Error>,
13 },
14
15 #[error("operation cancelled")]
16 Cancelled,
17
18 #[error("{0}")]
19 Fatal(String),
20}
21
22pub fn reported(message: impl Into<String>) -> anyhow::Error {
23 CommandError::reported(message).into()
24}
25
26pub fn reported_with_hint(message: impl Into<String>, hint: impl Into<String>) -> anyhow::Error {
27 CommandError::reported(message).with_hint(hint).into()
28}
29
30pub fn reported_with_source(
31 message: impl Into<String>,
32 source: Option<anyhow::Error>,
33) -> anyhow::Error {
34 CommandError::reported(message).with_source(source).into()
35}
36
37pub fn cancelled() -> anyhow::Error {
38 CommandError::cancelled().into()
39}
40
41pub fn fatal(message: impl Into<String>) -> anyhow::Error {
42 CommandError::fatal(message).into()
43}
44
45pub fn is_handled(err: &anyhow::Error) -> bool {
46 err.downcast_ref::<CommandError>().is_some()
47}
48
49impl CommandError {
50 pub fn reported(message: impl Into<String>) -> Self {
51 Self::Reported {
52 message: message.into(),
53 exit_code: 1,
54 hint: None,
55 source: None,
56 }
57 }
58
59 pub fn cancelled() -> Self {
60 Self::Cancelled
61 }
62
63 pub fn fatal(message: impl Into<String>) -> Self {
64 Self::Fatal(message.into())
65 }
66
67 #[must_use = "builder method returns a modified error and should be used"]
68 pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
69 if let Self::Reported {
70 hint: current_hint, ..
71 } = &mut self
72 {
73 *current_hint = Some(hint.into());
74 }
75
76 self
77 }
78
79 #[must_use = "builder method returns a modified error and should be used"]
80 pub fn with_exit_code(mut self, exit_code: i32) -> Self {
81 if let Self::Reported {
82 exit_code: current_exit_code,
83 ..
84 } = &mut self
85 {
86 *current_exit_code = exit_code;
87 }
88
89 self
90 }
91
92 #[must_use = "builder method returns a modified error and should be used"]
93 pub fn with_source(mut self, source: Option<anyhow::Error>) -> Self {
94 if let Self::Reported {
95 source: current_source,
96 ..
97 } = &mut self
98 {
99 *current_source = source;
100 }
101
102 self
103 }
104
105 pub fn as_reported(&self) -> Option<(&str, i32, Option<&str>)> {
106 match self {
107 Self::Reported {
108 message,
109 exit_code,
110 hint,
111 ..
112 } => Some((message.as_str(), *exit_code, hint.as_deref())),
113 _ => None,
114 }
115 }
116
117 pub fn exit_code(&self) -> i32 {
118 match self {
119 Self::Reported { exit_code, .. } => *exit_code,
120 Self::Cancelled => 130,
121 Self::Fatal(_) => 1,
122 }
123 }
124
125 pub fn is_fatal(&self) -> bool {
126 matches!(self, Self::Fatal(_))
127 }
128
129 pub fn hint(&self) -> Option<&str> {
130 self.as_reported().and_then(|(_, _, hint)| hint)
131 }
132}
133
134impl PartialEq for CommandError {
135 fn eq(&self, other: &Self) -> bool {
136 match (self, other) {
137 (
138 Self::Reported {
139 message: left_message,
140 exit_code: left_exit_code,
141 hint: left_hint,
142 ..
143 },
144 Self::Reported {
145 message: right_message,
146 exit_code: right_exit_code,
147 hint: right_hint,
148 ..
149 },
150 ) => {
151 left_message == right_message
152 && left_exit_code == right_exit_code
153 && left_hint == right_hint
154 }
155 (Self::Cancelled, Self::Cancelled) => true,
156 (Self::Fatal(left), Self::Fatal(right)) => left == right,
157 _ => false,
158 }
159 }
160}
161
162impl Eq for CommandError {}
163
164impl From<&CommandError> for ExitCode {
165 fn from(err: &CommandError) -> Self {
166 ExitCode::from(err.exit_code().clamp(0, 255) as u8)
167 }
168}
169
170impl From<CommandError> for ExitCode {
171 fn from(err: CommandError) -> Self {
172 ExitCode::from(&err)
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::{
179 CommandError, cancelled, fatal, is_handled, reported, reported_with_hint,
180 reported_with_source,
181 };
182 use std::process::ExitCode;
183
184 #[test]
185 fn reported_errors_are_handled_and_default_to_exit_one() {
186 let err = reported("already shown");
187
188 let cmd_err = err.downcast_ref::<CommandError>().expect("command error");
189 assert_eq!(cmd_err, &CommandError::reported("already shown"));
190 assert_eq!(cmd_err.exit_code(), 1);
191 assert!(is_handled(&err));
192 }
193
194 #[test]
195 fn reported_errors_can_carry_hints() {
196 let err = reported_with_hint("already shown", "try again later");
197
198 let cmd_err = err.downcast_ref::<CommandError>().expect("command error");
199 assert_eq!(
200 cmd_err,
201 &CommandError::reported("already shown").with_hint("try again later")
202 );
203 assert_eq!(cmd_err.hint(), Some("try again later"));
204 }
205
206 #[test]
207 fn reported_errors_can_carry_sources() {
208 let source = std::io::Error::other("disk full");
209 let err = reported_with_source("already shown", Some(source.into()));
210
211 let cmd_err = err.downcast_ref::<CommandError>().expect("command error");
212 assert!(std::error::Error::source(cmd_err).is_some());
213 }
214
215 #[test]
216 fn reported_builder_can_customize_hint_and_exit_code() {
217 let err = CommandError::reported("already shown")
218 .with_hint("try again later")
219 .with_exit_code(2);
220
221 assert_eq!(
222 err.as_reported(),
223 Some(("already shown", 2, Some("try again later")))
224 );
225 }
226
227 #[test]
228 fn cancelled_errors_exit_with_130() {
229 let err = cancelled();
230
231 let cmd_err = err.downcast_ref::<CommandError>().expect("command error");
232 assert_eq!(cmd_err, &CommandError::Cancelled);
233 assert_eq!(cmd_err.exit_code(), 130);
234 }
235
236 #[test]
237 fn fatal_errors_exit_with_one() {
238 let err = fatal("boom");
239
240 let cmd_err = err.downcast_ref::<CommandError>().expect("command error");
241 assert_eq!(cmd_err, &CommandError::Fatal("boom".to_string()));
242 assert!(cmd_err.is_fatal());
243 assert_eq!(cmd_err.exit_code(), 1);
244 }
245
246 #[test]
247 fn command_error_converts_to_exit_code() {
248 let reported = CommandError::reported("already shown").with_exit_code(2);
249 let cancelled = CommandError::Cancelled;
250 let fatal = CommandError::Fatal("boom".to_string());
251
252 assert_eq!(ExitCode::from(&reported), ExitCode::from(2));
253 assert_eq!(ExitCode::from(&cancelled), ExitCode::from(130));
254 assert_eq!(ExitCode::from(&fatal), ExitCode::from(1));
255 }
256}