Skip to main content

fast_yaml_cli/
error.rs

1use std::path::PathBuf;
2use thiserror::Error;
3
4/// Exit codes for CLI application
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6// Not all exit codes are currently used, but they form a complete set for CLI error handling
7#[allow(dead_code)]
8pub enum ExitCode {
9    /// Operation completed successfully
10    Success = 0,
11    /// YAML parsing failed
12    ParseError = 1,
13    /// Linter found errors
14    LintErrors = 2,
15    /// I/O operation failed
16    IoError = 3,
17    /// Invalid command-line arguments
18    InvalidArgs = 4,
19}
20
21/// Errors that can occur during file discovery.
22#[derive(Debug, Error)]
23pub enum DiscoveryError {
24    /// Invalid globset pattern (from include/exclude patterns)
25    #[error("invalid glob pattern '{pattern}': {source}")]
26    InvalidPattern {
27        /// The pattern that was invalid
28        pattern: String,
29        /// The underlying error
30        #[source]
31        source: globset::Error,
32    },
33
34    /// IO error during directory traversal
35    #[error("failed to read '{path}': {source}")]
36    IoError {
37        /// The path that caused the error
38        path: PathBuf,
39        /// The underlying IO error
40        #[source]
41        source: std::io::Error,
42    },
43
44    /// Permission denied
45    #[error("permission denied: '{path}'")]
46    PermissionDenied {
47        /// The path where permission was denied
48        path: PathBuf,
49    },
50
51    /// Broken symbolic link
52    #[error("broken symbolic link: '{path}'")]
53    BrokenSymlink {
54        /// The path to the broken symlink
55        path: PathBuf,
56    },
57
58    /// Path does not exist
59    #[error("path does not exist: '{path}'")]
60    PathNotFound {
61        /// The path that was not found
62        path: PathBuf,
63    },
64
65    /// Error reading from stdin
66    #[error("failed to read file list from stdin: {source}")]
67    StdinError {
68        /// The underlying IO error
69        #[source]
70        source: std::io::Error,
71    },
72
73    /// Too many paths provided
74    #[error("exceeded maximum of {max} paths")]
75    TooManyPaths {
76        /// The maximum allowed
77        max: usize,
78    },
79}
80
81impl ExitCode {
82    /// Converts exit code to i32 for use with `std::process::exit`
83    pub const fn as_i32(self) -> i32 {
84        self as i32
85    }
86}
87
88/// Format error with colored output (if enabled)
89pub fn format_error(err: &anyhow::Error, use_color: bool) -> String {
90    use std::fmt::Write;
91    let mut output = String::new();
92
93    #[cfg(feature = "colors")]
94    if use_color {
95        use colored::Colorize;
96        let _ = writeln!(output, "{} {}", "error:".red().bold(), err);
97
98        // Show error chain
99        for (i, cause) in err.chain().skip(1).enumerate() {
100            let _ = writeln!(
101                output,
102                "  {}{} {}",
103                "caused by".dimmed(),
104                format!("[{i}]").dimmed(),
105                cause.to_string().dimmed()
106            );
107        }
108        return output;
109    }
110
111    // Fallback for no-color or when colors feature is disabled
112    let _ = writeln!(output, "error: {err}");
113
114    for (i, cause) in err.chain().skip(1).enumerate() {
115        let _ = writeln!(output, "  caused by[{i}] {cause}");
116    }
117
118    output
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_exit_code_values() {
127        assert_eq!(ExitCode::Success.as_i32(), 0);
128        assert_eq!(ExitCode::ParseError.as_i32(), 1);
129        assert_eq!(ExitCode::LintErrors.as_i32(), 2);
130        assert_eq!(ExitCode::IoError.as_i32(), 3);
131        assert_eq!(ExitCode::InvalidArgs.as_i32(), 4);
132    }
133
134    #[test]
135    fn test_format_error_no_color() {
136        let err = anyhow::anyhow!("test error");
137        let formatted = format_error(&err, false);
138        assert!(formatted.contains("error: test error"));
139    }
140
141    #[test]
142    fn test_format_error_with_chain() {
143        use anyhow::Context;
144        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
145        // Context trait applies to Result, not Error directly
146        let err: anyhow::Error = Err::<(), _>(io_err)
147            .context("Failed to read config")
148            .unwrap_err();
149        let formatted = format_error(&err, false);
150        assert!(formatted.contains("Failed to read config"));
151        assert!(formatted.contains("caused by"));
152    }
153}