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 if f.width().is_none() {
114 }
117 }
118 }
119 Ok(())
120 }
121 Self::SubcommandRequired(cmd) => {
122 write!(f, "{}: ", color::red("Error"))?;
123 write!(f, "'{}' requires a subcommand", color::bold(cmd))?;
124 write!(
125 f,
126 "\n\n{}: use '{} --help' for available subcommands",
127 color::yellow("Hint"),
128 cmd
129 )
130 }
131 Self::NoRunFunction(cmd) => {
132 write!(
133 f,
134 "{}: command '{}' is not runnable",
135 color::red("Error"),
136 color::bold(cmd)
137 )
138 }
139 Self::FlagParsing {
140 message,
141 flag,
142 suggestions,
143 } => {
144 write!(f, "{}: {}", color::red("Error"), message)?;
145 if let Some(flag_name) = flag {
146 write!(f, " for flag '{}'", color::bold(flag_name))?;
147 }
148
149 if !suggestions.is_empty() {
150 write!(f, "\n\n")?;
151 if suggestions.len() == 1 {
152 write!(f, "{}: {}", color::yellow("Expected"), suggestions[0])?;
153 } else {
154 writeln!(f, "{} one of:", color::yellow("Expected"))?;
155 for suggestion in suggestions {
156 writeln!(f, " {}", color::green(suggestion))?;
157 }
158 if f.width().is_none() {
160 }
162 }
163 }
164 Ok(())
165 }
166 Self::ArgumentParsing(msg) => {
167 write!(f, "{}: invalid argument - {}", color::red("Error"), msg)
168 }
169 Self::ArgumentValidation {
170 message,
171 expected,
172 received,
173 } => {
174 write!(f, "{}: {}", color::red("Error"), message)?;
175 write!(f, "\n\n{}: {}", color::yellow("Expected"), expected)?;
176 write!(
177 f,
178 "\n{}: {} argument{}",
179 color::yellow("Received"),
180 received,
181 if *received == 1 { "" } else { "s" }
182 )
183 }
184 Self::Validation(msg) => {
185 write!(f, "{}: {}", color::red("Validation error"), msg)
186 }
187 Self::Completion(msg) => {
188 write!(f, "{}: {}", color::red("Completion error"), msg)
189 }
190 Self::Io(err) => {
191 write!(f, "{}: {}", color::red("IO error"), err)
192 }
193 Self::Custom(err) => {
194 write!(f, "{}: {}", color::red("Error"), err)
195 }
196 }
197 }
198}
199
200impl std::error::Error for Error {
201 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
202 match self {
203 Self::Io(err) => Some(err),
204 Self::Custom(err) => Some(err.as_ref()),
205 _ => None,
206 }
207 }
208}
209
210impl From<std::io::Error> for Error {
211 fn from(err: std::io::Error) -> Self {
212 Self::Io(err)
213 }
214}
215
216pub type Result<T> = std::result::Result<T, Error>;
231
232impl Error {
233 pub fn flag_parsing(message: impl Into<String>) -> Self {
235 Self::FlagParsing {
236 message: message.into(),
237 flag: None,
238 suggestions: vec![],
239 }
240 }
241
242 pub fn flag_parsing_with_suggestions(
244 message: impl Into<String>,
245 flag: impl Into<String>,
246 suggestions: Vec<String>,
247 ) -> Self {
248 Self::FlagParsing {
249 message: message.into(),
250 flag: Some(flag.into()),
251 suggestions,
252 }
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use std::error::Error as StdError;
260
261 #[test]
262 fn test_error_display() {
263 unsafe { std::env::set_var("NO_COLOR", "1") };
265
266 assert_eq!(
267 Error::CommandNotFound {
268 command: "test".to_string(),
269 suggestions: vec![],
270 }
271 .to_string(),
272 "Error: unknown command test"
273 );
274 assert_eq!(
275 Error::SubcommandRequired("kubectl".to_string()).to_string(),
276 "Error: 'kubectl' requires a subcommand\n\nHint: use 'kubectl --help' for available subcommands"
277 );
278 assert_eq!(
279 Error::FlagParsing {
280 message: "invalid flag".to_string(),
281 flag: Some("invalid".to_string()),
282 suggestions: vec![],
283 }
284 .to_string(),
285 "Error: invalid flag for flag 'invalid'"
286 );
287
288 unsafe { std::env::remove_var("NO_COLOR") };
289 }
290
291 #[test]
292 fn test_error_source() {
293 let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
294 let error = Error::Io(io_error);
295 assert!(error.source().is_some());
296
297 let error = Error::CommandNotFound {
298 command: "test".to_string(),
299 suggestions: vec![],
300 };
301 assert!(error.source().is_none());
302 }
303
304 #[test]
305 fn test_error_with_suggestions() {
306 unsafe { std::env::set_var("NO_COLOR", "1") };
308
309 let error = Error::CommandNotFound {
311 command: "strt".to_string(),
312 suggestions: vec!["start".to_string()],
313 };
314 assert_eq!(
315 error.to_string(),
316 "Error: unknown command strt\n\nDid you mean this?\n start"
317 );
318
319 let error = Error::CommandNotFound {
321 command: "lst".to_string(),
322 suggestions: vec!["list".to_string(), "last".to_string()],
323 };
324 assert_eq!(
325 error.to_string(),
326 "Error: unknown command lst\n\nDid you mean one of these?\n list\n last\n"
327 );
328
329 unsafe { std::env::remove_var("NO_COLOR") };
330 }
331}