cargo_run/commands/
validate.rs

1//! This module provides validation functionality for Scripts.toml files.
2//!
3//! It validates syntax, script references, and tool requirements.
4
5use crate::commands::script::{Scripts, Script};
6use std::collections::HashSet;
7use std::process::Command;
8use colored::*;
9
10/// Validation result containing all issues found.
11#[derive(Debug, Default)]
12pub struct ValidationResult {
13    pub errors: Vec<ValidationError>,
14    pub warnings: Vec<ValidationWarning>,
15}
16
17/// Validation error with context.
18#[derive(Debug)]
19pub struct ValidationError {
20    pub script: Option<String>,
21    pub message: String,
22}
23
24/// Validation warning with context.
25#[derive(Debug)]
26pub struct ValidationWarning {
27    pub script: Option<String>,
28    pub message: String,
29}
30
31impl ValidationResult {
32    /// Check if validation passed (no errors).
33    pub fn is_valid(&self) -> bool {
34        self.errors.is_empty()
35    }
36
37    /// Add an error to the validation result.
38    pub fn add_error(&mut self, script: Option<String>, message: String) {
39        self.errors.push(ValidationError { script, message });
40    }
41
42    /// Add a warning to the validation result.
43    pub fn add_warning(&mut self, script: Option<String>, message: String) {
44        self.warnings.push(ValidationWarning { script, message });
45    }
46}
47
48/// Validate the Scripts.toml file.
49///
50/// This function performs comprehensive validation including:
51/// - Script reference validation (includes)
52/// - Tool requirement checking
53/// - Toolchain validation
54///
55/// # Arguments
56///
57/// * `scripts` - The parsed Scripts collection to validate
58///
59/// # Returns
60///
61/// A ValidationResult containing all errors and warnings found.
62pub fn validate_scripts(scripts: &Scripts) -> ValidationResult {
63    let mut result = ValidationResult::default();
64    let script_names: HashSet<&String> = scripts.scripts.keys().collect();
65
66    // Validate each script
67    for (script_name, script) in &scripts.scripts {
68        validate_script(script_name, script, &script_names, &mut result);
69    }
70
71    result
72}
73
74/// Validate a single script.
75fn 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            // Simple scripts don't need validation beyond syntax
84        }
85        Script::Inline {
86            include,
87            requires,
88            toolchain,
89            ..
90        } | Script::CILike {
91            include,
92            requires,
93            toolchain,
94            ..
95        } => {
96            // Validate includes
97            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            // Validate tool requirements
109            if let Some(reqs) = requires {
110                for req in reqs {
111                    validate_requirement(script_name, req, result);
112                }
113            }
114
115            // Validate toolchain
116            if let Some(tc) = toolchain {
117                validate_toolchain(script_name, tc, result);
118            }
119        }
120    }
121}
122
123/// Validate a tool requirement.
124fn validate_requirement(script_name: &str, requirement: &str, result: &mut ValidationResult) {
125    if let Some((tool, version_req)) = requirement.split_once(' ') {
126        // Check if tool exists
127        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                // For simple version checks (contains), use substring matching
134                // For complex version checks (>=, <=, etc.), we'll do basic validation
135                if version_req.starts_with(">=") || version_req.starts_with("<=") || 
136                   version_req.starts_with(">") || version_req.starts_with("<") {
137                    // Complex version comparison - for now, just check if tool exists
138                    // Full semantic version comparison would require a version parsing library
139                    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                    // Simple version check (contains)
150                    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        // Just check if tool exists
170        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
180/// Validate a toolchain requirement.
181fn validate_toolchain(script_name: &str, toolchain: &str, result: &mut ValidationResult) {
182    // Check if it's a Python toolchain (python:X.Y format)
183    if toolchain.starts_with("python:") {
184        let python_version = toolchain.strip_prefix("python:").unwrap_or("");
185        // Check if Python is installed
186        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        // Rust toolchain validation via rustup
213        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
238/// Print validation results in a user-friendly format.
239pub 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