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::{
24 BatchConfig, BatchProcessor, FormatOperation, LintOperation, ValidationOperation,
25};
26use crate::error::CliError;
27use crate::file_discovery::{DiscoveryConfig, FileDiscovery};
28use colored::Colorize;
29use std::path::PathBuf;
30
31/// Batch validate multiple HEDL files.
32///
33/// Validates multiple HEDL files for syntax and structural correctness, with
34/// optional parallel processing for improved performance on large file sets.
35///
36/// # Arguments
37///
38/// * `patterns` - List of file patterns (glob patterns or explicit paths)
39/// * `strict` - If `true`, enables strict reference validation for all files
40/// * `recursive` - Enable recursive directory traversal
41/// * `max_depth` - Maximum recursion depth
42/// * `parallel` - If `true`, processes files in parallel (automatically enabled for 4+ files)
43/// * `verbose` - If `true`, shows detailed progress information
44///
45/// # Returns
46///
47/// Returns `Ok(())` if all files are valid, `Err` with a summary if any fail.
48///
49/// # Errors
50///
51/// Returns `Err` if:
52/// - Any file cannot be read
53/// - Any file contains syntax errors
54/// - In strict mode, if any references cannot be resolved
55///
56/// # Examples
57///
58/// ```no_run
59/// use hedl_cli::commands::batch_validate;
60///
61/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
62/// // Validate multiple files in parallel
63/// let patterns = vec!["*.hedl".to_string()];
64/// batch_validate(patterns, false, false, 10, true, false)?;
65///
66/// // Strict validation with verbose output
67/// let patterns = vec!["**/*.hedl".to_string()];
68/// batch_validate(patterns, true, true, 10, true, true)?;
69/// # Ok(())
70/// # }
71/// ```
72///
73/// # Output
74///
75/// Displays progress information and a summary:
76/// - Success/failure for each file (✓ or ✗)
77/// - Detailed error messages for failures
78/// - Final count of failures
79///
80/// # Performance
81///
82/// Automatically uses parallel processing when beneficial (4+ files by default).
83/// Can be forced with the `parallel` flag for smaller file sets.
84pub fn batch_validate(
85 patterns: Vec<String>,
86 strict: bool,
87 recursive: bool,
88 max_depth: usize,
89 parallel: bool,
90 verbose: bool,
91) -> Result<(), CliError> {
92 batch_validate_with_config(
93 patterns, strict, recursive, max_depth, parallel, verbose, None,
94 )
95}
96
97/// Batch validate with custom configuration.
98///
99/// Like `batch_validate`, but allows overriding the max files limit.
100pub fn batch_validate_with_config(
101 patterns: Vec<String>,
102 strict: bool,
103 recursive: bool,
104 max_depth: usize,
105 parallel: bool,
106 verbose: bool,
107 max_files_override: Option<Option<usize>>,
108) -> Result<(), CliError> {
109 // Discover files from patterns
110 let discovery_config = DiscoveryConfig {
111 max_depth: Some(max_depth),
112 extension: Some("hedl".to_string()),
113 recursive,
114 ..Default::default()
115 };
116
117 let discovery = FileDiscovery::new(patterns, discovery_config);
118 let paths = discovery.discover()?;
119
120 let mut config = BatchConfig {
121 parallel_threshold: if parallel { 1 } else { usize::MAX },
122 verbose,
123 ..Default::default()
124 };
125
126 // Apply CLI override if provided
127 if let Some(override_limit) = max_files_override {
128 config.max_files = override_limit;
129 }
130
131 // Validate file count against limit
132 crate::batch::validate_file_count(paths.len(), config.max_files)?;
133
134 // Warn if processing many files
135 crate::batch::warn_large_batch(paths.len(), verbose);
136
137 let processor = BatchProcessor::new(config);
138 let operation = ValidationOperation { strict };
139
140 let results = processor.process(&paths, operation, true)?;
141
142 if results.has_failures() {
143 eprintln!();
144 eprintln!("{}", "Validation failures:".red().bold());
145 for failure in results.failures() {
146 eprintln!(" {} {}", "✗".red(), failure.path.display());
147 if let Err(e) = &failure.result {
148 let e: &CliError = e;
149 eprintln!(" {}", e.to_string().dimmed());
150 }
151 }
152 return Err(CliError::invalid_input(format!(
153 "{} of {} files failed validation",
154 results.failure_count(),
155 results.total_files()
156 )));
157 }
158
159 Ok(())
160}
161
162/// Batch format multiple HEDL files to canonical form.
163///
164/// Formats multiple HEDL files to canonical form, with options for check-only mode,
165/// ditto optimization, and count hints. Supports parallel processing for improved
166/// performance on large file sets.
167///
168/// # Arguments
169///
170/// * `patterns` - List of file patterns (glob patterns or explicit paths)
171/// * `output_dir` - Optional output directory for formatted files. If `None`, files are processed in-place
172/// * `check` - If `true`, only checks if files are canonical without reformatting
173/// * `ditto` - If `true`, uses ditto optimization (repeated values as `"`)
174/// * `with_counts` - If `true`, automatically adds count hints to all matrix lists
175/// * `recursive` - Enable recursive directory traversal
176/// * `max_depth` - Maximum recursion depth
177/// * `parallel` - If `true`, processes files in parallel (automatically enabled for 4+ files)
178/// * `verbose` - If `true`, shows detailed progress information
179///
180/// # Returns
181///
182/// Returns `Ok(())` if all files are successfully formatted, `Err` with a summary if any fail.
183///
184/// # Errors
185///
186/// Returns `Err` if:
187/// - Any file cannot be read
188/// - Any file contains syntax errors
189/// - Canonicalization fails for any file
190/// - In check mode, if any file is not already canonical
191/// - Output directory cannot be created
192/// - Formatted files cannot be written
193///
194/// # Examples
195///
196/// ```no_run
197/// use hedl_cli::commands::batch_format;
198///
199/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
200/// // Format files to output directory
201/// let patterns = vec!["*.hedl".to_string()];
202/// batch_format(patterns, Some("formatted/".to_string()), false, true, false, false, 10, true, false)?;
203///
204/// // Check if files are canonical
205/// let patterns = vec!["**/*.hedl".to_string()];
206/// batch_format(patterns, None, true, true, false, true, 10, true, false)?;
207/// # Ok(())
208/// # }
209/// ```
210///
211/// # Output
212///
213/// Displays progress information and a summary:
214/// - Success/failure for each file (✓ or ✗)
215/// - Detailed error messages for failures
216/// - Final count of failures
217///
218/// # Performance
219///
220/// Automatically uses parallel processing when beneficial (4+ files by default).
221/// Can be forced with the `parallel` flag for smaller file sets.
222#[allow(clippy::too_many_arguments)]
223pub fn batch_format(
224 patterns: Vec<String>,
225 output_dir: Option<String>,
226 check: bool,
227 ditto: bool,
228 with_counts: bool,
229 recursive: bool,
230 max_depth: usize,
231 parallel: bool,
232 verbose: bool,
233) -> Result<(), CliError> {
234 batch_format_with_config(
235 patterns,
236 output_dir,
237 check,
238 ditto,
239 with_counts,
240 recursive,
241 max_depth,
242 parallel,
243 verbose,
244 None,
245 )
246}
247
248/// Batch format with custom configuration.
249///
250/// Like `batch_format`, but allows overriding the max files limit.
251#[allow(clippy::too_many_arguments)]
252pub fn batch_format_with_config(
253 patterns: Vec<String>,
254 output_dir: Option<String>,
255 check: bool,
256 ditto: bool,
257 with_counts: bool,
258 recursive: bool,
259 max_depth: usize,
260 parallel: bool,
261 verbose: bool,
262 max_files_override: Option<Option<usize>>,
263) -> Result<(), CliError> {
264 // Discover files from patterns
265 let discovery_config = DiscoveryConfig {
266 max_depth: Some(max_depth),
267 extension: Some("hedl".to_string()),
268 recursive,
269 ..Default::default()
270 };
271
272 let discovery = FileDiscovery::new(patterns, discovery_config);
273 let paths = discovery.discover()?;
274
275 let mut config = BatchConfig {
276 parallel_threshold: if parallel { 1 } else { usize::MAX },
277 verbose,
278 ..Default::default()
279 };
280
281 // Apply CLI override if provided
282 if let Some(override_limit) = max_files_override {
283 config.max_files = override_limit;
284 }
285
286 // Validate file count against limit
287 crate::batch::validate_file_count(paths.len(), config.max_files)?;
288
289 // Warn if processing many files
290 crate::batch::warn_large_batch(paths.len(), verbose);
291
292 let processor = BatchProcessor::new(config);
293 let operation = FormatOperation {
294 check,
295 ditto,
296 with_counts,
297 };
298
299 let results = processor.process(&paths, operation, true)?;
300
301 // If not in check mode and output_dir is specified, write formatted files
302 if !check {
303 if let Some(out_dir) = output_dir {
304 std::fs::create_dir_all(&out_dir).map_err(|e| CliError::io_error(&out_dir, e))?;
305
306 for result in results.successes() {
307 if let Ok(formatted) = &result.result {
308 let output_path = PathBuf::from(&out_dir).join(
309 result
310 .path
311 .file_name()
312 .ok_or_else(|| CliError::invalid_input("Invalid file name"))?,
313 );
314 std::fs::write(&output_path, formatted)
315 .map_err(|e| CliError::io_error(&output_path, e))?;
316 }
317 }
318 }
319 }
320
321 if results.has_failures() {
322 eprintln!();
323 eprintln!("{}", "Format failures:".red().bold());
324 for failure in results.failures() {
325 eprintln!(" {} {}", "✗".red(), failure.path.display());
326 if let Err(e) = &failure.result {
327 let e: &CliError = e;
328 eprintln!(" {}", e.to_string().dimmed());
329 }
330 }
331 return Err(CliError::invalid_input(format!(
332 "{} of {} files failed formatting",
333 results.failure_count(),
334 results.total_files()
335 )));
336 }
337
338 Ok(())
339}
340
341/// Batch lint multiple HEDL files for best practices and style issues.
342///
343/// Lints multiple HEDL files for potential issues, style violations, and best
344/// practice deviations. Supports parallel processing for improved performance
345/// on large file sets.
346///
347/// # Arguments
348///
349/// * `patterns` - List of file patterns (glob patterns or explicit paths)
350/// * `warn_error` - If `true`, treat warnings as errors (fail on any warning)
351/// * `recursive` - Enable recursive directory traversal
352/// * `max_depth` - Maximum recursion depth
353/// * `parallel` - If `true`, processes files in parallel (automatically enabled for 4+ files)
354/// * `verbose` - If `true`, shows detailed progress information
355///
356/// # Returns
357///
358/// Returns `Ok(())` if no issues are found (or only hints), `Err` if errors or warnings
359/// (with `warn_error` enabled) are detected.
360///
361/// # Errors
362///
363/// Returns `Err` if:
364/// - Any file cannot be read
365/// - Any file contains syntax errors
366/// - Lint errors are found in any file
367/// - Warnings are found and `warn_error` is `true`
368///
369/// # Examples
370///
371/// ```no_run
372/// use hedl_cli::commands::batch_lint;
373///
374/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
375/// // Lint multiple files
376/// let patterns = vec!["*.hedl".to_string()];
377/// batch_lint(patterns, false, false, 10, true, false)?;
378///
379/// // Strict linting (warnings as errors)
380/// let patterns = vec!["**/*.hedl".to_string()];
381/// batch_lint(patterns, true, true, 10, true, true)?;
382/// # Ok(())
383/// # }
384/// ```
385///
386/// # Output
387///
388/// Displays:
389/// - Progress information for each file
390/// - All lint diagnostics with severity, rule ID, message, and line number
391/// - Suggestions for fixing issues
392/// - Summary of total issues found
393///
394/// # Performance
395///
396/// Automatically uses parallel processing when beneficial (4+ files by default).
397/// Can be forced with the `parallel` flag for smaller file sets.
398pub fn batch_lint(
399 patterns: Vec<String>,
400 warn_error: bool,
401 recursive: bool,
402 max_depth: usize,
403 parallel: bool,
404 verbose: bool,
405) -> Result<(), CliError> {
406 batch_lint_with_config(
407 patterns, warn_error, recursive, max_depth, parallel, verbose, None,
408 )
409}
410
411/// Batch lint with custom configuration.
412///
413/// Like `batch_lint`, but allows overriding the max files limit.
414pub fn batch_lint_with_config(
415 patterns: Vec<String>,
416 warn_error: bool,
417 recursive: bool,
418 max_depth: usize,
419 parallel: bool,
420 verbose: bool,
421 max_files_override: Option<Option<usize>>,
422) -> Result<(), CliError> {
423 // Discover files from patterns
424 let discovery_config = DiscoveryConfig {
425 max_depth: Some(max_depth),
426 extension: Some("hedl".to_string()),
427 recursive,
428 ..Default::default()
429 };
430
431 let discovery = FileDiscovery::new(patterns, discovery_config);
432 let paths = discovery.discover()?;
433
434 let mut config = BatchConfig {
435 parallel_threshold: if parallel { 1 } else { usize::MAX },
436 verbose,
437 ..Default::default()
438 };
439
440 // Apply CLI override if provided
441 if let Some(override_limit) = max_files_override {
442 config.max_files = override_limit;
443 }
444
445 // Validate file count against limit
446 crate::batch::validate_file_count(paths.len(), config.max_files)?;
447
448 // Warn if processing many files
449 crate::batch::warn_large_batch(paths.len(), verbose);
450
451 let processor = BatchProcessor::new(config);
452 let operation = LintOperation { warn_error };
453
454 let results = processor.process(&paths, operation, true)?;
455
456 // Show lint diagnostics for files that have issues
457 let mut total_issues = 0;
458 for result in results.successes() {
459 if let Ok(diagnostics) = &result.result {
460 let diagnostics: &Vec<String> = diagnostics;
461 if !diagnostics.is_empty() {
462 total_issues += diagnostics.len();
463 println!();
464 println!("{} {}:", "Linting".yellow().bold(), result.path.display());
465 for diagnostic in diagnostics {
466 println!(" {diagnostic}");
467 }
468 }
469 }
470 }
471
472 if results.has_failures() {
473 eprintln!();
474 eprintln!("{}", "Lint failures:".red().bold());
475 for failure in results.failures() {
476 eprintln!(" {} {}", "✗".red(), failure.path.display());
477 if let Err(e) = &failure.result {
478 let e: &CliError = e;
479 eprintln!(" {}", e.to_string().dimmed());
480 }
481 }
482 return Err(CliError::invalid_input(format!(
483 "{} of {} files failed linting",
484 results.failure_count(),
485 results.total_files()
486 )));
487 }
488
489 if total_issues > 0 {
490 println!();
491 println!(
492 "{} {} issues found across {} files",
493 "Summary:".bright_blue().bold(),
494 total_issues,
495 results.total_files()
496 );
497 }
498
499 Ok(())
500}