Skip to main content

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}