mielin_cli/
error.rs

1//! Error types and user-friendly error messages for MielinCTL
2
3use std::fmt;
4
5/// CLI-specific error types with user-friendly messages
6#[derive(Debug)]
7pub enum CliError {
8    /// Configuration error
9    Config(String),
10    /// Connection error
11    Connection(String),
12    /// File operation error
13    FileOperation(String),
14    /// Invalid input
15    InvalidInput(String),
16    /// Not found error
17    NotFound(String),
18    /// Permission denied
19    PermissionDenied(String),
20    /// Timeout error
21    Timeout(String),
22    /// Internal error
23    Internal(String),
24}
25
26impl fmt::Display for CliError {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            CliError::Config(msg) => write!(f, "Configuration error: {}", msg),
30            CliError::Connection(msg) => write!(f, "Connection error: {}", msg),
31            CliError::FileOperation(msg) => write!(f, "File operation error: {}", msg),
32            CliError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
33            CliError::NotFound(msg) => write!(f, "Not found: {}", msg),
34            CliError::PermissionDenied(msg) => write!(f, "Permission denied: {}", msg),
35            CliError::Timeout(msg) => write!(f, "Operation timed out: {}", msg),
36            CliError::Internal(msg) => write!(f, "Internal error: {}", msg),
37        }
38    }
39}
40
41impl std::error::Error for CliError {}
42
43/// Convert anyhow::Error to user-friendly message
44pub fn format_error(error: &anyhow::Error) -> String {
45    let error_str = error.to_string();
46
47    if error_str.contains("timed out") || error_str.contains("timeout") {
48        "Operation timed out. Try increasing the timeout with: \
49         mielinctl node config cli.command_timeout_secs <seconds>"
50            .to_string()
51    } else if error_str.contains("Connection refused") || error_str.contains("connect") {
52        "Could not connect to the MielinOS daemon. \
53         Make sure it's running with: mielinctl daemon"
54            .to_string()
55    } else if error_str.contains("No such file") || error_str.contains("not found") {
56        format!("File not found: {}", error_str)
57    } else if error_str.contains("Permission denied") {
58        "Permission denied. You may need elevated privileges to perform this operation.".to_string()
59    } else if error_str.contains("config") || error_str.contains("Config") {
60        format!(
61            "Configuration error: {}. \
62             Check your config with: mielinctl node config --all",
63            error_str
64        )
65    } else {
66        error_str
67    }
68}
69
70/// Error context extension trait for adding user-friendly context
71pub trait ErrorContext<T> {
72    fn context_user_friendly(self, msg: &str) -> anyhow::Result<T>;
73}
74
75impl<T, E> ErrorContext<T> for Result<T, E>
76where
77    E: std::error::Error + Send + Sync + 'static,
78{
79    fn context_user_friendly(self, msg: &str) -> anyhow::Result<T> {
80        self.map_err(|e| anyhow::anyhow!("{}: {}", msg, e))
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn test_cli_error_display() {
90        let error = CliError::Config("invalid format".to_string());
91        assert_eq!(error.to_string(), "Configuration error: invalid format");
92
93        let error = CliError::Connection("refused".to_string());
94        assert_eq!(error.to_string(), "Connection error: refused");
95
96        let error = CliError::Timeout("30s".to_string());
97        assert_eq!(error.to_string(), "Operation timed out: 30s");
98    }
99
100    #[test]
101    fn test_format_error_timeout() {
102        let error = anyhow::anyhow!("Operation timed out");
103        let formatted = format_error(&error);
104        assert!(formatted.contains("timed out"));
105        assert!(formatted.contains("command_timeout_secs"));
106    }
107
108    #[test]
109    fn test_format_error_connection() {
110        let error = anyhow::anyhow!("Connection refused");
111        let formatted = format_error(&error);
112        assert!(formatted.contains("connect"));
113        assert!(formatted.contains("daemon"));
114    }
115
116    #[test]
117    fn test_format_error_not_found() {
118        let error = anyhow::anyhow!("No such file or directory");
119        let formatted = format_error(&error);
120        assert!(formatted.contains("File not found"));
121    }
122}