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