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