ricecoder_ide/
config_validation.rs1use crate::error::{IdeError, IdeResult};
7use crate::types::*;
8use tracing::debug;
9
10pub struct ConfigValidator;
12
13impl ConfigValidator {
14 pub fn validate_complete(config: &IdeIntegrationConfig) -> IdeResult<()> {
16 debug!("Validating complete IDE configuration");
17
18 Self::validate_provider_chain(&config.providers)?;
20
21 if let Some(vscode_config) = &config.vscode {
23 Self::validate_vscode_config(vscode_config)?;
24 }
25
26 if let Some(terminal_config) = &config.terminal {
27 Self::validate_terminal_config(terminal_config)?;
28 }
29
30 debug!("Configuration validation passed");
31 Ok(())
32 }
33
34 fn validate_provider_chain(providers: &ProviderChainConfig) -> IdeResult<()> {
36 debug!("Validating provider chain configuration");
37
38 let any_enabled = providers.external_lsp.enabled
40 || providers
41 .configured_rules
42 .as_ref()
43 .map(|c| c.enabled)
44 .unwrap_or(false)
45 || providers.builtin_providers.enabled;
46
47 if !any_enabled {
48 return Err(IdeError::config_validation_error(
49 "Configuration validation failed: At least one provider must be enabled.\n\
50 \n\
51 Remediation steps:\n\
52 1. Enable at least one of: external_lsp, configured_rules, or builtin_providers\n\
53 2. Example configuration:\n\
54 providers:\n\
55 external_lsp:\n\
56 enabled: true\n\
57 builtin_providers:\n\
58 enabled: true\n\
59 3. For more details, see: https://ricecoder.dev/docs/ide-integration/configuration",
60 ));
61 }
62
63 Self::validate_external_lsp(&providers.external_lsp)?;
65
66 if let Some(rules_config) = &providers.configured_rules {
68 Self::validate_configured_rules(rules_config)?;
69 }
70
71 Self::validate_builtin_providers(&providers.builtin_providers)?;
73
74 Ok(())
75 }
76
77 fn validate_external_lsp(config: &ExternalLspConfig) -> IdeResult<()> {
79 if !config.enabled {
80 return Ok(());
81 }
82
83 debug!("Validating external LSP configuration");
84
85 if config.servers.is_empty() {
86 return Err(IdeError::config_validation_error(
87 "Configuration validation failed: External LSP is enabled but no servers are configured.\n\
88 \n\
89 Remediation steps:\n\
90 1. Add at least one LSP server configuration, or\n\
91 2. Disable external_lsp if you don't want to use external LSP servers\n\
92 \n\
93 Example configuration:\n\
94 providers:\n\
95 external_lsp:\n\
96 enabled: true\n\
97 servers:\n\
98 rust:\n\
99 language: rust\n\
100 command: rust-analyzer\n\
101 args: []\n\
102 timeout_ms: 5000\n\
103 \n\
104 For more details, see: https://ricecoder.dev/docs/ide-integration/lsp-configuration",
105 ));
106 }
107
108 for (language, server_config) in &config.servers {
110 Self::validate_lsp_server_config(language, server_config)?;
111 }
112
113 if config.health_check_interval_ms == 0 {
115 return Err(IdeError::config_validation_error(
116 "Configuration validation failed: health_check_interval_ms must be greater than 0.\n\
117 \n\
118 Remediation steps:\n\
119 1. Set health_check_interval_ms to a positive value (e.g., 5000 for 5 seconds)\n\
120 2. Example configuration:\n\
121 providers:\n\
122 external_lsp:\n\
123 health_check_interval_ms: 5000",
124 ));
125 }
126
127 Ok(())
128 }
129
130 fn validate_lsp_server_config(language: &str, config: &LspServerConfig) -> IdeResult<()> {
132 debug!("Validating LSP server configuration for language: {}", language);
133
134 if config.language.is_empty() {
136 return Err(IdeError::config_validation_error(format!(
137 "Configuration validation failed: LSP server for '{}' has empty language field.\n\
138 \n\
139 Remediation steps:\n\
140 1. Ensure the language field matches the key (e.g., 'rust' for rust-analyzer)\n\
141 2. Example configuration:\n\
142 servers:\n\
143 rust:\n\
144 language: rust\n\
145 command: rust-analyzer",
146 language
147 )));
148 }
149
150 if config.command.is_empty() {
152 return Err(IdeError::config_validation_error(format!(
153 "Configuration validation failed: LSP server for '{}' has empty command.\n\
154 \n\
155 Remediation steps:\n\
156 1. Specify the command to start the LSP server\n\
157 2. Common LSP servers:\n\
158 - Rust: rust-analyzer\n\
159 - TypeScript: typescript-language-server\n\
160 - Python: pylsp\n\
161 3. Example configuration:\n\
162 servers:\n\
163 {}:\n\
164 command: <lsp-server-command>\n\
165 \n\
166 For more details, see: https://ricecoder.dev/docs/ide-integration/lsp-servers",
167 language, language
168 )));
169 }
170
171 if config.timeout_ms == 0 {
173 return Err(IdeError::config_validation_error(format!(
174 "Configuration validation failed: LSP server for '{}' has invalid timeout (0ms).\n\
175 \n\
176 Remediation steps:\n\
177 1. Set timeout_ms to a positive value (e.g., 5000 for 5 seconds)\n\
178 2. Recommended values:\n\
179 - Fast operations: 1000-2000ms\n\
180 - Normal operations: 5000-10000ms\n\
181 - Slow operations: 15000-30000ms\n\
182 3. Example configuration:\n\
183 servers:\n\
184 {}:\n\
185 timeout_ms: 5000",
186 language, language
187 )));
188 }
189
190 if config.timeout_ms > 120000 {
191 return Err(IdeError::config_validation_error(format!(
192 "Configuration validation failed: LSP server for '{}' has excessive timeout ({}ms > 120000ms).\n\
193 \n\
194 Remediation steps:\n\
195 1. Reduce timeout_ms to a reasonable value (max 120000ms = 2 minutes)\n\
196 2. Example configuration:\n\
197 servers:\n\
198 {}:\n\
199 timeout_ms: 30000",
200 language, config.timeout_ms, language
201 )));
202 }
203
204 Ok(())
205 }
206
207 fn validate_configured_rules(config: &ConfiguredRulesConfig) -> IdeResult<()> {
209 if !config.enabled {
210 return Ok(());
211 }
212
213 debug!("Validating configured rules configuration");
214
215 if config.rules_path.is_empty() {
216 return Err(IdeError::config_validation_error(
217 "Configuration validation failed: Configured rules are enabled but rules_path is empty.\n\
218 \n\
219 Remediation steps:\n\
220 1. Specify a valid path to the rules file, or\n\
221 2. Disable configured_rules if you don't want to use custom rules\n\
222 \n\
223 Example configuration:\n\
224 providers:\n\
225 configured_rules:\n\
226 enabled: true\n\
227 rules_path: config/ide-rules.yaml\n\
228 \n\
229 For more details, see: https://ricecoder.dev/docs/ide-integration/custom-rules",
230 ));
231 }
232
233 Ok(())
234 }
235
236 fn validate_builtin_providers(config: &BuiltinProvidersConfig) -> IdeResult<()> {
238 if !config.enabled {
239 return Ok(());
240 }
241
242 debug!("Validating built-in providers configuration");
243
244 if config.languages.is_empty() {
245 return Err(IdeError::config_validation_error(
246 "Configuration validation failed: Built-in providers are enabled but no languages are configured.\n\
247 \n\
248 Remediation steps:\n\
249 1. Add at least one language to the languages list, or\n\
250 2. Disable builtin_providers if you don't want to use built-in providers\n\
251 \n\
252 Supported languages:\n\
253 - rust\n\
254 - typescript\n\
255 - python\n\
256 \n\
257 Example configuration:\n\
258 providers:\n\
259 builtin_providers:\n\
260 enabled: true\n\
261 languages:\n\
262 - rust\n\
263 - typescript\n\
264 - python",
265 ));
266 }
267
268 let valid_languages = ["rust", "typescript", "python"];
270 for lang in &config.languages {
271 if !valid_languages.contains(&lang.as_str()) {
272 return Err(IdeError::config_validation_error(format!(
273 "Configuration validation failed: Unknown language '{}' in builtin_providers.\n\
274 \n\
275 Supported languages:\n\
276 - rust\n\
277 - typescript\n\
278 - python\n\
279 \n\
280 Remediation steps:\n\
281 1. Use only supported language names\n\
282 2. Example configuration:\n\
283 providers:\n\
284 builtin_providers:\n\
285 languages:\n\
286 - rust\n\
287 - typescript",
288 lang
289 )));
290 }
291 }
292
293 Ok(())
294 }
295
296 fn validate_vscode_config(config: &VsCodeConfig) -> IdeResult<()> {
298 if !config.enabled {
299 return Ok(());
300 }
301
302 debug!("Validating VS Code configuration");
303
304 if config.port == 0 {
306 return Err(IdeError::config_validation_error(
307 "Configuration validation failed: VS Code integration is enabled but port is 0.\n\
308 \n\
309 Remediation steps:\n\
310 1. Specify a valid port number (1-65535)\n\
311 2. Recommended ports:\n\
312 - Development: 8000-9000\n\
313 - Production: 3000-5000\n\
314 3. Example configuration:\n\
315 vscode:\n\
316 enabled: true\n\
317 port: 8080",
318 ));
319 }
320
321 if config.features.is_empty() {
328 return Err(IdeError::config_validation_error(
329 "Configuration validation failed: VS Code integration is enabled but no features are configured.\n\
330 \n\
331 Remediation steps:\n\
332 1. Add at least one feature to the features list, or\n\
333 2. Disable VS Code integration if you don't want to use it\n\
334 \n\
335 Available features:\n\
336 - completion\n\
337 - diagnostics\n\
338 - hover\n\
339 - definition\n\
340 \n\
341 Example configuration:\n\
342 vscode:\n\
343 enabled: true\n\
344 features:\n\
345 - completion\n\
346 - diagnostics\n\
347 - hover",
348 ));
349 }
350
351 let valid_features = ["completion", "diagnostics", "hover", "definition"];
353 for feature in &config.features {
354 if !valid_features.contains(&feature.as_str()) {
355 return Err(IdeError::config_validation_error(format!(
356 "Configuration validation failed: Unknown feature '{}' in VS Code configuration.\n\
357 \n\
358 Available features:\n\
359 - completion\n\
360 - diagnostics\n\
361 - hover\n\
362 - definition\n\
363 \n\
364 Remediation steps:\n\
365 1. Use only supported feature names\n\
366 2. Example configuration:\n\
367 vscode:\n\
368 features:\n\
369 - completion\n\
370 - diagnostics",
371 feature
372 )));
373 }
374 }
375
376 Ok(())
377 }
378
379 fn validate_terminal_config(config: &TerminalConfig) -> IdeResult<()> {
381 debug!("Validating terminal editor configuration");
382
383 let any_enabled = config
385 .vim
386 .as_ref()
387 .map(|c| c.enabled)
388 .unwrap_or(false)
389 || config
390 .emacs
391 .as_ref()
392 .map(|c| c.enabled)
393 .unwrap_or(false);
394
395 if !any_enabled {
396 return Ok(()); }
398
399 if let Some(vim_config) = &config.vim {
401 if vim_config.enabled && vim_config.plugin_manager.is_empty() {
402 return Err(IdeError::config_validation_error(
403 "Configuration validation failed: Vim/Neovim integration is enabled but plugin_manager is empty.\n\
404 \n\
405 Remediation steps:\n\
406 1. Specify a valid plugin manager\n\
407 2. Supported plugin managers:\n\
408 - vim-plug\n\
409 - vundle\n\
410 - pathogen\n\
411 - packer (for neovim)\n\
412 - lazy.nvim (for neovim)\n\
413 3. Example configuration:\n\
414 terminal:\n\
415 vim:\n\
416 enabled: true\n\
417 plugin_manager: vim-plug",
418 ));
419 }
420 }
421
422 if let Some(emacs_config) = &config.emacs {
423 if emacs_config.enabled && emacs_config.package_manager.is_empty() {
424 return Err(IdeError::config_validation_error(
425 "Configuration validation failed: Emacs integration is enabled but package_manager is empty.\n\
426 \n\
427 Remediation steps:\n\
428 1. Specify a valid package manager\n\
429 2. Supported package managers:\n\
430 - use-package\n\
431 - straight.el\n\
432 - quelpa\n\
433 3. Example configuration:\n\
434 terminal:\n\
435 emacs:\n\
436 enabled: true\n\
437 package_manager: use-package",
438 ));
439 }
440 }
441
442 Ok(())
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449 use std::collections::HashMap;
450
451 fn create_valid_config() -> IdeIntegrationConfig {
452 IdeIntegrationConfig {
453 vscode: Some(VsCodeConfig {
454 enabled: true,
455 port: 8080,
456 features: vec!["completion".to_string()],
457 settings: serde_json::json!({}),
458 }),
459 terminal: None,
460 providers: ProviderChainConfig {
461 external_lsp: ExternalLspConfig {
462 enabled: true,
463 servers: {
464 let mut map = HashMap::new();
465 map.insert(
466 "rust".to_string(),
467 LspServerConfig {
468 language: "rust".to_string(),
469 command: "rust-analyzer".to_string(),
470 args: vec![],
471 timeout_ms: 5000,
472 },
473 );
474 map
475 },
476 health_check_interval_ms: 5000,
477 },
478 configured_rules: None,
479 builtin_providers: BuiltinProvidersConfig {
480 enabled: true,
481 languages: vec!["rust".to_string()],
482 },
483 },
484 }
485 }
486
487 #[test]
488 fn test_validate_complete_valid_config() {
489 let config = create_valid_config();
490 assert!(ConfigValidator::validate_complete(&config).is_ok());
491 }
492
493 #[test]
494 fn test_validate_no_providers_enabled() {
495 let mut config = create_valid_config();
496 config.providers.external_lsp.enabled = false;
497 config.providers.builtin_providers.enabled = false;
498
499 let result = ConfigValidator::validate_complete(&config);
500 assert!(result.is_err());
501 assert!(result
502 .unwrap_err()
503 .to_string()
504 .contains("At least one provider must be enabled"));
505 }
506
507 #[test]
508 fn test_validate_empty_lsp_servers() {
509 let mut config = create_valid_config();
510 config.providers.external_lsp.servers.clear();
511
512 let result = ConfigValidator::validate_complete(&config);
513 assert!(result.is_err());
514 assert!(result
515 .unwrap_err()
516 .to_string()
517 .contains("no servers are configured"));
518 }
519
520 #[test]
521 fn test_validate_invalid_lsp_command() {
522 let mut config = create_valid_config();
523 config
524 .providers
525 .external_lsp
526 .servers
527 .get_mut("rust")
528 .unwrap()
529 .command = String::new();
530
531 let result = ConfigValidator::validate_complete(&config);
532 assert!(result.is_err());
533 assert!(result
534 .unwrap_err()
535 .to_string()
536 .contains("empty command"));
537 }
538
539 #[test]
540 fn test_validate_invalid_lsp_timeout() {
541 let mut config = create_valid_config();
542 config
543 .providers
544 .external_lsp
545 .servers
546 .get_mut("rust")
547 .unwrap()
548 .timeout_ms = 0;
549
550 let result = ConfigValidator::validate_complete(&config);
551 assert!(result.is_err());
552 assert!(result
553 .unwrap_err()
554 .to_string()
555 .contains("invalid timeout"));
556 }
557
558 #[test]
559 fn test_validate_excessive_lsp_timeout() {
560 let mut config = create_valid_config();
561 config
562 .providers
563 .external_lsp
564 .servers
565 .get_mut("rust")
566 .unwrap()
567 .timeout_ms = 200000;
568
569 let result = ConfigValidator::validate_complete(&config);
570 assert!(result.is_err());
571 assert!(result
572 .unwrap_err()
573 .to_string()
574 .contains("excessive timeout"));
575 }
576
577 #[test]
578 fn test_validate_invalid_vscode_port() {
579 let mut config = create_valid_config();
580 config.vscode.as_mut().unwrap().port = 0;
581
582 let result = ConfigValidator::validate_complete(&config);
583 assert!(result.is_err());
584 assert!(result
585 .unwrap_err()
586 .to_string()
587 .contains("port is 0"));
588 }
589
590 #[test]
591 fn test_validate_empty_vscode_features() {
592 let mut config = create_valid_config();
593 config.vscode.as_mut().unwrap().features.clear();
594
595 let result = ConfigValidator::validate_complete(&config);
596 assert!(result.is_err());
597 assert!(result
598 .unwrap_err()
599 .to_string()
600 .contains("no features are configured"));
601 }
602
603 #[test]
604 fn test_validate_invalid_vscode_feature() {
605 let mut config = create_valid_config();
606 config.vscode.as_mut().unwrap().features = vec!["invalid_feature".to_string()];
607
608 let result = ConfigValidator::validate_complete(&config);
609 assert!(result.is_err());
610 assert!(result
611 .unwrap_err()
612 .to_string()
613 .contains("Unknown feature"));
614 }
615
616 #[test]
617 fn test_validate_empty_builtin_languages() {
618 let mut config = create_valid_config();
619 config.providers.builtin_providers.languages.clear();
620
621 let result = ConfigValidator::validate_complete(&config);
622 assert!(result.is_err());
623 assert!(result
624 .unwrap_err()
625 .to_string()
626 .contains("no languages are configured"));
627 }
628
629 #[test]
630 fn test_validate_invalid_builtin_language() {
631 let mut config = create_valid_config();
632 config.providers.builtin_providers.languages = vec!["invalid_lang".to_string()];
633
634 let result = ConfigValidator::validate_complete(&config);
635 assert!(result.is_err());
636 assert!(result
637 .unwrap_err()
638 .to_string()
639 .contains("Unknown language"));
640 }
641
642 #[test]
643 fn test_validate_vim_empty_plugin_manager() {
644 let mut config = create_valid_config();
645 config.terminal = Some(TerminalConfig {
646 vim: Some(VimConfig {
647 enabled: true,
648 plugin_manager: String::new(),
649 }),
650 emacs: None,
651 });
652
653 let result = ConfigValidator::validate_complete(&config);
654 assert!(result.is_err());
655 assert!(result
656 .unwrap_err()
657 .to_string()
658 .contains("plugin_manager is empty"));
659 }
660
661 #[test]
662 fn test_validate_vim_neovim_empty_plugin_manager() {
663 let mut config = create_valid_config();
664 config.terminal = Some(TerminalConfig {
665 vim: Some(VimConfig {
666 enabled: true,
667 plugin_manager: String::new(),
668 }),
669 emacs: None,
670 });
671
672 let result = ConfigValidator::validate_complete(&config);
673 assert!(result.is_err());
674 assert!(result
675 .unwrap_err()
676 .to_string()
677 .contains("plugin_manager is empty"));
678 }
679
680 #[test]
681 fn test_validate_emacs_empty_package_manager() {
682 let mut config = create_valid_config();
683 config.terminal = Some(TerminalConfig {
684 vim: None,
685 emacs: Some(EmacsConfig {
686 enabled: true,
687 package_manager: String::new(),
688 }),
689 });
690
691 let result = ConfigValidator::validate_complete(&config);
692 assert!(result.is_err());
693 assert!(result
694 .unwrap_err()
695 .to_string()
696 .contains("package_manager is empty"));
697 }
698}