ricecoder_ide/
config_validation.rs

1//! Configuration validation for IDE integration
2//!
3//! This module provides comprehensive configuration validation with clear error messages
4//! and remediation steps for IDE integration configuration.
5
6use crate::error::{IdeError, IdeResult};
7use crate::types::*;
8use tracing::debug;
9
10/// Configuration validator for IDE integration
11pub struct ConfigValidator;
12
13impl ConfigValidator {
14    /// Validate complete IDE integration configuration
15    pub fn validate_complete(config: &IdeIntegrationConfig) -> IdeResult<()> {
16        debug!("Validating complete IDE configuration");
17
18        // Validate provider chain
19        Self::validate_provider_chain(&config.providers)?;
20
21        // Validate IDE-specific configurations
22        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    /// Validate provider chain configuration
35    fn validate_provider_chain(providers: &ProviderChainConfig) -> IdeResult<()> {
36        debug!("Validating provider chain configuration");
37
38        // Check that at least one provider is enabled
39        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        // Validate external LSP configuration
64        Self::validate_external_lsp(&providers.external_lsp)?;
65
66        // Validate configured rules configuration
67        if let Some(rules_config) = &providers.configured_rules {
68            Self::validate_configured_rules(rules_config)?;
69        }
70
71        // Validate built-in providers configuration
72        Self::validate_builtin_providers(&providers.builtin_providers)?;
73
74        Ok(())
75    }
76
77    /// Validate external LSP configuration
78    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        // Validate each server configuration
109        for (language, server_config) in &config.servers {
110            Self::validate_lsp_server_config(language, server_config)?;
111        }
112
113        // Validate health check interval
114        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    /// Validate individual LSP server configuration
131    fn validate_lsp_server_config(language: &str, config: &LspServerConfig) -> IdeResult<()> {
132        debug!("Validating LSP server configuration for language: {}", language);
133
134        // Validate language field
135        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        // Validate command field
151        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        // Validate timeout
172        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    /// Validate configured rules configuration
208    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    /// Validate built-in providers configuration
237    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        // Validate language names
269        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    /// Validate VS Code configuration
297    fn validate_vscode_config(config: &VsCodeConfig) -> IdeResult<()> {
298        if !config.enabled {
299            return Ok(());
300        }
301
302        debug!("Validating VS Code configuration");
303
304        // Validate port
305        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        // Note: u16 max is 65535, so this check is not needed but kept for clarity
322        // if config.port > 65535 {
323        //     return Err(...);
324        // }
325
326        // Validate features
327        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        // Validate feature names
352        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    /// Validate terminal editor configuration
380    fn validate_terminal_config(config: &TerminalConfig) -> IdeResult<()> {
381        debug!("Validating terminal editor configuration");
382
383        // At least one editor should be configured
384        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(()); // Terminal config is optional
397        }
398
399        // Validate individual editor configurations
400        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}