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
11trait StdIoWriteCompat {
12    fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::io::Result<()>;
13}
14
15impl<T: std::io::Write> StdIoWriteCompat for T {
16    fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::io::Result<()> {
17        std::io::Write::write_fmt(self, args)
18    }
19}
20
21fn print_validation_errors(colors: Colors, errors: &[ConfigValidationError]) {
22    let _ = writeln!(
23        std::io::stdout(),
24        "{}Validation errors found:{}",
25        colors.red(),
26        colors.reset()
27    );
28    let _ = writeln!(std::io::stdout());
29
30    let global_errors: Vec<&ConfigValidationError> = errors
31        .iter()
32        .filter(|e| e.file().to_string_lossy().contains(".config"))
33        .collect();
34
35    let local_errors: Vec<&ConfigValidationError> = errors
36        .iter()
37        .filter(|e| e.file().to_string_lossy().contains(".agent"))
38        .collect();
39
40    let other_errors: Vec<&ConfigValidationError> = errors
41        .iter()
42        .filter(|e| {
43            let path = e.file().to_string_lossy();
44            !path.contains(".config") && !path.contains(".agent")
45        })
46        .collect();
47
48    if !global_errors.is_empty() {
49        let _ = writeln!(
50            std::io::stdout(),
51            "{}~/.config/ralph-workflow.toml:{}",
52            colors.yellow(),
53            colors.reset()
54        );
55        global_errors
56            .iter()
57            .for_each(|error| print_config_error(colors, error));
58        let _ = writeln!(std::io::stdout());
59    }
60
61    if !local_errors.is_empty() {
62        let _ = writeln!(
63            std::io::stdout(),
64            "{}.agent/ralph-workflow.toml:{}",
65            colors.yellow(),
66            colors.reset()
67        );
68        local_errors
69            .iter()
70            .for_each(|error| print_config_error(colors, error));
71        let _ = writeln!(std::io::stdout());
72    }
73
74    if !other_errors.is_empty() {
75        other_errors.iter().for_each(|error| {
76            let _ = writeln!(
77                std::io::stdout(),
78                "{}{}:{}",
79                colors.yellow(),
80                error.file().display(),
81                colors.reset()
82            );
83            print_config_error(colors, error);
84            let _ = writeln!(std::io::stdout());
85        });
86    }
87
88    let _ = writeln!(
89        std::io::stdout(),
90        "{}Fix these errors and try again.{}",
91        colors.red(),
92        colors.reset()
93    );
94}
95
96fn print_config_sources<R: ConfigEnvironment>(colors: Colors, env: &R) {
97    let global_path = env.unified_config_path();
98    let local_path = env.local_config_path();
99
100    let _ = writeln!(
101        std::io::stdout(),
102        "{}Configuration sources:{}",
103        colors.cyan(),
104        colors.reset()
105    );
106
107    if let Some(path) = global_path {
108        let exists = env.file_exists(&path);
109        let _ = writeln!(
110            std::io::stdout(),
111            "  Global: {} {}",
112            path.display(),
113            if exists {
114                format!("{}(active){}", colors.green(), colors.reset())
115            } else {
116                format!("{}(not found){}", colors.dim(), colors.reset())
117            }
118        );
119    }
120
121    if let Some(path) = local_path {
122        let exists = env.file_exists(&path);
123        let _ = writeln!(
124            std::io::stdout(),
125            "  Local:  {} {}",
126            path.display(),
127            if exists {
128                format!("{}(active){}", colors.green(), colors.reset())
129            } else {
130                format!("{}(not found){}", colors.dim(), colors.reset())
131            }
132        );
133    }
134}
135
136fn print_effective_settings(colors: Colors, config: &Config) {
137    let _ = writeln!(std::io::stdout());
138    let _ = writeln!(
139        std::io::stdout(),
140        "{}Effective settings:{}",
141        colors.cyan(),
142        colors.reset()
143    );
144    let _ = writeln!(std::io::stdout(), "  Verbosity: {}", config.verbosity as u8);
145    let _ = writeln!(
146        std::io::stdout(),
147        "  Developer iterations: {}",
148        config.developer_iters
149    );
150    let _ = writeln!(
151        std::io::stdout(),
152        "  Reviewer reviews: {}",
153        config.reviewer_reviews
154    );
155    let _ = writeln!(
156        std::io::stdout(),
157        "  Interactive: {}",
158        config.behavior.interactive
159    );
160    let _ = writeln!(
161        std::io::stdout(),
162        "  Isolation mode: {}",
163        config.isolation_mode
164    );
165}
166
167fn print_merged_config(colors: Colors, merged_unified: Option<UnifiedConfig>) {
168    let _ = writeln!(std::io::stdout());
169    let _ = writeln!(
170        std::io::stdout(),
171        "{}Full merged configuration:{}",
172        colors.cyan(),
173        colors.reset()
174    );
175    if let Some(unified) = merged_unified {
176        let toml_str = toml::to_string_pretty(&unified)
177            .unwrap_or_else(|_| "Error serializing config".to_string());
178        let _ = writeln!(std::io::stdout(), "{toml_str}");
179    }
180}
181
182/// Handle the `--check-config` flag with a custom environment.
183///
184/// Validates all config files and displays effective merged settings.
185/// Returns error (non-zero exit) if validation fails.
186///
187/// # Arguments
188///
189/// * `colors` - Terminal color configuration for output
190/// * `env` - Config environment for path resolution and file operations
191/// * `verbose` - Whether to display full merged configuration
192///
193/// # Returns
194///
195/// Returns `Ok(true)` if validation succeeded, or an error if validation failed.
196///
197/// # Errors
198///
199/// Returns error if the operation fails.
200pub fn handle_check_config_with<R: ConfigEnvironment>(
201    colors: Colors,
202    env: &R,
203    verbose: bool,
204) -> anyhow::Result<bool> {
205    let _ = writeln!(
206        std::io::stdout(),
207        "{}Checking configuration...{}",
208        colors.dim(),
209        colors.reset()
210    );
211    let _ = writeln!(std::io::stdout());
212
213    let (config, merged_unified, warnings) = match load_config_from_path_with_env(None, env) {
214        Ok(result) => result,
215        Err(ConfigLoadWithValidationError::ValidationErrors(errors)) => {
216            print_validation_errors(colors, &errors);
217            return Err(anyhow::anyhow!("Configuration validation failed"));
218        }
219        Err(ConfigLoadWithValidationError::Io(e)) => {
220            return Err(anyhow::anyhow!("Failed to read config file: {e}"));
221        }
222    };
223
224    if !warnings.is_empty() {
225        let _ = writeln!(
226            std::io::stdout(),
227            "{}Warnings:{}",
228            colors.yellow(),
229            colors.reset()
230        );
231        warnings.iter().for_each(|warning| {
232            let _ = writeln!(std::io::stdout(), "  {warning}");
233        });
234        let _ = writeln!(std::io::stdout());
235    }
236
237    print_config_sources(colors, env);
238    print_effective_settings(colors, &config);
239
240    if verbose {
241        print_merged_config(colors, merged_unified);
242    }
243
244    let _ = writeln!(std::io::stdout());
245    let _ = writeln!(
246        std::io::stdout(),
247        "{}Configuration valid{}",
248        colors.green(),
249        colors.reset()
250    );
251
252    Ok(true)
253}
254
255/// Print a single config validation error with appropriate formatting.
256fn print_config_error(colors: Colors, error: &ConfigValidationError) {
257    match error {
258        ConfigValidationError::TomlSyntax { error, .. } => {
259            let _ = writeln!(
260                std::io::stdout(),
261                "  {}TOML syntax error:{}",
262                colors.red(),
263                colors.reset()
264            );
265            let _ = writeln!(std::io::stdout(), "    {error}");
266        }
267        ConfigValidationError::UnknownKey {
268            key, suggestion, ..
269        } => {
270            let _ = writeln!(
271                std::io::stdout(),
272                "  {}Unknown key '{}'{}",
273                colors.red(),
274                key,
275                colors.reset()
276            );
277            if let Some(s) = suggestion {
278                let _ = writeln!(
279                    std::io::stdout(),
280                    "    {}Did you mean '{}'?{}",
281                    colors.dim(),
282                    s,
283                    colors.reset()
284                );
285            }
286        }
287        ConfigValidationError::InvalidValue { key, message, .. } => {
288            let _ = writeln!(
289                std::io::stdout(),
290                "  {}Invalid value for '{}'{}",
291                colors.red(),
292                key,
293                colors.reset()
294            );
295            let _ = writeln!(std::io::stdout(), "    {message}");
296        }
297    }
298}
299
300/// Handle the `--check-config` flag using the default environment.
301///
302/// Convenience wrapper that uses [`RealConfigEnvironment`] internally.
303///
304/// # Errors
305///
306/// Returns error if the operation fails.
307pub fn handle_check_config(colors: Colors, verbose: bool) -> anyhow::Result<bool> {
308    handle_check_config_with(colors, &RealConfigEnvironment, verbose)
309}