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#[derive(Debug, Clone, Bpaf)]
16#[bpaf(generate(identify_args_inner))]
17#[allow(clippy::struct_excessive_bools)]
18pub struct IdentifyArgs {
19 #[bpaf(long("explain"), switch)]
21 pub explain: bool,
22
23 #[bpaf(external(lintel_cli_common::cli_cache_options))]
24 pub cache: CliCacheOptions,
25
26 #[bpaf(long("no-syntax-highlighting"), switch)]
28 pub no_syntax_highlighting: bool,
29
30 #[bpaf(long("no-pager"), switch)]
32 pub no_pager: bool,
33
34 #[bpaf(long("extended"), switch)]
36 pub extended: bool,
37
38 #[bpaf(positional("FILE"), complete_shell(ShellComp::File { mask: None }))]
40 pub file: String,
41}
42
43pub fn identify_args() -> impl bpaf::Parser<IdentifyArgs> {
45 identify_args_inner()
46}
47
48#[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
92fn 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 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}