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}