Skip to main content

envswitch/
error.rs

1use thiserror::Error;
2
3
4#[derive(Debug, Error)]
5pub enum ConfigError {
6    #[error("Configuration '{0}' not found")]
7    ConfigNotFound(String),
8    
9    #[error("Configuration file error: {0}")]
10    FileError(#[from] std::io::Error),
11    
12    #[error("JSON parsing error: {0}")]
13    JsonError(#[from] serde_json::Error),
14    
15    #[error("Configuration '{0}' already exists")]
16    ConfigExists(String),
17    
18    #[error("Invalid configuration directory")]
19    InvalidConfigDir,
20    
21    #[error("Invalid configuration name: {0}")]
22    InvalidConfigName(String),
23    
24    #[error("Configuration validation failed: {0}")]
25    ValidationError(String),
26    
27    #[error("Permission denied: {0}")]
28    PermissionDenied(String),
29    
30    #[error("Environment variable error: {0}")]
31    EnvError(#[from] EnvError),
32}
33
34#[derive(Debug, Error)]
35pub enum EnvError {
36    #[error("Shell detection failed")]
37    ShellDetectionFailed,
38    
39    #[error("Environment variable setting failed: {0}")]
40    SetVariableFailed(String),
41    
42    #[error("Unsupported shell: {0}")]
43    UnsupportedShell(String),
44    
45    #[error("Invalid environment variable name: {0}")]
46    InvalidVariableName(String),
47    
48    #[error("Invalid environment variable value: {0}")]
49    InvalidVariableValue(String),
50    
51    #[error("Command generation failed: {0}")]
52    CommandGenerationFailed(String),
53}
54
55#[derive(Debug, Error)]
56pub enum AppError {
57    #[error("Configuration error: {0}")]
58    Config(#[from] ConfigError),
59    
60    #[error("Environment error: {0}")]
61    Environment(#[from] EnvError),
62    
63    #[error("CLI argument error: {0}")]
64    CliError(String),
65    
66    #[error("General error: {0}")]
67    General(String),
68}
69
70// Type aliases for convenience
71pub type ConfigResult<T> = Result<T, ConfigError>;
72pub type EnvResult<T> = Result<T, EnvError>;
73pub type AppResult<T> = Result<T, AppError>;
74
75impl ConfigError {
76    /// Provides user-friendly error messages with suggestions
77    pub fn user_message(&self) -> String {
78        match self {
79            ConfigError::ConfigNotFound(name) => {
80                format!("Configuration '{}' not found. Use 'envswitch list' to see available configurations.", name)
81            }
82            ConfigError::FileError(err) => {
83                format!("File operation failed: {}. Check file permissions and disk space.", err)
84            }
85            ConfigError::JsonError(err) => {
86                format!("Configuration file format error: {}. The file may be corrupted.", err)
87            }
88            ConfigError::ConfigExists(name) => {
89                format!("Configuration '{}' already exists. Use 'envswitch edit {}' to modify it.", name, name)
90            }
91            ConfigError::InvalidConfigDir => {
92                "Cannot access configuration directory. Check permissions for ~/.config/envswitch/".to_string()
93            }
94            ConfigError::InvalidConfigName(name) => {
95                format!("Invalid configuration name '{}'. Names must contain only letters, numbers, hyphens, and underscores.", name)
96            }
97            ConfigError::ValidationError(msg) => {
98                format!("Configuration validation failed: {}", msg)
99            }
100            ConfigError::PermissionDenied(path) => {
101                format!("Permission denied accessing '{}'. Check file permissions.", path)
102            }
103            ConfigError::EnvError(env_err) => {
104                format!("Environment variable error: {}", env_err.user_message())
105            }
106        }
107    }
108}
109
110impl EnvError {
111    /// Provides user-friendly error messages with suggestions
112    pub fn user_message(&self) -> String {
113        match self {
114            EnvError::ShellDetectionFailed => {
115                "Could not detect your shell. Please set the SHELL environment variable or use a supported shell (zsh, fish, bash).".to_string()
116            }
117            EnvError::SetVariableFailed(var) => {
118                format!("Failed to set environment variable '{}'. Check if the variable name is valid.", var)
119            }
120            EnvError::UnsupportedShell(shell) => {
121                format!("Shell '{}' is not fully supported. Falling back to generic export commands.", shell)
122            }
123            EnvError::InvalidVariableName(name) => {
124                format!("Invalid environment variable name '{}'. Names must start with a letter and contain only letters, numbers, and underscores.", name)
125            }
126            EnvError::InvalidVariableValue(value) => {
127                format!("Invalid environment variable value: {}", value)
128            }
129            EnvError::CommandGenerationFailed(msg) => {
130                format!("Failed to generate shell commands: {}", msg)
131            }
132        }
133    }
134}
135
136/// Validates environment variable names according to POSIX standards
137pub fn validate_env_var_name(name: &str) -> Result<(), EnvError> {
138    if name.is_empty() {
139        return Err(EnvError::InvalidVariableName("Name cannot be empty".to_string()));
140    }
141    
142    // Must start with letter or underscore
143    let first_char = name.chars().next().unwrap();
144    if !first_char.is_ascii_alphabetic() && first_char != '_' {
145        return Err(EnvError::InvalidVariableName(
146            format!("Name '{}' must start with a letter or underscore", name)
147        ));
148    }
149    
150    // Rest must be alphanumeric or underscore
151    for (i, c) in name.chars().enumerate() {
152        if !c.is_ascii_alphanumeric() && c != '_' {
153            return Err(EnvError::InvalidVariableName(
154                format!("Name '{}' contains invalid character '{}' at position {}", name, c, i)
155            ));
156        }
157    }
158    
159    Ok(())
160}
161
162/// Validates configuration alias names
163pub fn validate_config_name(name: &str) -> Result<(), ConfigError> {
164    if name.is_empty() {
165        return Err(ConfigError::InvalidConfigName("Name cannot be empty".to_string()));
166    }
167    
168    if name.len() > 50 {
169        return Err(ConfigError::InvalidConfigName("Name too long (max 50 characters)".to_string()));
170    }
171    
172    // Allow letters, numbers, hyphens, and underscores
173    for (i, c) in name.chars().enumerate() {
174        if !c.is_ascii_alphanumeric() && c != '-' && c != '_' {
175            return Err(ConfigError::InvalidConfigName(
176                format!("Name '{}' contains invalid character '{}' at position {}", name, c, i)
177            ));
178        }
179    }
180    
181    // Cannot start with hyphen
182    if name.starts_with('-') {
183        return Err(ConfigError::InvalidConfigName("Name cannot start with hyphen".to_string()));
184    }
185    
186    Ok(())
187}
188#[cfg
189(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_validate_env_var_name_valid() {
195        assert!(validate_env_var_name("VALID_NAME").is_ok());
196        assert!(validate_env_var_name("_VALID").is_ok());
197        assert!(validate_env_var_name("VAR123").is_ok());
198        assert!(validate_env_var_name("A").is_ok());
199    }
200
201    #[test]
202    fn test_validate_env_var_name_invalid() {
203        assert!(validate_env_var_name("").is_err());
204        assert!(validate_env_var_name("123INVALID").is_err());
205        assert!(validate_env_var_name("INVALID-NAME").is_err());
206        assert!(validate_env_var_name("INVALID.NAME").is_err());
207        assert!(validate_env_var_name("INVALID NAME").is_err());
208    }
209
210    #[test]
211    fn test_validate_config_name_valid() {
212        assert!(validate_config_name("valid-name").is_ok());
213        assert!(validate_config_name("valid_name").is_ok());
214        assert!(validate_config_name("ValidName123").is_ok());
215        assert!(validate_config_name("a").is_ok());
216    }
217
218    #[test]
219    fn test_validate_config_name_invalid() {
220        assert!(validate_config_name("").is_err());
221        assert!(validate_config_name("-invalid").is_err());
222        assert!(validate_config_name("invalid.name").is_err());
223        assert!(validate_config_name("invalid name").is_err());
224        assert!(validate_config_name(&"a".repeat(51)).is_err()); // Too long
225    }
226
227    #[test]
228    fn test_error_user_messages() {
229        let config_error = ConfigError::ConfigNotFound("test".to_string());
230        let message = config_error.user_message();
231        assert!(message.contains("test"));
232        assert!(message.contains("envswitch list"));
233
234        let env_error = EnvError::ShellDetectionFailed;
235        let message = env_error.user_message();
236        assert!(message.contains("shell"));
237        assert!(message.contains("SHELL"));
238    }
239}