1use crate::commands::script::{Scripts, Script};
6use std::collections::HashSet;
7use std::process::Command;
8use colored::*;
9
10#[derive(Debug, Default)]
12pub struct ValidationResult {
13 pub errors: Vec<ValidationError>,
14 pub warnings: Vec<ValidationWarning>,
15}
16
17#[derive(Debug)]
19pub struct ValidationError {
20 pub script: Option<String>,
21 pub message: String,
22}
23
24#[derive(Debug)]
26pub struct ValidationWarning {
27 pub script: Option<String>,
28 pub message: String,
29}
30
31impl ValidationResult {
32 pub fn is_valid(&self) -> bool {
34 self.errors.is_empty()
35 }
36
37 pub fn add_error(&mut self, script: Option<String>, message: String) {
39 self.errors.push(ValidationError { script, message });
40 }
41
42 pub fn add_warning(&mut self, script: Option<String>, message: String) {
44 self.warnings.push(ValidationWarning { script, message });
45 }
46}
47
48pub fn validate_scripts(scripts: &Scripts) -> ValidationResult {
63 let mut result = ValidationResult::default();
64 let script_names: HashSet<&String> = scripts.scripts.keys().collect();
65
66 for (script_name, script) in &scripts.scripts {
68 validate_script(script_name, script, &script_names, &mut result);
69 }
70
71 result
72}
73
74fn validate_script(
76 script_name: &str,
77 script: &Script,
78 available_scripts: &HashSet<&String>,
79 result: &mut ValidationResult,
80) {
81 match script {
82 Script::Default(_) => {
83 }
85 Script::Inline {
86 include,
87 requires,
88 toolchain,
89 ..
90 } | Script::CILike {
91 include,
92 requires,
93 toolchain,
94 ..
95 } => {
96 if let Some(includes) = include {
98 for include_name in includes {
99 if !available_scripts.contains(include_name) {
100 result.add_error(
101 Some(script_name.to_string()),
102 format!("Script '{}' references non-existent script '{}'", script_name, include_name),
103 );
104 }
105 }
106 }
107
108 if let Some(reqs) = requires {
110 for req in reqs {
111 validate_requirement(script_name, req, result);
112 }
113 }
114
115 if let Some(tc) = toolchain {
117 validate_toolchain(script_name, tc, result);
118 }
119 }
120 }
121}
122
123fn validate_requirement(script_name: &str, requirement: &str, result: &mut ValidationResult) {
125 if let Some((tool, version_req)) = requirement.split_once(' ') {
126 let output = Command::new(tool).arg("--version").output();
128 match output {
129 Ok(output_result) => {
130 let output_str = String::from_utf8_lossy(&output_result.stdout);
131 let version_line = output_str.lines().next().unwrap_or("");
132
133 if version_req.starts_with(">=") || version_req.starts_with("<=") ||
136 version_req.starts_with(">") || version_req.starts_with("<") {
137 result.add_warning(
140 Some(script_name.to_string()),
141 format!(
142 "Tool '{}' found (version: {}), but complex version requirement '{}' validation is limited",
143 tool,
144 version_line,
145 version_req
146 ),
147 );
148 } else if !version_line.contains(version_req) {
149 result.add_error(
151 Some(script_name.to_string()),
152 format!(
153 "Tool '{}' version requirement '{}' not met. Found: {}",
154 tool,
155 version_req,
156 version_line
157 ),
158 );
159 }
160 }
161 Err(_) => {
162 result.add_error(
163 Some(script_name.to_string()),
164 format!("Required tool '{}' is not installed or not in PATH", tool),
165 );
166 }
167 }
168 } else {
169 let output = Command::new(requirement).output();
171 if output.is_err() {
172 result.add_error(
173 Some(script_name.to_string()),
174 format!("Required tool '{}' is not installed or not in PATH", requirement),
175 );
176 }
177 }
178}
179
180fn validate_toolchain(script_name: &str, toolchain: &str, result: &mut ValidationResult) {
182 if toolchain.starts_with("python:") {
184 let python_version = toolchain.strip_prefix("python:").unwrap_or("");
185 let output = Command::new("python").arg("--version").output()
187 .or_else(|_| Command::new("python3").arg("--version").output());
188
189 match output {
190 Ok(output_result) => {
191 let output_str = String::from_utf8_lossy(&output_result.stdout);
192 if !output_str.contains(python_version) {
193 result.add_warning(
194 Some(script_name.to_string()),
195 format!(
196 "Python toolchain '{}' requirement: Python found ({}), but version '{}' not verified",
197 toolchain,
198 output_str.trim(),
199 python_version
200 ),
201 );
202 }
203 }
204 Err(_) => {
205 result.add_error(
206 Some(script_name.to_string()),
207 format!("Python toolchain '{}' required but Python is not installed or not in PATH", toolchain),
208 );
209 }
210 }
211 } else {
212 let output = Command::new("rustup")
214 .arg("toolchain")
215 .arg("list")
216 .output();
217
218 match output {
219 Ok(output_result) => {
220 let output_str = String::from_utf8_lossy(&output_result.stdout);
221 if !output_str.contains(toolchain) {
222 result.add_error(
223 Some(script_name.to_string()),
224 format!("Required Rust toolchain '{}' is not installed", toolchain),
225 );
226 }
227 }
228 Err(_) => {
229 result.add_error(
230 Some(script_name.to_string()),
231 "rustup is not installed or not in PATH".to_string(),
232 );
233 }
234 }
235 }
236}
237
238pub fn print_validation_results(result: &ValidationResult) {
240 if result.is_valid() && result.warnings.is_empty() {
241 println!("{}", "✓ All validations passed!".green().bold());
242 return;
243 }
244
245 if !result.errors.is_empty() {
246 println!("\n{}", "❌ Validation Errors:".red().bold());
247 for (idx, error) in result.errors.iter().enumerate() {
248 if let Some(script) = &error.script {
249 println!(
250 " {}. Script '{}': {}",
251 idx + 1,
252 script.bold().yellow(),
253 error.message.red()
254 );
255 } else {
256 println!(" {}. {}", idx + 1, error.message.red());
257 }
258 }
259 }
260
261 if !result.warnings.is_empty() {
262 println!("\n{}", "⚠️ Validation Warnings:".yellow().bold());
263 for (idx, warning) in result.warnings.iter().enumerate() {
264 if let Some(script) = &warning.script {
265 println!(
266 " {}. Script '{}': {}",
267 idx + 1,
268 script.bold().yellow(),
269 warning.message.yellow()
270 );
271 } else {
272 println!(" {}. {}", idx + 1, warning.message.yellow());
273 }
274 }
275 }
276
277 println!();
278 if result.is_valid() {
279 println!("{}", "✓ Validation completed with warnings".green().bold());
280 } else {
281 println!(
282 "{}",
283 format!("✗ Found {} error(s)", result.errors.len()).red().bold()
284 );
285 }
286}
287