Skip to main content

ralph_workflow/cli/init/config_generation/
validation.rs

1//! Configuration validation and error display.
2//!
3//! Handles `--check-config` flag to validate config files and display effective settings.
4
5use crate::config::loader::{load_config_from_path_with_env, ConfigLoadWithValidationError};
6use crate::config::unified::UnifiedConfig;
7use crate::config::validation::ConfigValidationError;
8use crate::config::{Config, ConfigEnvironment, RealConfigEnvironment};
9use crate::logger::Colors;
10
11fn print_validation_errors(colors: Colors, errors: &[ConfigValidationError]) {
12    println!("{}Validation errors found:{}", colors.red(), colors.reset());
13    println!();
14
15    // Group errors by file for clearer presentation
16    let mut global_errors: Vec<_> = Vec::new();
17    let mut local_errors: Vec<_> = Vec::new();
18    let mut other_errors: Vec<_> = Vec::new();
19
20    for error in errors {
21        let path_str = error.file().to_string_lossy();
22        if path_str.contains(".config") {
23            global_errors.push(error);
24        } else if path_str.contains(".agent") {
25            local_errors.push(error);
26        } else {
27            other_errors.push(error);
28        }
29    }
30
31    if !global_errors.is_empty() {
32        println!(
33            "{}~/.config/ralph-workflow.toml:{}",
34            colors.yellow(),
35            colors.reset()
36        );
37        for error in global_errors {
38            print_config_error(colors, error);
39        }
40        println!();
41    }
42
43    if !local_errors.is_empty() {
44        println!(
45            "{}.agent/ralph-workflow.toml:{}",
46            colors.yellow(),
47            colors.reset()
48        );
49        for error in local_errors {
50            print_config_error(colors, error);
51        }
52        println!();
53    }
54
55    if !other_errors.is_empty() {
56        for error in other_errors {
57            println!(
58                "{}{}:{}",
59                colors.yellow(),
60                error.file().display(),
61                colors.reset()
62            );
63            print_config_error(colors, error);
64            println!();
65        }
66    }
67
68    println!(
69        "{}Fix these errors and try again.{}",
70        colors.red(),
71        colors.reset()
72    );
73}
74
75fn print_config_sources<R: ConfigEnvironment>(colors: Colors, env: &R) {
76    let global_path = env.unified_config_path();
77    let local_path = env.local_config_path();
78
79    println!("{}Configuration sources:{}", colors.cyan(), colors.reset());
80
81    if let Some(path) = global_path {
82        let exists = env.file_exists(&path);
83        println!(
84            "  Global: {} {}",
85            path.display(),
86            if exists {
87                format!("{}(active){}", colors.green(), colors.reset())
88            } else {
89                format!("{}(not found){}", colors.dim(), colors.reset())
90            }
91        );
92    }
93
94    if let Some(path) = local_path {
95        let exists = env.file_exists(&path);
96        println!(
97            "  Local:  {} {}",
98            path.display(),
99            if exists {
100                format!("{}(active){}", colors.green(), colors.reset())
101            } else {
102                format!("{}(not found){}", colors.dim(), colors.reset())
103            }
104        );
105    }
106}
107
108fn print_effective_settings(colors: Colors, config: &Config) {
109    println!();
110    println!("{}Effective settings:{}", colors.cyan(), colors.reset());
111    println!("  Verbosity: {}", config.verbosity as u8);
112    println!("  Developer iterations: {}", config.developer_iters);
113    println!("  Reviewer reviews: {}", config.reviewer_reviews);
114    println!("  Interactive: {}", config.behavior.interactive);
115    println!("  Isolation mode: {}", config.isolation_mode);
116}
117
118fn print_merged_config(colors: Colors, merged_unified: Option<UnifiedConfig>) {
119    println!();
120    println!(
121        "{}Full merged configuration:{}",
122        colors.cyan(),
123        colors.reset()
124    );
125    if let Some(unified) = merged_unified {
126        let toml_str = toml::to_string_pretty(&unified)
127            .unwrap_or_else(|_| "Error serializing config".to_string());
128        println!("{toml_str}");
129    }
130}
131
132/// Handle the `--check-config` flag with a custom environment.
133///
134/// Validates all config files and displays effective merged settings.
135/// Returns error (non-zero exit) if validation fails.
136///
137/// # Arguments
138///
139/// * `colors` - Terminal color configuration for output
140/// * `env` - Config environment for path resolution and file operations
141/// * `verbose` - Whether to display full merged configuration
142///
143/// # Returns
144///
145/// Returns `Ok(true)` if validation succeeded, or an error if validation failed.
146///
147/// # Errors
148///
149/// Returns error if the operation fails.
150pub fn handle_check_config_with<R: ConfigEnvironment>(
151    colors: Colors,
152    env: &R,
153    verbose: bool,
154) -> anyhow::Result<bool> {
155    println!(
156        "{}Checking configuration...{}",
157        colors.dim(),
158        colors.reset()
159    );
160    println!();
161
162    let (config, merged_unified, warnings) = match load_config_from_path_with_env(None, env) {
163        Ok(result) => result,
164        Err(ConfigLoadWithValidationError::ValidationErrors(errors)) => {
165            print_validation_errors(colors, &errors);
166            return Err(anyhow::anyhow!("Configuration validation failed"));
167        }
168        Err(ConfigLoadWithValidationError::Io(e)) => {
169            return Err(anyhow::anyhow!("Failed to read config file: {e}"));
170        }
171    };
172
173    if !warnings.is_empty() {
174        println!("{}Warnings:{}", colors.yellow(), colors.reset());
175        for warning in &warnings {
176            println!("  {warning}");
177        }
178        println!();
179    }
180
181    print_config_sources(colors, env);
182    print_effective_settings(colors, &config);
183
184    if verbose {
185        print_merged_config(colors, merged_unified);
186    }
187
188    println!();
189    println!("{}Configuration valid{}", colors.green(), colors.reset());
190
191    Ok(true)
192}
193
194/// Print a single config validation error with appropriate formatting.
195fn print_config_error(colors: Colors, error: &ConfigValidationError) {
196    match error {
197        ConfigValidationError::TomlSyntax { error, .. } => {
198            println!("  {}TOML syntax error:{}", colors.red(), colors.reset());
199            println!("    {error}");
200        }
201        ConfigValidationError::UnknownKey {
202            key, suggestion, ..
203        } => {
204            println!("  {}Unknown key '{}'{}", colors.red(), key, colors.reset());
205            if let Some(s) = suggestion {
206                println!(
207                    "    {}Did you mean '{}'?{}",
208                    colors.dim(),
209                    s,
210                    colors.reset()
211                );
212            }
213        }
214        ConfigValidationError::InvalidValue { key, message, .. } => {
215            println!(
216                "  {}Invalid value for '{}'{}",
217                colors.red(),
218                key,
219                colors.reset()
220            );
221            println!("    {message}");
222        }
223    }
224}
225
226/// Handle the `--check-config` flag using the default environment.
227///
228/// Convenience wrapper that uses [`RealConfigEnvironment`] internally.
229///
230/// # Errors
231///
232/// Returns error if the operation fails.
233pub fn handle_check_config(colors: Colors, verbose: bool) -> anyhow::Result<bool> {
234    handle_check_config_with(colors, &RealConfigEnvironment, verbose)
235}