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}