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}