1use crate::error::{IdeError, IdeResult};
4use crate::types::IdeIntegrationConfig;
5use ricecoder_storage::PathResolver;
6use std::path::PathBuf;
7use tracing::{debug, info};
8
9pub struct ConfigManager;
11
12impl Default for ConfigManager {
13 fn default() -> Self {
14 Self::new()
15 }
16}
17
18impl ConfigManager {
19 pub fn new() -> Self {
21 ConfigManager
22 }
23
24 pub async fn load_from_yaml_file(file_path: &str) -> IdeResult<IdeIntegrationConfig> {
26 debug!("Loading IDE configuration from YAML: {}", file_path);
27
28 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 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 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 Self::validate_config(&config)?;
53
54 info!("Successfully loaded IDE configuration from {}", file_path);
55 Ok(config)
56 }
57
58 pub async fn load_from_json_file(file_path: &str) -> IdeResult<IdeIntegrationConfig> {
60 debug!("Loading IDE configuration from JSON: {}", file_path);
61
62 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 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 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 Self::validate_config(&config)?;
87
88 info!("Successfully loaded IDE configuration from {}", file_path);
89 Ok(config)
90 }
91
92 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 fn validate_config(config: &IdeIntegrationConfig) -> IdeResult<()> {
107 debug!("Validating IDE configuration");
108
109 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 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 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 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 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}