Skip to main content

lintel_validate/
lib.rs

1#![doc = include_str!("../README.md")]
2#![allow(unused_assignments)] // thiserror/miette derive macros trigger false positives
3
4extern crate alloc;
5
6use std::time::Instant;
7
8use anyhow::Result;
9use bpaf::Bpaf;
10
11use lintel_cli_common::CliCacheOptions;
12
13// -----------------------------------------------------------------------
14// Core validation modules
15// -----------------------------------------------------------------------
16
17pub mod catalog;
18pub mod diagnostics;
19pub mod discover;
20pub mod parsers;
21pub mod registry;
22pub mod reporter;
23pub mod validate;
24
25pub use reporter::Reporter;
26
27// -----------------------------------------------------------------------
28// ValidateArgs — shared CLI struct
29// -----------------------------------------------------------------------
30
31#[derive(Debug, Clone, Bpaf)]
32pub struct ValidateArgs {
33    #[bpaf(long("exclude"), argument("PATTERN"))]
34    pub exclude: Vec<String>,
35
36    #[bpaf(external(lintel_cli_common::cli_cache_options))]
37    pub cache: CliCacheOptions,
38
39    #[bpaf(positional("PATH"))]
40    pub globs: Vec<String>,
41}
42
43impl From<&ValidateArgs> for validate::ValidateArgs {
44    fn from(args: &ValidateArgs) -> Self {
45        // When a single directory is passed as an arg, use it as the config
46        // search directory so that `lintel.toml` inside that directory is found.
47        let config_dir = args
48            .globs
49            .iter()
50            .find(|g| std::path::Path::new(g).is_dir())
51            .map(std::path::PathBuf::from);
52
53        validate::ValidateArgs {
54            globs: args.globs.clone(),
55            exclude: args.exclude.clone(),
56            cache_dir: args.cache.cache_dir.clone(),
57            force_schema_fetch: args.cache.force_schema_fetch || args.cache.force,
58            force_validation: args.cache.force_validation || args.cache.force,
59            no_catalog: args.cache.no_catalog,
60            config_dir,
61            schema_cache_ttl: args.cache.schema_cache_ttl,
62        }
63    }
64}
65
66// -----------------------------------------------------------------------
67// Helpers
68// -----------------------------------------------------------------------
69
70/// Format a verbose line for a checked file, including cache status tags.
71pub fn format_checked_verbose(file: &validate::CheckedFile) -> String {
72    use lintel_schema_cache::CacheStatus;
73    use lintel_validation_cache::ValidationCacheStatus;
74
75    let schema_tag = match file.cache_status {
76        Some(CacheStatus::Hit) => " [cached]",
77        Some(CacheStatus::Miss | CacheStatus::Disabled) => " [fetched]",
78        None => "",
79    };
80    let validation_tag = match file.validation_cache_status {
81        Some(ValidationCacheStatus::Hit) => " [validated:cached]",
82        Some(ValidationCacheStatus::Miss) => " [validated]",
83        None => "",
84    };
85    format!(
86        "  {} ({}){schema_tag}{validation_tag}",
87        file.path, file.schema
88    )
89}
90
91/// Load `lintel.toml` and merge its excludes into the args.
92///
93/// Config excludes are prepended so they have the same priority as CLI excludes.
94/// When a directory arg is passed (e.g. `lintel check some/dir`), we search
95/// for `lintel.toml` starting from that directory rather than cwd.
96pub fn merge_config(args: &mut ValidateArgs) {
97    let search_dir = args
98        .globs
99        .iter()
100        .find(|g| std::path::Path::new(g).is_dir())
101        .map(std::path::PathBuf::from);
102
103    let cfg_result = match &search_dir {
104        Some(dir) => lintel_config::find_and_load(dir).map(Option::unwrap_or_default),
105        None => lintel_config::load(),
106    };
107
108    match cfg_result {
109        Ok(cfg) => {
110            // Config excludes first, then CLI excludes.
111            let cli_excludes = core::mem::take(&mut args.exclude);
112            args.exclude = cfg.exclude;
113            args.exclude.extend(cli_excludes);
114        }
115        Err(e) => {
116            eprintln!("warning: failed to load lintel.toml: {e}");
117        }
118    }
119}
120
121// -----------------------------------------------------------------------
122// Run function — shared between check/ci/validate commands
123// -----------------------------------------------------------------------
124
125/// Run validation and report results via the given reporter.
126///
127/// Returns `true` if there were validation errors, `false` if clean.
128///
129/// # Errors
130///
131/// Returns an error if file collection or schema validation encounters an I/O error.
132pub async fn run(args: &mut ValidateArgs, reporter: &mut dyn Reporter) -> Result<bool> {
133    merge_config(args);
134
135    let lib_args = validate::ValidateArgs::from(&*args);
136    let start = Instant::now();
137    let result = validate::run_with(&lib_args, None, |file| {
138        reporter.on_file_checked(file);
139    })
140    .await?;
141    let had_errors = result.has_errors();
142    let elapsed = start.elapsed();
143
144    reporter.report(result, elapsed);
145
146    Ok(had_errors)
147}