Skip to main content

vkteams_bot_cli/
errors.rs

1use std::fmt;
2use vkteams_bot::error::BotError;
3
4// Static error message prefixes to avoid repeated allocations
5pub static FILE_NOT_FOUND: &str = "File not found: ";
6pub static DIR_NOT_FOUND: &str = "Directory not found: ";
7pub static NOT_A_FILE: &str = "Path is not a file: ";
8pub static NOT_A_DIR: &str = "Path is not a directory: ";
9pub static WRITE_ERROR: &str = "Failed to write file: ";
10pub static READ_ERROR: &str = "Failed to read file: ";
11pub static DOWNLOAD_ERROR: &str = "Failed to download file: ";
12pub static API_ERROR: &str = "API Error: ";
13pub static INPUT_ERROR: &str = "Input Error: ";
14pub static UNEXPECTED_ERROR: &str = "Unexpected Error: ";
15
16/// Enum representing different types of CLI errors
17#[derive(Debug)]
18pub enum CliError {
19    /// API error from the underlying vkteams-bot crate
20    ApiError(BotError),
21    /// Error that occurs when file operations fail
22    FileError(String),
23    /// Error that occurs with invalid CLI arguments
24    InputError(String),
25    /// Unexpected or general error
26    UnexpectedError(String),
27    /// Storage related errors
28    Storage(String),
29    /// Configuration related errors
30    Config(String),
31    /// Daemon related errors
32    DaemonAlreadyRunning,
33    /// Daemon not running error
34    DaemonNotRunning,
35    /// System related errors
36    System(String),
37}
38
39/// Result type for CLI operations
40pub type Result<T> = std::result::Result<T, CliError>;
41
42impl From<BotError> for CliError {
43    fn from(error: BotError) -> Self {
44        CliError::ApiError(error)
45    }
46}
47
48impl From<std::io::Error> for CliError {
49    fn from(error: std::io::Error) -> Self {
50        CliError::FileError(error.to_string())
51    }
52}
53
54impl From<serde_json::Error> for CliError {
55    fn from(error: serde_json::Error) -> Self {
56        CliError::UnexpectedError(format!("JSON error: {error}"))
57    }
58}
59
60impl fmt::Display for CliError {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        match self {
63            CliError::ApiError(err) => write!(f, "{API_ERROR}{err}"),
64            CliError::FileError(err) => write!(f, "File Error: {err}"),
65            CliError::InputError(err) => write!(f, "{INPUT_ERROR}{err}"),
66            CliError::UnexpectedError(err) => write!(f, "{UNEXPECTED_ERROR}{err}"),
67            CliError::Storage(err) => write!(f, "Storage Error: {err}"),
68            CliError::Config(err) => write!(f, "Configuration Error: {err}"),
69            CliError::DaemonAlreadyRunning => write!(f, "Daemon is already running"),
70            CliError::DaemonNotRunning => write!(f, "Daemon is not running"),
71            CliError::System(err) => write!(f, "System Error: {err}"),
72        }
73    }
74}
75
76impl std::error::Error for CliError {}
77
78impl CliError {
79    /// Returns the appropriate exit code for this error
80    #[must_use]
81    pub fn exit_code(&self) -> i32 {
82        match self {
83            CliError::ApiError(_) => exitcode::UNAVAILABLE,
84            CliError::FileError(_) => exitcode::IOERR,
85            CliError::InputError(_) => exitcode::USAGE,
86            CliError::UnexpectedError(_) => exitcode::SOFTWARE,
87            CliError::Storage(_) => exitcode::IOERR,
88            CliError::Config(_) => exitcode::CONFIG,
89            CliError::DaemonAlreadyRunning => exitcode::TEMPFAIL,
90            CliError::DaemonNotRunning => exitcode::TEMPFAIL,
91            CliError::System(_) => exitcode::OSERR,
92        }
93    }
94
95    // TODO: Enable this method when we need direct error exit functionality
96    // /// Prints the error message and exits with appropriate code
97    // pub fn exit_with_error(self) -> ! {
98    //     // Avoid unnecessary string allocation for commonly used error types
99    //     let error_message = match &self {
100    //         CliError::ApiError(err) => format!("{API_ERROR}{err}"),
101    //         CliError::FileError(msg) => format!("File Error: {msg}"),
102    //         CliError::InputError(msg) => format!("{INPUT_ERROR}{msg}"),
103    //         CliError::UnexpectedError(msg) => format!("{UNEXPECTED_ERROR}{msg}"),
104    //     };
105    //
106    //     eprintln!("{}", error_message.red());
107    //     exit(self.exit_code());
108    // }
109}
110
111/// A module to re-export all error types and constants
112pub mod prelude {
113    pub use super::{
114        API_ERROR, DIR_NOT_FOUND, DOWNLOAD_ERROR, FILE_NOT_FOUND, INPUT_ERROR, NOT_A_DIR,
115        NOT_A_FILE, READ_ERROR, UNEXPECTED_ERROR, WRITE_ERROR,
116    };
117    pub use super::{CliError, Result};
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use serde_json;
124    use std::io;
125    use vkteams_bot::error::BotError;
126
127    #[test]
128    fn test_cli_error_display_and_exit_code() {
129        let api_err = CliError::ApiError(BotError::Api(vkteams_bot::error::ApiError {
130            description: "api fail".to_string(),
131        }));
132        assert!(format!("{api_err}").contains("API Error:"));
133        assert_eq!(api_err.exit_code(), exitcode::UNAVAILABLE);
134
135        let file_err = CliError::FileError("file fail".to_string());
136        assert!(format!("{file_err}").contains("File Error:"));
137        assert_eq!(file_err.exit_code(), exitcode::IOERR);
138
139        let input_err = CliError::InputError("bad arg".to_string());
140        assert!(format!("{input_err}").contains("Input Error:"));
141        assert_eq!(input_err.exit_code(), exitcode::USAGE);
142
143        let unexp_err = CliError::UnexpectedError("boom".to_string());
144        assert!(format!("{unexp_err}").contains("Unexpected Error:"));
145        assert_eq!(unexp_err.exit_code(), exitcode::SOFTWARE);
146    }
147
148    #[test]
149    fn test_from_bot_error() {
150        let bot_err = BotError::Api(vkteams_bot::error::ApiError {
151            description: "api fail".to_string(),
152        });
153        let cli_err: CliError = bot_err.into();
154        match cli_err {
155            CliError::ApiError(_) => {}
156            _ => panic!("Expected ApiError variant"),
157        }
158    }
159
160    #[test]
161    fn test_from_io_error() {
162        let io_err = io::Error::other("io fail");
163        let cli_err: CliError = io_err.into();
164        match cli_err {
165            CliError::FileError(msg) => assert!(msg.contains("io fail")),
166            _ => panic!("Expected FileError variant"),
167        }
168    }
169
170    #[test]
171    fn test_from_serde_json_error() {
172        let json_err = serde_json::from_str::<u32>("not a number").unwrap_err();
173        let cli_err: CliError = json_err.into();
174        match cli_err {
175            CliError::UnexpectedError(msg) => assert!(msg.contains("JSON error")),
176            _ => panic!("Expected UnexpectedError variant"),
177        }
178    }
179}