ralph_workflow/cli/init/config_generation/
validation.rs1use 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
182pub 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
255fn 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
300pub fn handle_check_config(colors: Colors, verbose: bool) -> anyhow::Result<bool> {
308 handle_check_config_with(colors, &RealConfigEnvironment, verbose)
309}