Skip to main content

lintel_identify/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::path::Path;
4
5use anyhow::{Context, Result};
6use bpaf::{Bpaf, ShellComp};
7use lintel_cli_common::{CLIGlobalOptions, CliCacheOptions};
8
9use lintel_explain::resolve::ResolvedFileSchema;
10
11// ---------------------------------------------------------------------------
12// CLI args
13// ---------------------------------------------------------------------------
14
15#[derive(Debug, Clone, Bpaf)]
16#[bpaf(generate(identify_args_inner))]
17#[allow(clippy::struct_excessive_bools)]
18pub struct IdentifyArgs {
19    /// Show detailed schema documentation
20    #[bpaf(long("explain"), switch)]
21    pub explain: bool,
22
23    #[bpaf(external(lintel_cli_common::cli_cache_options))]
24    pub cache: CliCacheOptions,
25
26    /// Disable syntax highlighting in code blocks
27    #[bpaf(long("no-syntax-highlighting"), switch)]
28    pub no_syntax_highlighting: bool,
29
30    /// Print output directly instead of piping through a pager
31    #[bpaf(long("no-pager"), switch)]
32    pub no_pager: bool,
33
34    /// Show extended details like $comment annotations
35    #[bpaf(long("extended"), switch)]
36    pub extended: bool,
37
38    /// File to identify
39    #[bpaf(positional("FILE"), complete_shell(ShellComp::File { mask: None }))]
40    pub file: String,
41}
42
43/// Construct the bpaf parser for `IdentifyArgs`.
44pub fn identify_args() -> impl bpaf::Parser<IdentifyArgs> {
45    identify_args_inner()
46}
47
48// ---------------------------------------------------------------------------
49// Entry point
50// ---------------------------------------------------------------------------
51
52#[allow(clippy::missing_panics_doc, clippy::missing_errors_doc)]
53pub async fn run(args: IdentifyArgs, global: &CLIGlobalOptions) -> Result<bool> {
54    let file_path = Path::new(&args.file);
55    if !file_path.exists() {
56        anyhow::bail!("file not found: {}", args.file);
57    }
58
59    let content = std::fs::read_to_string(file_path)
60        .with_context(|| format!("failed to read {}", args.file))?;
61
62    let path_str = file_path.display().to_string();
63
64    let Some(resolved) =
65        lintel_explain::resolve::resolve_schema_for_content(&content, file_path, None, &args.cache)
66            .await?
67    else {
68        eprintln!("{path_str}");
69        eprintln!("  no schema found");
70        return Ok(false);
71    };
72
73    print_identification(&path_str, &resolved);
74
75    if args.explain {
76        lintel_explain::explain_resolved_schema(
77            &resolved,
78            &args.cache,
79            global,
80            &lintel_explain::ExplainDisplayArgs {
81                no_syntax_highlighting: args.no_syntax_highlighting,
82                no_pager: args.no_pager,
83                extended: args.extended,
84            },
85        )
86        .await?;
87    }
88
89    Ok(false)
90}
91
92/// Print the identification summary to stdout.
93fn print_identification(path_str: &str, resolved: &ResolvedFileSchema) {
94    let schema_uri = &resolved.schema_uri;
95    let display_name = &resolved.display_name;
96
97    println!("{path_str}");
98    if display_name == schema_uri {
99        println!("  schema: {schema_uri}");
100    } else {
101        println!("  schema: {display_name} ({schema_uri})");
102    }
103    println!("  source: {}", resolved.source);
104
105    if let Some(pattern) = &resolved.matched_pattern {
106        println!("  matched: {pattern}");
107    }
108    if resolved.file_match.len() > 1 {
109        let globs = resolved
110            .file_match
111            .iter()
112            .map(String::as_str)
113            .collect::<Vec<_>>()
114            .join(", ");
115        println!("  globs: {globs}");
116    }
117    if let Some(desc) = &resolved.description {
118        println!("  description: {desc}");
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    use bpaf::Parser;
127    use lintel_cli_common::cli_global_options;
128
129    // Helper to build the CLI parser matching the binary's structure.
130    fn test_cli() -> bpaf::OptionParser<(CLIGlobalOptions, IdentifyArgs)> {
131        bpaf::construct!(cli_global_options(), identify_args())
132            .to_options()
133            .descr("test identify args")
134    }
135
136    #[test]
137    fn cli_parses_identify_basic() -> anyhow::Result<()> {
138        let (_, args) = test_cli()
139            .run_inner(&["file.json"])
140            .map_err(|e| anyhow::anyhow!("{e:?}"))?;
141        assert_eq!(args.file, "file.json");
142        assert!(!args.explain);
143        assert!(!args.cache.no_catalog);
144        assert!(!args.cache.force_schema_fetch);
145        assert!(args.cache.cache_dir.is_none());
146        assert!(args.cache.schema_cache_ttl.is_none());
147        Ok(())
148    }
149
150    #[test]
151    fn cli_parses_identify_explain() -> anyhow::Result<()> {
152        let (_, args) = test_cli()
153            .run_inner(&["file.json", "--explain"])
154            .map_err(|e| anyhow::anyhow!("{e:?}"))?;
155        assert_eq!(args.file, "file.json");
156        assert!(args.explain);
157        Ok(())
158    }
159
160    #[test]
161    fn cli_parses_identify_no_catalog() -> anyhow::Result<()> {
162        let (_, args) = test_cli()
163            .run_inner(&["--no-catalog", "file.json"])
164            .map_err(|e| anyhow::anyhow!("{e:?}"))?;
165        assert_eq!(args.file, "file.json");
166        assert!(args.cache.no_catalog);
167        Ok(())
168    }
169
170    #[test]
171    fn cli_parses_identify_all_options() -> anyhow::Result<()> {
172        let (_, args) = test_cli()
173            .run_inner(&[
174                "--explain",
175                "--no-catalog",
176                "--force-schema-fetch",
177                "--cache-dir",
178                "/tmp/cache",
179                "--schema-cache-ttl",
180                "30m",
181                "tsconfig.json",
182            ])
183            .map_err(|e| anyhow::anyhow!("{e:?}"))?;
184        assert_eq!(args.file, "tsconfig.json");
185        assert!(args.explain);
186        assert!(args.cache.no_catalog);
187        assert!(args.cache.force_schema_fetch);
188        assert_eq!(args.cache.cache_dir.as_deref(), Some("/tmp/cache"));
189        assert_eq!(
190            args.cache.schema_cache_ttl,
191            Some(core::time::Duration::from_secs(30 * 60))
192        );
193        Ok(())
194    }
195}