hedl_cli/commands/batch_commands.rs
1// Dweve HEDL - Hierarchical Entity Data Language
2//
3// Copyright (c) 2025 Dweve IP B.V. and individual contributors.
4//
5// SPDX-License-Identifier: Apache-2.0
6//
7// Licensed under the Apache License, Version 2.0 (the "License");
8// you may not use this file except in compliance with the License.
9// You may obtain a copy of the License in the LICENSE file at the
10// root of this repository or at: http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Batch command implementations - Process multiple HEDL files efficiently
19//!
20//! This module provides batch processing capabilities for validating, formatting,
21//! and linting multiple HEDL files in parallel or sequentially.
22
23use crate::batch::{BatchConfig, BatchProcessor, FormatOperation, LintOperation, ValidationOperation};
24use crate::error::CliError;
25use colored::Colorize;
26use std::path::PathBuf;
27
28/// Batch validate multiple HEDL files.
29///
30/// Validates multiple HEDL files for syntax and structural correctness, with
31/// optional parallel processing for improved performance on large file sets.
32///
33/// # Arguments
34///
35/// * `files` - List of file paths to validate
36/// * `strict` - If `true`, enables strict reference validation for all files
37/// * `parallel` - If `true`, processes files in parallel (automatically enabled for 4+ files)
38/// * `verbose` - If `true`, shows detailed progress information
39///
40/// # Returns
41///
42/// Returns `Ok(())` if all files are valid, `Err` with a summary if any fail.
43///
44/// # Errors
45///
46/// Returns `Err` if:
47/// - Any file cannot be read
48/// - Any file contains syntax errors
49/// - In strict mode, if any references cannot be resolved
50///
51/// # Examples
52///
53/// ```no_run
54/// use hedl_cli::commands::batch_validate;
55///
56/// # fn main() -> Result<(), String> {
57/// // Validate multiple files in parallel
58/// let files = vec!["file1.hedl".to_string(), "file2.hedl".to_string()];
59/// batch_validate(files, false, true, false)?;
60///
61/// // Strict validation with verbose output
62/// let files = vec!["test1.hedl".to_string(), "test2.hedl".to_string()];
63/// batch_validate(files, true, true, true)?;
64/// # Ok(())
65/// # }
66/// ```
67///
68/// # Output
69///
70/// Displays progress information and a summary:
71/// - Success/failure for each file (✓ or ✗)
72/// - Detailed error messages for failures
73/// - Final count of failures
74///
75/// # Performance
76///
77/// Automatically uses parallel processing when beneficial (4+ files by default).
78/// Can be forced with the `parallel` flag for smaller file sets.
79pub fn batch_validate(
80 files: Vec<String>,
81 strict: bool,
82 parallel: bool,
83 verbose: bool,
84) -> Result<(), String> {
85 let paths: Vec<PathBuf> = files.iter().map(PathBuf::from).collect();
86
87 let config = BatchConfig {
88 parallel_threshold: if parallel { 1 } else { usize::MAX },
89 verbose,
90 ..Default::default()
91 };
92
93 let processor = BatchProcessor::new(config);
94 let operation = ValidationOperation { strict };
95
96 let results = processor
97 .process(&paths, operation, true)
98 .map_err(|e: CliError| e.to_string())?;
99
100 if results.has_failures() {
101 eprintln!();
102 eprintln!("{}", "Validation failures:".red().bold());
103 for failure in results.failures() {
104 eprintln!(" {} {}", "✗".red(), failure.path.display());
105 if let Err(e) = &failure.result {
106 let e: &CliError = e;
107 eprintln!(" {}", e.to_string().dimmed());
108 }
109 }
110 return Err(format!(
111 "{} of {} files failed validation",
112 results.failure_count(),
113 results.total_files()
114 ));
115 }
116
117 Ok(())
118}
119
120/// Batch format multiple HEDL files to canonical form.
121///
122/// Formats multiple HEDL files to canonical form, with options for check-only mode,
123/// ditto optimization, and count hints. Supports parallel processing for improved
124/// performance on large file sets.
125///
126/// # Arguments
127///
128/// * `files` - List of file paths to format
129/// * `output_dir` - Optional output directory for formatted files. If `None`, files are processed in-place
130/// * `check` - If `true`, only checks if files are canonical without reformatting
131/// * `ditto` - If `true`, uses ditto optimization (repeated values as `"`)
132/// * `with_counts` - If `true`, automatically adds count hints to all matrix lists
133/// * `parallel` - If `true`, processes files in parallel (automatically enabled for 4+ files)
134/// * `verbose` - If `true`, shows detailed progress information
135///
136/// # Returns
137///
138/// Returns `Ok(())` if all files are successfully formatted, `Err` with a summary if any fail.
139///
140/// # Errors
141///
142/// Returns `Err` if:
143/// - Any file cannot be read
144/// - Any file contains syntax errors
145/// - Canonicalization fails for any file
146/// - In check mode, if any file is not already canonical
147/// - Output directory cannot be created
148/// - Formatted files cannot be written
149///
150/// # Examples
151///
152/// ```no_run
153/// use hedl_cli::commands::batch_format;
154///
155/// # fn main() -> Result<(), String> {
156/// // Format files to output directory
157/// let files = vec!["file1.hedl".to_string(), "file2.hedl".to_string()];
158/// batch_format(files, Some("formatted/".to_string()), false, true, false, true, false)?;
159///
160/// // Check if files are canonical
161/// let files = vec!["test1.hedl".to_string(), "test2.hedl".to_string()];
162/// batch_format(files, None, true, true, false, true, false)?;
163///
164/// // Format with count hints
165/// let files = vec!["data.hedl".to_string()];
166/// batch_format(files, Some("output/".to_string()), false, true, true, false, true)?;
167/// # Ok(())
168/// # }
169/// ```
170///
171/// # Output
172///
173/// Displays progress information and a summary:
174/// - Success/failure for each file (✓ or ✗)
175/// - Detailed error messages for failures
176/// - Final count of failures
177///
178/// # Performance
179///
180/// Automatically uses parallel processing when beneficial (4+ files by default).
181/// Can be forced with the `parallel` flag for smaller file sets.
182pub fn batch_format(
183 files: Vec<String>,
184 output_dir: Option<String>,
185 check: bool,
186 ditto: bool,
187 with_counts: bool,
188 parallel: bool,
189 verbose: bool,
190) -> Result<(), String> {
191 let paths: Vec<PathBuf> = files.iter().map(PathBuf::from).collect();
192
193 let config = BatchConfig {
194 parallel_threshold: if parallel { 1 } else { usize::MAX },
195 verbose,
196 ..Default::default()
197 };
198
199 let processor = BatchProcessor::new(config);
200 let operation = FormatOperation {
201 check,
202 ditto,
203 with_counts,
204 };
205
206 let results = processor
207 .process(&paths, operation, true)
208 .map_err(|e: CliError| e.to_string())?;
209
210 // If not in check mode and output_dir is specified, write formatted files
211 if !check && output_dir.is_some() {
212 let out_dir = output_dir.unwrap();
213 std::fs::create_dir_all(&out_dir)
214 .map_err(|e| format!("Failed to create output directory '{}': {}", out_dir, e))?;
215
216 for result in results.successes() {
217 if let Ok(formatted) = &result.result {
218 let output_path = PathBuf::from(&out_dir).join(
219 result
220 .path
221 .file_name()
222 .ok_or("Invalid file name")?,
223 );
224 std::fs::write(&output_path, formatted).map_err(|e| {
225 format!("Failed to write '{}': {}", output_path.display(), e)
226 })?;
227 }
228 }
229 }
230
231 if results.has_failures() {
232 eprintln!();
233 eprintln!("{}", "Format failures:".red().bold());
234 for failure in results.failures() {
235 eprintln!(" {} {}", "✗".red(), failure.path.display());
236 if let Err(e) = &failure.result {
237 let e: &CliError = e;
238 eprintln!(" {}", e.to_string().dimmed());
239 }
240 }
241 return Err(format!(
242 "{} of {} files failed formatting",
243 results.failure_count(),
244 results.total_files()
245 ));
246 }
247
248 Ok(())
249}
250
251/// Batch lint multiple HEDL files for best practices and style issues.
252///
253/// Lints multiple HEDL files for potential issues, style violations, and best
254/// practice deviations. Supports parallel processing for improved performance
255/// on large file sets.
256///
257/// # Arguments
258///
259/// * `files` - List of file paths to lint
260/// * `warn_error` - If `true`, treat warnings as errors (fail on any warning)
261/// * `parallel` - If `true`, processes files in parallel (automatically enabled for 4+ files)
262/// * `verbose` - If `true`, shows detailed progress information
263///
264/// # Returns
265///
266/// Returns `Ok(())` if no issues are found (or only hints), `Err` if errors or warnings
267/// (with `warn_error` enabled) are detected.
268///
269/// # Errors
270///
271/// Returns `Err` if:
272/// - Any file cannot be read
273/// - Any file contains syntax errors
274/// - Lint errors are found in any file
275/// - Warnings are found and `warn_error` is `true`
276///
277/// # Examples
278///
279/// ```no_run
280/// use hedl_cli::commands::batch_lint;
281///
282/// # fn main() -> Result<(), String> {
283/// // Lint multiple files
284/// let files = vec!["file1.hedl".to_string(), "file2.hedl".to_string()];
285/// batch_lint(files, false, true, false)?;
286///
287/// // Strict linting (warnings as errors)
288/// let files = vec!["test1.hedl".to_string(), "test2.hedl".to_string()];
289/// batch_lint(files, true, true, true)?;
290/// # Ok(())
291/// # }
292/// ```
293///
294/// # Output
295///
296/// Displays:
297/// - Progress information for each file
298/// - All lint diagnostics with severity, rule ID, message, and line number
299/// - Suggestions for fixing issues
300/// - Summary of total issues found
301///
302/// # Performance
303///
304/// Automatically uses parallel processing when beneficial (4+ files by default).
305/// Can be forced with the `parallel` flag for smaller file sets.
306pub fn batch_lint(
307 files: Vec<String>,
308 warn_error: bool,
309 parallel: bool,
310 verbose: bool,
311) -> Result<(), String> {
312 let paths: Vec<PathBuf> = files.iter().map(PathBuf::from).collect();
313
314 let config = BatchConfig {
315 parallel_threshold: if parallel { 1 } else { usize::MAX },
316 verbose,
317 ..Default::default()
318 };
319
320 let processor = BatchProcessor::new(config);
321 let operation = LintOperation { warn_error };
322
323 let results = processor
324 .process(&paths, operation, true)
325 .map_err(|e: CliError| e.to_string())?;
326
327 // Show lint diagnostics for files that have issues
328 let mut total_issues = 0;
329 for result in results.successes() {
330 if let Ok(diagnostics) = &result.result {
331 let diagnostics: &Vec<String> = diagnostics;
332 if !diagnostics.is_empty() {
333 total_issues += diagnostics.len();
334 println!();
335 println!("{} {}:", "Linting".yellow().bold(), result.path.display());
336 for diagnostic in diagnostics {
337 println!(" {}", diagnostic);
338 }
339 }
340 }
341 }
342
343 if results.has_failures() {
344 eprintln!();
345 eprintln!("{}", "Lint failures:".red().bold());
346 for failure in results.failures() {
347 eprintln!(" {} {}", "✗".red(), failure.path.display());
348 if let Err(e) = &failure.result {
349 let e: &CliError = e;
350 eprintln!(" {}", e.to_string().dimmed());
351 }
352 }
353 return Err(format!(
354 "{} of {} files failed linting",
355 results.failure_count(),
356 results.total_files()
357 ));
358 }
359
360 if total_issues > 0 {
361 println!();
362 println!(
363 "{} {} issues found across {} files",
364 "Summary:".bright_blue().bold(),
365 total_issues,
366 results.total_files()
367 );
368 }
369
370 Ok(())
371}