jsona_cli/commands/
format.rs1use 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 #[clap(long = "option", short)]
120 pub options: Vec<String>,
121
122 #[clap(long, short)]
124 pub force: bool,
125
126 #[clap(long)]
128 pub check: bool,
129
130 pub files: Vec<String>,
134}