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
11fn print_validation_errors(colors: Colors, errors: &[ConfigValidationError]) {
12 println!("{}Validation errors found:{}", colors.red(), colors.reset());
13 println!();
14
15 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
132pub 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
194fn 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
226pub fn handle_check_config(colors: Colors, verbose: bool) -> anyhow::Result<bool> {
234 handle_check_config_with(colors, &RealConfigEnvironment, verbose)
235}