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
70pub type ConfigResult<T> = Result<T, ConfigError>;
72pub type EnvResult<T> = Result<T, EnvError>;
73pub type AppResult<T> = Result<T, AppError>;
74
75impl ConfigError {
76 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 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
136pub 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 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 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
162pub 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 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 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()); }
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}