agpm_cli/cli/validate/
executor.rs

1//! Validation execution logic and orchestration.
2
3use anyhow::Result;
4use colored::Colorize;
5use std::path::PathBuf;
6
7use crate::manifest::find_manifest_with_optional;
8
9use super::command::{OutputFormat, ValidateCommand};
10use super::results::ValidationResults;
11use super::validators;
12
13impl ValidateCommand {
14    /// Execute the validate command to check project configuration.
15    ///
16    /// This method orchestrates the complete validation process, performing
17    /// checks according to the specified options and outputting results in
18    /// the requested format.
19    ///
20    /// # Validation Process
21    ///
22    /// 1. **Manifest Loading**: Locates and loads the manifest file
23    /// 2. **Basic Validation**: Checks syntax and required fields
24    /// 3. **Extended Checks**: Performs optional network and dependency checks
25    /// 4. **Result Compilation**: Aggregates all validation results
26    /// 5. **Output Generation**: Formats and displays results
27    /// 6. **Exit Code**: Returns success/failure based on results and strict mode
28    ///
29    /// # Validation Ordering
30    ///
31    /// Validations are performed in this order to provide early feedback:
32    /// 1. Manifest structure and syntax
33    /// 2. Dependency resolution (if `--resolve`)
34    /// 3. Source accessibility (if `--sources`)
35    /// 4. Local path validation (if `--paths`)
36    /// 5. Lockfile consistency (if `--check-lock`)
37    ///
38    /// # Returns
39    ///
40    /// - `Ok(())` if validation passes (or in strict mode, no warnings)
41    /// - `Err(anyhow::Error)` if:
42    ///   - Manifest file is not found
43    ///   - Manifest has syntax errors
44    ///   - Critical validation failures occur
45    ///   - Strict mode is enabled and warnings are present
46    ///
47    /// # Examples
48    ///
49    /// ```ignore
50    /// use agpm_cli::cli::validate::{ValidateCommand, OutputFormat};
51    ///
52    /// let cmd = ValidateCommand {
53    ///     file: None,
54    ///     resolve: true,
55    ///     check_lock: true,
56    ///     sources: false,
57    ///     paths: true,
58    ///     format: OutputFormat::Text,
59    ///     verbose: true,
60    ///     quiet: false,
61    ///     strict: false,
62    ///     render: false,
63    /// };
64    /// // cmd.execute().await?;
65    /// ```
66    pub async fn execute(self) -> Result<()> {
67        self.execute_with_manifest_path(None).await
68    }
69
70    /// Execute the validate command with an optional manifest path.
71    ///
72    /// This method performs validation of the agpm.toml manifest file and optionally
73    /// the associated lockfile. It can validate manifest syntax, source availability,
74    /// and dependency resolution consistency.
75    ///
76    /// # Arguments
77    ///
78    /// * `manifest_path` - Optional path to the agpm.toml file. If None, searches
79    ///   for agpm.toml in current directory and parent directories. If the command
80    ///   has a `file` field set, that takes precedence.
81    ///
82    /// # Returns
83    ///
84    /// - `Ok(())` if validation passes
85    /// - `Err(anyhow::Error)` if validation fails or manifest is invalid
86    ///
87    /// # Examples
88    ///
89    /// ```ignore
90    /// use agpm_cli::cli::validate::ValidateCommand;
91    /// use std::path::PathBuf;
92    ///
93    /// let cmd = ValidateCommand {
94    ///     file: None,
95    ///     check_lock: false,
96    ///     resolve: false,
97    ///     format: OutputFormat::Text,
98    ///     json: false,
99    ///     paths: false,
100    ///     fix: false,
101    /// };
102    ///
103    /// cmd.execute_with_manifest_path(Some(PathBuf::from("./agpm.toml"))).await?;
104    /// ```
105    pub async fn execute_with_manifest_path(self, manifest_path: Option<PathBuf>) -> Result<()> {
106        // Find or use specified manifest file
107        let manifest_path = if let Some(ref path) = self.file {
108            PathBuf::from(path)
109        } else {
110            match find_manifest_with_optional(manifest_path) {
111                Ok(path) => path,
112                Err(e) => {
113                    let error_msg =
114                        "No agpm.toml found in current directory or any parent directory";
115
116                    if matches!(self.format, OutputFormat::Json) {
117                        let validation_results = ValidationResults {
118                            valid: false,
119                            errors: vec![error_msg.to_string()],
120                            ..Default::default()
121                        };
122                        println!("{}", serde_json::to_string_pretty(&validation_results)?);
123                        return Err(e);
124                    } else if !self.quiet {
125                        println!("{} {}", "✗".red(), error_msg);
126                    }
127                    return Err(e);
128                }
129            }
130        };
131
132        self.execute_from_path(manifest_path).await
133    }
134
135    /// Executes validation using a specific manifest path
136    ///
137    /// This method performs the same validation as `execute()` but accepts
138    /// an explicit manifest path instead of searching for it.
139    ///
140    /// # Arguments
141    ///
142    /// * `manifest_path` - Path to the manifest file to validate
143    ///
144    /// # Returns
145    ///
146    /// Returns `Ok(())` if validation succeeds
147    ///
148    /// # Errors
149    ///
150    /// Returns an error if:
151    /// - The manifest file doesn't exist
152    /// - The manifest has syntax errors
153    /// - Sources are invalid or unreachable (with --resolve flag)
154    /// - Dependencies have conflicts
155    pub async fn execute_from_path(self, manifest_path: PathBuf) -> Result<()> {
156        // For consistency with execute(), require the manifest to exist
157        if !manifest_path.exists() {
158            let error_msg = format!("Manifest file {} not found", manifest_path.display());
159
160            if matches!(self.format, OutputFormat::Json) {
161                let validation_results = ValidationResults {
162                    valid: false,
163                    errors: vec![error_msg],
164                    ..Default::default()
165                };
166                println!("{}", serde_json::to_string_pretty(&validation_results)?);
167            } else if !self.quiet {
168                println!("{} {}", "✗".red(), error_msg);
169            }
170
171            return Err(anyhow::anyhow!("Manifest file {} not found", manifest_path.display()));
172        }
173
174        // Validation results for JSON output
175        let mut validation_results = ValidationResults::default();
176        let mut warnings = Vec::new();
177        let mut errors = Vec::new();
178
179        // Load and validate manifest structure
180        let manifest = validators::validate_manifest(
181            &manifest_path,
182            &self.format,
183            self.verbose,
184            self.quiet,
185            &mut validation_results,
186            &mut warnings,
187            &mut errors,
188        )
189        .await?;
190
191        // Check if dependencies can be resolved
192        if self.resolve {
193            validators::validate_dependencies(
194                &manifest,
195                &self.format,
196                self.verbose,
197                self.quiet,
198                &mut validation_results,
199                &mut warnings,
200                &mut errors,
201            )
202            .await?;
203        }
204
205        // Check if sources are accessible
206        if self.sources {
207            validators::validate_sources(
208                &manifest,
209                &self.format,
210                self.verbose,
211                self.quiet,
212                &mut validation_results,
213                &mut warnings,
214                &mut errors,
215            )
216            .await?;
217        }
218
219        // Check local file paths
220        if self.paths {
221            let mut ctx = validators::ValidationContext::new(
222                &manifest,
223                &self.format,
224                self.verbose,
225                self.quiet,
226                &mut validation_results,
227                &mut warnings,
228                &mut errors,
229            );
230            validators::validate_paths(&mut ctx, &manifest_path).await?;
231        }
232
233        // Check lockfile consistency
234        if self.check_lock {
235            let project_dir = manifest_path.parent().unwrap();
236            let mut ctx = validators::ValidationContext::new(
237                &manifest,
238                &self.format,
239                self.verbose,
240                self.quiet,
241                &mut validation_results,
242                &mut warnings,
243                &mut errors,
244            );
245            validators::validate_lockfile(&mut ctx, project_dir).await?;
246        }
247
248        // Validate template rendering if requested
249        if self.render {
250            let project_dir = manifest_path.parent().unwrap();
251            let mut ctx = validators::ValidationContext::new(
252                &manifest,
253                &self.format,
254                self.verbose,
255                self.quiet,
256                &mut validation_results,
257                &mut warnings,
258                &mut errors,
259            );
260            validators::validate_templates(&mut ctx, project_dir).await?;
261        }
262
263        // Handle strict mode - treat warnings as errors
264        if self.strict && !warnings.is_empty() {
265            let error_msg = "Strict mode: Warnings treated as errors";
266            errors.extend(warnings.clone());
267
268            if matches!(self.format, OutputFormat::Json) {
269                validation_results.valid = false;
270                validation_results.errors = errors;
271                println!("{}", serde_json::to_string_pretty(&validation_results)?);
272                return Err(anyhow::anyhow!("Strict mode validation failed"));
273            } else if !self.quiet {
274                println!("{} {}", "✗".red(), error_msg);
275            }
276            return Err(anyhow::anyhow!("Strict mode validation failed"));
277        }
278
279        // Set final validation status
280        validation_results.valid = errors.is_empty();
281        validation_results.errors = errors;
282        validation_results.warnings = warnings;
283
284        // Output results
285        match self.format {
286            OutputFormat::Json => {
287                println!("{}", serde_json::to_string_pretty(&validation_results)?);
288            }
289            OutputFormat::Text => {
290                if !self.quiet && !validation_results.warnings.is_empty() {
291                    for warning in &validation_results.warnings {
292                        println!("⚠ Warning: {warning}");
293                    }
294                }
295                // Individual validation steps already printed their success messages
296            }
297        }
298
299        Ok(())
300    }
301}