smart_config_commands/
utils.rs

1//! Functionality shared by multiple CLI commands.
2
3use std::io::{self, Write as _};
4
5use anstream::stream::{AsLockedWrite, RawStream};
6use anstyle::{AnsiColor, Color, Style};
7use smart_config::value::{StrValue, Value, WithOrigin};
8
9use crate::Printer;
10
11pub(crate) const STRING: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Cyan)));
12pub(crate) const NULL: Style = Style::new().bold();
13const BOOL: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Yellow)));
14const NUMBER: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Green)));
15const SECRET: Style = Style::new()
16    .bg_color(Some(Color::Ansi(AnsiColor::Cyan)))
17    .fg_color(None);
18const OBJECT_KEY: Style = Style::new().bold();
19
20impl<W: RawStream + AsLockedWrite> Printer<W> {
21    /// Outputs JSON with syntax highlighting.
22    ///
23    /// # Errors
24    ///
25    /// Proxies I/O errors.
26    pub fn print_json(&mut self, json: &serde_json::Value) -> io::Result<()> {
27        write_json_value(&mut self.writer, json, 0)?;
28        writeln!(&mut self.writer)
29    }
30
31    /// Outputs YAML adhering to the JSON model with syntax highlighting.
32    ///
33    /// # Errors
34    ///
35    /// Proxies I/O errors.
36    pub fn print_yaml(&mut self, json: &serde_json::Value) -> io::Result<()> {
37        write_yaml_value(&mut self.writer, json, 0, false)?;
38        writeln!(&mut self.writer)
39    }
40}
41
42pub(crate) fn write_json_value(
43    writer: &mut impl io::Write,
44    value: &serde_json::Value,
45    ident: usize,
46) -> io::Result<()> {
47    match value {
48        serde_json::Value::Null => write!(writer, "{NULL}null{NULL:#}"),
49        serde_json::Value::Bool(val) => write!(writer, "{BOOL}{val:?}{BOOL:#}"),
50        serde_json::Value::Number(val) => write!(writer, "{NUMBER}{val}{NUMBER:#}"),
51        serde_json::Value::String(val) => write!(writer, "{STRING}{val:?}{STRING:#}"),
52        serde_json::Value::Array(val) => {
53            if val.is_empty() {
54                write!(writer, "[]")
55            } else {
56                writeln!(writer, "[")?;
57                for (i, item) in val.iter().enumerate() {
58                    write!(writer, "{:ident$}  ", "")?;
59                    write_json_value(writer, item, ident + 2)?;
60                    if i + 1 < val.len() {
61                        writeln!(writer, ",")?;
62                    } else {
63                        writeln!(writer)?;
64                    }
65                }
66                write!(writer, "{:ident$}]", "")
67            }
68        }
69        serde_json::Value::Object(val) => {
70            if val.is_empty() {
71                write!(writer, "{{}}")
72            } else {
73                writeln!(writer, "{{")?;
74                for (i, (key, value)) in val.iter().enumerate() {
75                    write!(writer, "{:ident$}  {OBJECT_KEY}{key:?}{OBJECT_KEY:#}: ", "")?;
76                    write_json_value(writer, value, ident + 2)?;
77                    if i + 1 < val.len() {
78                        writeln!(writer, ",")?;
79                    } else {
80                        writeln!(writer)?;
81                    }
82                }
83                write!(writer, "{:ident$}}}", "")
84            }
85        }
86    }
87}
88
89fn yaml_string(val: &str) -> String {
90    // YAML has arcane rules escaping strings, so we just use the library.
91    let mut yaml = serde_yaml::to_string(val).unwrap();
92    if yaml.ends_with('\n') {
93        yaml.pop();
94    }
95    yaml
96}
97
98fn write_yaml_value(
99    writer: &mut impl io::Write,
100    value: &serde_json::Value,
101    ident: usize,
102    is_array_item: bool,
103) -> io::Result<()> {
104    match value {
105        serde_json::Value::Null => write!(writer, "{NULL}null{NULL:#}"),
106        serde_json::Value::Bool(val) => write!(writer, "{BOOL}{val:?}{BOOL:#}"),
107        serde_json::Value::Number(val) => write!(writer, "{NUMBER}{val}{NUMBER:#}"),
108        serde_json::Value::String(val) => {
109            let yaml_val = yaml_string(val);
110            write!(writer, "{STRING}{yaml_val}{STRING:#}")
111        }
112        serde_json::Value::Array(val) => {
113            if val.is_empty() {
114                if ident > 0 {
115                    write!(writer, " ")?; // We haven't output a space before the array in the parent array / object
116                }
117                write!(writer, "[]")
118            } else {
119                if ident > 0 {
120                    writeln!(writer)?;
121                }
122                for (i, item) in val.iter().enumerate() {
123                    write!(writer, "{:ident$}-", "")?;
124                    if !item.is_array() {
125                        write!(writer, " ")?; // If the item is another array, we'll output a newline instead
126                    }
127
128                    write_yaml_value(writer, item, ident + 2, true)?;
129                    if i + 1 < val.len() {
130                        writeln!(writer)?;
131                    }
132                }
133                Ok(())
134            }
135        }
136        serde_json::Value::Object(val) => {
137            if val.is_empty() {
138                if ident > 0 {
139                    write!(writer, " ")?; // We haven't output a space before the array in the parent array / object
140                }
141                write!(writer, "{{}}")
142            } else {
143                if ident > 0 && !is_array_item {
144                    writeln!(writer)?;
145                }
146                for (i, (key, value)) in val.iter().enumerate() {
147                    let yaml_key = yaml_string(key);
148                    if is_array_item && i == 0 {
149                        // Skip padding for the first item in an array
150                    } else {
151                        write!(writer, "{:ident$}", "")?;
152                    }
153                    write!(writer, "{OBJECT_KEY}{yaml_key}{OBJECT_KEY:#}:")?;
154                    if !value.is_object() && !value.is_array() {
155                        write!(writer, " ")?; // If the child value is an object or array, we'll output a newline
156                    }
157
158                    write_yaml_value(writer, value, ident + 2, false)?;
159                    if i + 1 < val.len() {
160                        writeln!(writer)?;
161                    }
162                }
163                Ok(())
164            }
165        }
166    }
167}
168
169pub(crate) fn write_value(
170    writer: &mut impl io::Write,
171    value: &WithOrigin,
172    ident: usize,
173) -> io::Result<()> {
174    match &value.inner {
175        Value::Null => write!(writer, "{NULL}null{NULL:#}"),
176        Value::Bool(val) => write!(writer, "{BOOL}{val:?}{BOOL:#}"),
177        Value::Number(val) => write!(writer, "{NUMBER}{val}{NUMBER:#}"),
178        Value::String(StrValue::Plain(val)) => write!(writer, "{STRING}{val:?}{STRING:#}"),
179        Value::String(StrValue::Secret(_)) => write!(writer, "{SECRET}[REDACTED]{SECRET:#}"),
180        Value::Array(val) => {
181            writeln!(writer, "[")?;
182            for item in val {
183                write!(writer, "{:ident$}  ", "")?;
184                write_value(writer, item, ident + 2)?;
185                writeln!(writer, ",")?;
186            }
187            write!(writer, "{:ident$}]", "")
188        }
189        Value::Object(val) => {
190            writeln!(writer, "{{")?;
191            for (key, value) in val {
192                write!(writer, "{:ident$}  {OBJECT_KEY}{key:?}{OBJECT_KEY:#}: ", "")?;
193                write_value(writer, value, ident + 2)?;
194                writeln!(writer, ",")?;
195            }
196            write!(writer, "{:ident$}}}", "")
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use anstream::AutoStream;
204
205    use crate::utils::write_yaml_value;
206
207    #[test]
208    fn writing_yaml() {
209        let original_yaml = "\
210test:
211  app_name: test
212  cache_size: 128 MiB
213  dir_paths:
214    - /usr/local/bin
215    - /usr/bin
216  funding:
217    address: '0x0000000000000000000000000000000000001234'
218    api_key: correct horse battery staple
219    balance: '0x123456'
220    secret_key: 0x000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f
221  nested:
222    complex:
223      array:
224        - 1
225        - 2
226      map:
227        value: 25
228    exit_on_error: false
229    method_limits:
230      - method: eth_blockNumber
231        rps: 3
232      - method: eth_getLogs
233        rps: 100
234    more_timeouts: []
235  object_store:
236    bucket_name: test-bucket
237    type: gcs
238  poll_latency: 300ms
239  port: 3000
240  required: 123
241  scaling_factor: 4.199999809265137
242  temp_dir: /var/folders/mw/lhb7m9dj3jbdm3w994t0_c8h0000gn/T/
243  timeout_sec: 60";
244        let json: serde_json::Value = serde_yaml::from_str(original_yaml).unwrap();
245        assert!(json["test"].as_object().unwrap().len() > 10, "{json:?}");
246
247        let mut buffer = vec![];
248        write_yaml_value(&mut AutoStream::never(&mut buffer), &json, 0, false).unwrap();
249        let produced_yaml = String::from_utf8(buffer).unwrap();
250        assert_eq!(produced_yaml, original_yaml);
251    }
252}