Skip to main content

flag_rs/
error.rs

1//! Error types for the flag framework
2//!
3//! This module defines the error types that can occur when parsing commands,
4//! flags, and arguments, or when executing command handlers.
5
6use std::fmt;
7
8/// The main error type for the flag framework
9///
10/// This enum represents all possible errors that can occur during command
11/// parsing, validation, and execution.
12#[derive(Debug)]
13pub enum Error {
14    /// The specified command was not found
15    ///
16    /// This error occurs when a user tries to run a subcommand that doesn't exist.
17    CommandNotFound {
18        /// The name of the unknown command
19        command: String,
20        /// Suggested similar commands
21        suggestions: Vec<String>,
22    },
23
24    /// A command requires a subcommand but none was provided
25    ///
26    /// This error occurs when a parent command has no run function and the user
27    /// doesn't specify which subcommand to run. Contains the parent command name.
28    SubcommandRequired(String),
29
30    /// A command has no run function defined
31    ///
32    /// This error occurs when trying to execute a command that has no run handler.
33    /// Contains the command name.
34    NoRunFunction(String),
35
36    /// An error occurred while parsing flag values
37    ///
38    /// This error occurs when a flag value cannot be parsed as the expected type
39    /// (e.g., "abc" for an integer flag).
40    FlagParsing {
41        /// Description of the error
42        message: String,
43        /// The flag that caused the error
44        flag: Option<String>,
45        /// Suggested valid values or format
46        suggestions: Vec<String>,
47    },
48
49    /// An error occurred while parsing command arguments
50    ///
51    /// This error occurs when command arguments don't meet requirements.
52    /// Contains a description of the error.
53    ArgumentParsing(String),
54
55    /// Argument validation failed
56    ///
57    /// This error occurs when arguments don't meet validation constraints.
58    ArgumentValidation {
59        /// Description of the validation failure
60        message: String,
61        /// Expected argument pattern
62        expected: String,
63        /// Number of arguments received
64        received: usize,
65    },
66
67    /// A validation error occurred
68    ///
69    /// This error occurs when custom validation logic fails.
70    /// Contains a description of the validation failure.
71    Validation(String),
72
73    /// An error occurred during shell completion
74    ///
75    /// This error occurs when completion functions fail.
76    /// Contains a description of the error.
77    Completion(String),
78
79    /// An I/O error occurred
80    ///
81    /// Wraps standard I/O errors that may occur during command execution.
82    Io(std::io::Error),
83
84    /// A custom error from user code
85    ///
86    /// Allows command handlers to return their own error types.
87    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
207/// Type alias for Results with the flag Error type
208///
209/// This is a convenience type alias for `std::result::Result<T, flag::Error>`.
210///
211/// # Examples
212///
213/// ```
214/// use flag_rs::error::{Error, Result};
215///
216/// fn parse_count(s: &str) -> Result<u32> {
217///     s.parse()
218///         .map_err(|_| Error::ArgumentParsing(format!("invalid count: {}", s)))
219/// }
220/// ```
221pub type Result<T> = std::result::Result<T, Error>;
222
223impl Error {
224    /// Create a simple flag parsing error
225    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    /// Create a flag parsing error with suggestions
234    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        // Test without color for predictable test output
255        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        // Test without color for predictable test output
298        unsafe { std::env::set_var("NO_COLOR", "1") };
299
300        // Single suggestion
301        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        // Multiple suggestions
311        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}