1use std::fmt;
7
8#[derive(Debug)]
13pub enum Error {
14 CommandNotFound {
18 command: String,
20 suggestions: Vec<String>,
22 },
23
24 SubcommandRequired(String),
29
30 NoRunFunction(String),
35
36 FlagParsing {
41 message: String,
43 flag: Option<String>,
45 suggestions: Vec<String>,
47 },
48
49 ArgumentParsing(String),
54
55 ArgumentValidation {
59 message: String,
61 expected: String,
63 received: usize,
65 },
66
67 Validation(String),
72
73 Completion(String),
78
79 Io(std::io::Error),
83
84 Custom(Box<dyn std::error::Error + Send + Sync>),
88}
89
90impl fmt::Display for Error {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 use crate::color;
93
94 match self {
95 Self::CommandNotFound {
96 command,
97 suggestions,
98 } => {
99 write!(f, "{}: unknown command ", color::red("Error"))?;
100 write!(f, "{}", color::bold(command))?;
101
102 if !suggestions.is_empty() {
103 write!(f, "\n\n")?;
104 if suggestions.len() == 1 {
105 writeln!(f, "{}?", color::yellow("Did you mean this"))?;
106 write!(f, " {}", color::green(&suggestions[0]))?;
107 } else {
108 writeln!(f, "{}?", color::yellow("Did you mean one of these"))?;
109 for suggestion in suggestions {
110 writeln!(f, " {}", color::green(suggestion))?;
111 }
112 }
113 }
114 Ok(())
115 }
116 Self::SubcommandRequired(cmd) => {
117 write!(f, "{}: ", color::red("Error"))?;
118 write!(f, "'{}' requires a subcommand", color::bold(cmd))?;
119 write!(
120 f,
121 "\n\n{}: use '{} --help' for available subcommands",
122 color::yellow("Hint"),
123 cmd
124 )
125 }
126 Self::NoRunFunction(cmd) => {
127 write!(
128 f,
129 "{}: command '{}' is not runnable",
130 color::red("Error"),
131 color::bold(cmd)
132 )
133 }
134 Self::FlagParsing {
135 message,
136 flag,
137 suggestions,
138 } => {
139 write!(f, "{}: {}", color::red("Error"), message)?;
140 if let Some(flag_name) = flag {
141 write!(f, " for flag '{}'", color::bold(flag_name))?;
142 }
143
144 if !suggestions.is_empty() {
145 write!(f, "\n\n")?;
146 if suggestions.len() == 1 {
147 write!(f, "{}: {}", color::yellow("Expected"), suggestions[0])?;
148 } else {
149 writeln!(f, "{} one of:", color::yellow("Expected"))?;
150 for suggestion in suggestions {
151 writeln!(f, " {}", color::green(suggestion))?;
152 }
153 }
154 }
155 Ok(())
156 }
157 Self::ArgumentParsing(msg) => {
158 write!(f, "{}: invalid argument - {}", color::red("Error"), msg)
159 }
160 Self::ArgumentValidation {
161 message,
162 expected,
163 received,
164 } => {
165 write!(f, "{}: {}", color::red("Error"), message)?;
166 write!(f, "\n\n{}: {}", color::yellow("Expected"), expected)?;
167 write!(
168 f,
169 "\n{}: {} argument{}",
170 color::yellow("Received"),
171 received,
172 if *received == 1 { "" } else { "s" }
173 )
174 }
175 Self::Validation(msg) => {
176 write!(f, "{}: {}", color::red("Validation error"), msg)
177 }
178 Self::Completion(msg) => {
179 write!(f, "{}: {}", color::red("Completion error"), msg)
180 }
181 Self::Io(err) => {
182 write!(f, "{}: {}", color::red("IO error"), err)
183 }
184 Self::Custom(err) => {
185 write!(f, "{}: {}", color::red("Error"), err)
186 }
187 }
188 }
189}
190
191impl std::error::Error for Error {
192 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
193 match self {
194 Self::Io(err) => Some(err),
195 Self::Custom(err) => Some(err.as_ref()),
196 _ => None,
197 }
198 }
199}
200
201impl From<std::io::Error> for Error {
202 fn from(err: std::io::Error) -> Self {
203 Self::Io(err)
204 }
205}
206
207pub type Result<T> = std::result::Result<T, Error>;
222
223impl Error {
224 pub fn flag_parsing(message: impl Into<String>) -> Self {
226 Self::FlagParsing {
227 message: message.into(),
228 flag: None,
229 suggestions: vec![],
230 }
231 }
232
233 pub fn flag_parsing_with_suggestions(
235 message: impl Into<String>,
236 flag: impl Into<String>,
237 suggestions: Vec<String>,
238 ) -> Self {
239 Self::FlagParsing {
240 message: message.into(),
241 flag: Some(flag.into()),
242 suggestions,
243 }
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use std::error::Error as StdError;
251
252 #[test]
253 fn test_error_display() {
254 unsafe { std::env::set_var("NO_COLOR", "1") };
256
257 assert_eq!(
258 Error::CommandNotFound {
259 command: "test".to_string(),
260 suggestions: vec![],
261 }
262 .to_string(),
263 "Error: unknown command test"
264 );
265 assert_eq!(
266 Error::SubcommandRequired("kubectl".to_string()).to_string(),
267 "Error: 'kubectl' requires a subcommand\n\nHint: use 'kubectl --help' for available subcommands"
268 );
269 assert_eq!(
270 Error::FlagParsing {
271 message: "invalid flag".to_string(),
272 flag: Some("invalid".to_string()),
273 suggestions: vec![],
274 }
275 .to_string(),
276 "Error: invalid flag for flag 'invalid'"
277 );
278
279 unsafe { std::env::remove_var("NO_COLOR") };
280 }
281
282 #[test]
283 fn test_error_source() {
284 let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
285 let error = Error::Io(io_error);
286 assert!(error.source().is_some());
287
288 let error = Error::CommandNotFound {
289 command: "test".to_string(),
290 suggestions: vec![],
291 };
292 assert!(error.source().is_none());
293 }
294
295 #[test]
296 fn test_error_with_suggestions() {
297 unsafe { std::env::set_var("NO_COLOR", "1") };
299
300 let error = Error::CommandNotFound {
302 command: "strt".to_string(),
303 suggestions: vec!["start".to_string()],
304 };
305 assert_eq!(
306 error.to_string(),
307 "Error: unknown command strt\n\nDid you mean this?\n start"
308 );
309
310 let error = Error::CommandNotFound {
312 command: "lst".to_string(),
313 suggestions: vec!["list".to_string(), "last".to_string()],
314 };
315 assert_eq!(
316 error.to_string(),
317 "Error: unknown command lst\n\nDid you mean one of these?\n list\n last\n"
318 );
319
320 unsafe { std::env::remove_var("NO_COLOR") };
321 }
322}