jsona_cli/commands/
lint.rs1use crate::{App, GeneralArgs};
2
3use anyhow::{anyhow, Context};
4use clap::Args;
5use codespan_reporting::files::SimpleFile;
6use jsona::parser;
7use jsona_util::{
8 environment::Environment,
9 schema::associations::{AssociationRule, SchemaAssociation},
10};
11use serde_json::json;
12use tokio::io::AsyncReadExt;
13
14impl<E: Environment> App<E> {
15 pub async fn execute_lint(&mut self, cmd: LintCommand) -> Result<(), anyhow::Error> {
16 if let Some(store) = &cmd.schemastore {
17 let url = self
18 .env
19 .to_url(store)
20 .ok_or_else(|| anyhow!("invalid schemastore {store}"))?;
21
22 self.schemas
23 .associations()
24 .add_from_schemastore(&Some(url), &self.env.root_uri())
25 .await
26 .with_context(|| "failed to load schema store")?;
27 } else if cmd.default_schemastore {
28 self.schemas
29 .associations()
30 .add_from_schemastore(&None, &self.env.root_uri())
31 .await
32 .with_context(|| "failed to load schema store")?;
33 }
34 if let Some(name) = &cmd.schema {
35 let url = self
36 .schemas
37 .associations()
38 .get_schema_url(name)
39 .ok_or_else(|| anyhow!("invalid or not found schema `{}`", name))?;
40 self.schemas.associations().add(
41 AssociationRule::glob("**")?,
42 SchemaAssociation {
43 meta: json!({"source": "command-line"}),
44 url,
45 priority: 999,
46 },
47 );
48 }
49
50 if cmd.files.is_empty() {
51 self.lint_stdin(cmd).await
52 } else {
53 self.lint_files(cmd).await
54 }
55 }
56
57 #[tracing::instrument(skip_all)]
58 async fn lint_stdin(&self, _cmd: LintCommand) -> Result<(), anyhow::Error> {
59 self.lint_file("-", true).await
60 }
61
62 #[tracing::instrument(skip_all)]
63 async fn lint_files(&mut self, cmd: LintCommand) -> Result<(), anyhow::Error> {
64 let mut result = Ok(());
65
66 for file in &cmd.files {
67 if let Err(error) = self.lint_file(file, false).await {
68 tracing::error!(%error, path = ?file, "invalid file");
69 result = Err(anyhow!("some files were not valid"));
70 }
71 }
72
73 result
74 }
75
76 #[tracing::instrument(skip_all, fields(%file_path))]
77 async fn lint_file(&self, file_path: &str, stdin: bool) -> Result<(), anyhow::Error> {
78 let (file_uri, source) = if stdin {
79 let mut source = String::new();
80 self.env
81 .stdin()
82 .read_to_string(&mut source)
83 .await
84 .map_err(|err| anyhow!("failed to read stdin, {err}"))?;
85 ("file:///_".parse().unwrap(), source)
86 } else {
87 self.load_file(file_path)
88 .await
89 .map_err(|err| anyhow!("failed to read {file_path}, {err}"))?
90 };
91 let parse = parser::parse(&source);
92 self.print_parse_errors(&SimpleFile::new(file_path, &source), &parse.errors)
93 .await?;
94
95 if !parse.errors.is_empty() {
96 return Err(anyhow!("syntax errors found"));
97 }
98
99 let dom = parse.into_dom();
100
101 if let Err(errors) = dom.validate() {
102 self.print_semantic_errors(&SimpleFile::new(file_path, &source), errors)
103 .await?;
104
105 return Err(anyhow!("semantic errors found"));
106 }
107
108 self.schemas
109 .associations()
110 .add_from_document(&file_uri, &dom);
111
112 if let Some(schema_association) = self.schemas.associations().query_for(&file_uri) {
113 tracing::debug!(
114 schema.url = %schema_association.url,
115 schema.name = schema_association.meta["name"].as_str().unwrap_or(""),
116 schema.source = schema_association.meta["source"].as_str().unwrap_or(""),
117 "using schema"
118 );
119
120 let errors = self.schemas.validate(&schema_association.url, &dom).await?;
121
122 if !errors.is_empty() {
123 self.print_schema_errors(&SimpleFile::new(file_path, &source), &dom, &errors)
124 .await?;
125
126 return Err(anyhow!("schema validation failed"));
127 }
128 }
129
130 Ok(())
131 }
132}
133
134#[derive(Debug, Clone, Args)]
135pub struct LintCommand {
136 #[clap(flatten)]
137 pub general: GeneralArgs,
138
139 #[clap(long)]
143 pub schema: Option<String>,
144
145 #[clap(long)]
147 pub schemastore: Option<String>,
148
149 #[clap(short = 'S', long = "default-schemastore")]
151 pub default_schemastore: bool,
152
153 pub files: Vec<String>,
155}