ricecoder_ide/
config.rs

1//! Configuration management for IDE integration
2
3use crate::error::{IdeError, IdeResult};
4use crate::types::IdeIntegrationConfig;
5use ricecoder_storage::PathResolver;
6use std::path::PathBuf;
7use tracing::{debug, info};
8
9/// Configuration manager for IDE integration
10pub struct ConfigManager;
11
12impl Default for ConfigManager {
13    fn default() -> Self {
14        Self::new()
15    }
16}
17
18impl ConfigManager {
19    /// Create a new configuration manager
20    pub fn new() -> Self {
21        ConfigManager
22    }
23
24    /// Load configuration from a YAML file
25    pub async fn load_from_yaml_file(file_path: &str) -> IdeResult<IdeIntegrationConfig> {
26        debug!("Loading IDE configuration from YAML: {}", file_path);
27
28        // Expand home directory if needed
29        let resolved_path = PathResolver::expand_home(&PathBuf::from(file_path))
30            .map_err(|e| IdeError::path_resolution_error(format!("Failed to resolve path: {}", e)))?;
31
32        // Read the file
33        let content = tokio::fs::read_to_string(&resolved_path)
34            .await
35            .map_err(|e| {
36                IdeError::config_error(format!(
37                    "Failed to read configuration file '{}': {}",
38                    resolved_path.display(),
39                    e
40                ))
41            })?;
42
43        // Parse YAML
44        let config: IdeIntegrationConfig = serde_yaml::from_str(&content).map_err(|e| {
45            IdeError::config_error(format!(
46                "Failed to parse YAML configuration: {}. Please check the file format.",
47                e
48            ))
49        })?;
50
51        // Validate configuration
52        Self::validate_config(&config)?;
53
54        info!("Successfully loaded IDE configuration from {}", file_path);
55        Ok(config)
56    }
57
58    /// Load configuration from a JSON file
59    pub async fn load_from_json_file(file_path: &str) -> IdeResult<IdeIntegrationConfig> {
60        debug!("Loading IDE configuration from JSON: {}", file_path);
61
62        // Expand home directory if needed
63        let resolved_path = PathResolver::expand_home(&PathBuf::from(file_path))
64            .map_err(|e| IdeError::path_resolution_error(format!("Failed to resolve path: {}", e)))?;
65
66        // Read the file
67        let content = tokio::fs::read_to_string(&resolved_path)
68            .await
69            .map_err(|e| {
70                IdeError::config_error(format!(
71                    "Failed to read configuration file '{}': {}",
72                    resolved_path.display(),
73                    e
74                ))
75            })?;
76
77        // Parse JSON
78        let config: IdeIntegrationConfig = serde_json::from_str(&content).map_err(|e| {
79            IdeError::config_error(format!(
80                "Failed to parse JSON configuration: {}. Please check the file format.",
81                e
82            ))
83        })?;
84
85        // Validate configuration
86        Self::validate_config(&config)?;
87
88        info!("Successfully loaded IDE configuration from {}", file_path);
89        Ok(config)
90    }
91
92    /// Load configuration from a file (auto-detect format)
93    pub async fn load_from_file(file_path: &str) -> IdeResult<IdeIntegrationConfig> {
94        if file_path.ends_with(".yaml") || file_path.ends_with(".yml") {
95            Self::load_from_yaml_file(file_path).await
96        } else if file_path.ends_with(".json") {
97            Self::load_from_json_file(file_path).await
98        } else {
99            Err(IdeError::config_error(
100                "Unsupported configuration file format. Use .yaml, .yml, or .json",
101            ))
102        }
103    }
104
105    /// Validate configuration
106    fn validate_config(config: &IdeIntegrationConfig) -> IdeResult<()> {
107        debug!("Validating IDE configuration");
108
109        // Validate provider chain configuration
110        if !config.providers.external_lsp.enabled
111            && !config
112                .providers
113                .configured_rules
114                .as_ref()
115                .map(|c| c.enabled)
116                .unwrap_or(false)
117            && !config.providers.builtin_providers.enabled
118        {
119            return Err(IdeError::config_validation_error(
120                "At least one provider must be enabled (external_lsp, configured_rules, or builtin_providers). \
121                 Please enable at least one provider in your configuration.",
122            ));
123        }
124
125        // Validate external LSP configuration
126        if config.providers.external_lsp.enabled {
127            if config.providers.external_lsp.servers.is_empty() {
128                return Err(IdeError::config_validation_error(
129                    "External LSP is enabled but no servers are configured. \
130                     Please add at least one LSP server configuration or disable external_lsp.",
131                ));
132            }
133
134            for (language, server_config) in &config.providers.external_lsp.servers {
135                if server_config.command.is_empty() {
136                    return Err(IdeError::config_validation_error(format!(
137                        "LSP server for language '{}' has empty command. \
138                         Please specify a valid command to start the LSP server.",
139                        language
140                    )));
141                }
142
143                if server_config.timeout_ms == 0 {
144                    return Err(IdeError::config_validation_error(format!(
145                        "LSP server for language '{}' has invalid timeout (0ms). \
146                         Please set a positive timeout value.",
147                        language
148                    )));
149                }
150            }
151        }
152
153        // Validate configured rules configuration
154        if let Some(rules_config) = &config.providers.configured_rules {
155            if rules_config.enabled && rules_config.rules_path.is_empty() {
156                return Err(IdeError::config_validation_error(
157                    "Configured rules are enabled but rules_path is empty. \
158                     Please specify a valid path to the rules file or disable configured_rules.",
159                ));
160            }
161        }
162
163        // Validate VS Code configuration
164        if let Some(vscode_config) = &config.vscode {
165            if vscode_config.enabled && vscode_config.port == 0 {
166                return Err(IdeError::config_validation_error(
167                    "VS Code integration is enabled but port is 0. \
168                     Please specify a valid port number (1-65535).",
169                ));
170            }
171        }
172
173        debug!("Configuration validation passed");
174        Ok(())
175    }
176
177    /// Get default configuration
178    pub fn default_config() -> IdeIntegrationConfig {
179        IdeIntegrationConfig {
180            vscode: None,
181            terminal: None,
182            providers: crate::types::ProviderChainConfig {
183                external_lsp: crate::types::ExternalLspConfig {
184                    enabled: true,
185                    servers: Default::default(),
186                    health_check_interval_ms: 5000,
187                },
188                configured_rules: None,
189                builtin_providers: crate::types::BuiltinProvidersConfig {
190                    enabled: true,
191                    languages: vec![
192                        "rust".to_string(),
193                        "typescript".to_string(),
194                        "python".to_string(),
195                    ],
196                },
197            },
198        }
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn test_validate_config_valid() {
208        let config = IdeIntegrationConfig {
209            vscode: Some(crate::types::VsCodeConfig {
210                enabled: true,
211                port: 8080,
212                features: vec!["completion".to_string()],
213                settings: serde_json::json!({}),
214            }),
215            terminal: None,
216            providers: crate::types::ProviderChainConfig {
217                external_lsp: crate::types::ExternalLspConfig {
218                    enabled: true,
219                    servers: {
220                        let mut map = std::collections::HashMap::new();
221                        map.insert(
222                            "rust".to_string(),
223                            crate::types::LspServerConfig {
224                                language: "rust".to_string(),
225                                command: "rust-analyzer".to_string(),
226                                args: vec![],
227                                timeout_ms: 5000,
228                            },
229                        );
230                        map
231                    },
232                    health_check_interval_ms: 5000,
233                },
234                configured_rules: None,
235                builtin_providers: crate::types::BuiltinProvidersConfig {
236                    enabled: true,
237                    languages: vec!["rust".to_string()],
238                },
239            },
240        };
241
242        assert!(ConfigManager::validate_config(&config).is_ok());
243    }
244
245    #[test]
246    fn test_validate_config_no_providers_enabled() {
247        let config = IdeIntegrationConfig {
248            vscode: None,
249            terminal: None,
250            providers: crate::types::ProviderChainConfig {
251                external_lsp: crate::types::ExternalLspConfig {
252                    enabled: false,
253                    servers: Default::default(),
254                    health_check_interval_ms: 5000,
255                },
256                configured_rules: None,
257                builtin_providers: crate::types::BuiltinProvidersConfig {
258                    enabled: false,
259                    languages: vec![],
260                },
261            },
262        };
263
264        let result = ConfigManager::validate_config(&config);
265        assert!(result.is_err());
266        assert!(result
267            .unwrap_err()
268            .to_string()
269            .contains("At least one provider must be enabled"));
270    }
271
272    #[test]
273    fn test_validate_config_empty_lsp_servers() {
274        let config = IdeIntegrationConfig {
275            vscode: None,
276            terminal: None,
277            providers: crate::types::ProviderChainConfig {
278                external_lsp: crate::types::ExternalLspConfig {
279                    enabled: true,
280                    servers: Default::default(),
281                    health_check_interval_ms: 5000,
282                },
283                configured_rules: None,
284                builtin_providers: crate::types::BuiltinProvidersConfig {
285                    enabled: false,
286                    languages: vec![],
287                },
288            },
289        };
290
291        let result = ConfigManager::validate_config(&config);
292        assert!(result.is_err());
293        assert!(result
294            .unwrap_err()
295            .to_string()
296            .contains("no servers are configured"));
297    }
298
299    #[test]
300    fn test_validate_config_invalid_lsp_command() {
301        let config = IdeIntegrationConfig {
302            vscode: None,
303            terminal: None,
304            providers: crate::types::ProviderChainConfig {
305                external_lsp: crate::types::ExternalLspConfig {
306                    enabled: true,
307                    servers: {
308                        let mut map = std::collections::HashMap::new();
309                        map.insert(
310                            "rust".to_string(),
311                            crate::types::LspServerConfig {
312                                language: "rust".to_string(),
313                                command: "".to_string(),
314                                args: vec![],
315                                timeout_ms: 5000,
316                            },
317                        );
318                        map
319                    },
320                    health_check_interval_ms: 5000,
321                },
322                configured_rules: None,
323                builtin_providers: crate::types::BuiltinProvidersConfig {
324                    enabled: false,
325                    languages: vec![],
326                },
327            },
328        };
329
330        let result = ConfigManager::validate_config(&config);
331        assert!(result.is_err());
332        assert!(result
333            .unwrap_err()
334            .to_string()
335            .contains("empty command"));
336    }
337
338    #[test]
339    fn test_validate_config_invalid_vscode_port() {
340        let config = IdeIntegrationConfig {
341            vscode: Some(crate::types::VsCodeConfig {
342                enabled: true,
343                port: 0,
344                features: vec![],
345                settings: serde_json::json!({}),
346            }),
347            terminal: None,
348            providers: crate::types::ProviderChainConfig {
349                external_lsp: crate::types::ExternalLspConfig {
350                    enabled: false,
351                    servers: Default::default(),
352                    health_check_interval_ms: 5000,
353                },
354                configured_rules: None,
355                builtin_providers: crate::types::BuiltinProvidersConfig {
356                    enabled: true,
357                    languages: vec!["rust".to_string()],
358                },
359            },
360        };
361
362        let result = ConfigManager::validate_config(&config);
363        assert!(result.is_err());
364        assert!(result
365            .unwrap_err()
366            .to_string()
367            .contains("port is 0"));
368    }
369
370    #[test]
371    fn test_default_config() {
372        let config = ConfigManager::default_config();
373        assert!(config.providers.external_lsp.enabled);
374        assert!(config.providers.builtin_providers.enabled);
375        assert_eq!(config.providers.builtin_providers.languages.len(), 3);
376    }
377}