jsona_cli/commands/
lint.rs

1use 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    /// URL to the schema to be used for validation.
140    ///
141    /// If --schemastore present, a schema name can be used.
142    #[clap(long)]
143    pub schema: Option<String>,
144
145    /// URL to a custom schema store
146    #[clap(long)]
147    pub schemastore: Option<String>,
148
149    /// Use default schemastore
150    #[clap(short = 'S', long = "default-schemastore")]
151    pub default_schemastore: bool,
152
153    /// Paths or glob patterns to JSONA documents.
154    pub files: Vec<String>,
155}