jsona_cli/commands/
format.rs

1use crate::{App, GeneralArgs};
2
3use anyhow::anyhow;
4use clap::Args;
5use codespan_reporting::files::SimpleFile;
6use jsona::{formatter, parser};
7use jsona_util::environment::Environment;
8use tokio::io::{AsyncReadExt, AsyncWriteExt};
9
10impl<E: Environment> App<E> {
11    pub async fn execute_format(&mut self, cmd: FormatCommand) -> Result<(), anyhow::Error> {
12        if cmd.files.is_empty() {
13            self.format_stdin(cmd).await
14        } else {
15            self.format_files(cmd).await
16        }
17    }
18
19    #[tracing::instrument(skip_all)]
20    async fn format_stdin(&mut self, cmd: FormatCommand) -> Result<(), anyhow::Error> {
21        let mut source = String::new();
22        self.env.stdin().read_to_string(&mut source).await?;
23
24        let display_path = "-";
25
26        let p = parser::parse(&source);
27
28        if !p.errors.is_empty() {
29            self.print_parse_errors(&SimpleFile::new(display_path, source.as_str()), &p.errors)
30                .await?;
31
32            if !cmd.force {
33                return Err(anyhow!("no formatting was done due to syntax errors"));
34            }
35        }
36        let format_opts = self.format_options(&cmd)?;
37
38        let formatted = formatter::format_syntax(p.into_syntax(), format_opts);
39
40        if cmd.check {
41            if source != formatted {
42                return Err(anyhow!("the input was not properly formatted"));
43            }
44        } else {
45            let mut stdout = self.env.stdout();
46            stdout.write_all(formatted.as_bytes()).await?;
47            stdout.flush().await?;
48        }
49
50        Ok(())
51    }
52
53    #[tracing::instrument(skip_all)]
54    async fn format_files(&mut self, cmd: FormatCommand) -> Result<(), anyhow::Error> {
55        let mut result = Ok(());
56
57        let format_opts = self.format_options(&cmd)?;
58
59        for path in &cmd.files {
60            let (url, source) = self
61                .load_file(path)
62                .await
63                .map_err(|err| anyhow!("failed to read {path}, {err}"))?;
64
65            let p = parser::parse(&source);
66
67            if !p.errors.is_empty() {
68                self.print_parse_errors(&SimpleFile::new(path, source.as_str()), &p.errors)
69                    .await?;
70
71                if !cmd.force {
72                    result = Err(anyhow!(
73                        "some files were not formatted due to syntax errors"
74                    ));
75                    continue;
76                }
77            }
78
79            let formatted = formatter::format_syntax(p.into_syntax(), format_opts.clone());
80
81            if cmd.check {
82                if source != formatted {
83                    tracing::error!(?path, "the file is not properly formatted");
84                    result = Err(anyhow!("some files were not properly formatted"));
85                }
86            } else if source != formatted {
87                self.env.write_file(&url, formatted.as_bytes()).await?;
88            }
89        }
90
91        result
92    }
93
94    fn format_options(&self, cmd: &FormatCommand) -> Result<formatter::Options, anyhow::Error> {
95        let mut format_opts = formatter::Options::default();
96        format_opts.update_from_str(cmd.options.iter().filter_map(|s| {
97            let mut split = s.split('=');
98            let k = split.next();
99            let v = split.next();
100
101            if let (Some(k), Some(v)) = (k, v) {
102                Some((k, v))
103            } else {
104                tracing::error!(option = %s, "malformed formatter option");
105                None
106            }
107        }))?;
108
109        Ok(format_opts)
110    }
111}
112
113#[derive(Debug, Clone, Args)]
114pub struct FormatCommand {
115    #[clap(flatten)]
116    pub general: GeneralArgs,
117
118    /// A formatter option given as a "key=value", can be set multiple times.
119    #[clap(long = "option", short)]
120    pub options: Vec<String>,
121
122    /// Ignore syntax errors and force formatting.
123    #[clap(long, short)]
124    pub force: bool,
125
126    /// Dry-run and report any files that are not correctly formatted.
127    #[clap(long)]
128    pub check: bool,
129
130    /// JSONA files to format.
131    ///
132    /// If the only argument is "-", the standard input will be used.
133    pub files: Vec<String>,
134}