jsonformat/
lib.rs

1//! jsonformat is a library for formatting json.
2//!
3//! It does not do anything more than that, which makes it so fast.
4
5use std::{
6    io,
7    io::{Read, Write},
8};
9
10/// Set the indentation used for the formatting.
11///
12/// Note: It is *not* recommended to set indentation to anything oder than some spaces or some tabs,
13/// but nothing is stopping you from doing that.
14#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
15pub enum Indentation<'a> {
16    /// Fast path for two spaces
17    TwoSpace,
18    /// Fast path for four spaces
19    FourSpace,
20    /// Fast path for tab
21    Tab,
22    /// Use a custom indentation String
23    Custom(&'a str),
24}
25
26impl Default for Indentation<'_> {
27    fn default() -> Self {
28        Self::TwoSpace
29    }
30}
31
32/// # Formats a json string
33///
34/// The indentation can be set to any value using [`Indentation`]
35/// The default value is two spaces
36/// The default indentation is faster than a custom one
37pub fn format(json: &str, indentation: Indentation) -> String {
38    let mut reader = json.as_bytes();
39    let mut writer = Vec::with_capacity(json.len());
40
41    format_reader_writer(&mut reader, &mut writer, indentation).unwrap();
42    String::from_utf8(writer).unwrap()
43}
44
45/// # Formats a json string
46///
47/// The indentation can be set to any value using [`Indentation`]
48/// The default value is two spaces
49/// The default indentation is faster than a custom one
50pub fn format_reader_writer<R, W>(
51    reader: R,
52    mut writer: W,
53    indentation: Indentation,
54) -> io::Result<()>
55where
56    R: Read,
57    W: Write,
58{
59    let mut escaped = false;
60    let mut in_string = false;
61    let mut indent_level = 0usize;
62    let mut newline_requested = false; // invalidated if next character is ] or }
63
64    for char in reader.bytes() {
65        let char = char?;
66        if in_string {
67            let mut escape_here = false;
68            match char {
69                b'"' => {
70                    if !escaped {
71                        in_string = false;
72                    }
73                }
74                b'\\' => {
75                    if !escaped {
76                        escape_here = true;
77                    }
78                }
79                _ => {}
80            }
81            writer.write_all(&[char])?;
82            escaped = escape_here;
83        } else {
84            let mut auto_push = true;
85            let mut request_newline = false;
86            let old_level = indent_level;
87
88            match char {
89                b'"' => in_string = true,
90                b' ' | b'\n' | b'\r' | b'\t' => continue,
91                b'[' => {
92                    indent_level += 1;
93                    request_newline = true;
94                }
95                b'{' => {
96                    indent_level += 1;
97                    request_newline = true;
98                }
99                b'}' | b']' => {
100                    indent_level = indent_level.saturating_sub(1);
101                    if !newline_requested {
102                        // see comment below about newline_requested
103                        writer.write_all(b"\n")?;
104                        indent(&mut writer, indent_level, indentation)?;
105                    }
106                }
107                b':' => {
108                    auto_push = false;
109                    writer.write_all(&[char])?;
110                    writer.write_all(&[b' '])?;
111                }
112                b',' => {
113                    request_newline = true;
114                }
115                _ => {}
116            }
117            if newline_requested && char != b']' && char != b'}' {
118                // newline only happens after { [ and ,
119                // this means we can safely assume that it being followed up by } or ]
120                // means an empty object/array
121                writer.write_all(b"\n")?;
122                indent(&mut writer, old_level, indentation)?;
123            }
124
125            if auto_push {
126                writer.write_all(&[char])?;
127            }
128
129            newline_requested = request_newline;
130        }
131    }
132
133    // trailing newline
134    writer.write_all(b"\n")?;
135
136    Ok(())
137}
138
139fn indent<W>(writer: &mut W, level: usize, indent_str: Indentation) -> io::Result<()>
140where
141    W: Write,
142{
143    for _ in 0..level {
144        match indent_str {
145            Indentation::TwoSpace => {
146                writer.write_all(b"  ")?;
147            }
148            Indentation::FourSpace => {
149                writer.write_all(b"    ")?;
150            }
151            Indentation::Tab => {
152                writer.write_all(b"\t")?;
153            }
154            Indentation::Custom(indent) => {
155                writer.write_all(indent.as_bytes())?;
156            }
157        }
158    }
159
160    Ok(())
161}
162
163#[cfg(test)]
164mod test {
165    use super::*;
166
167    #[test]
168    fn echoes_primitive() {
169        let json = "1.35\n";
170        assert_eq!(json, format(json, Indentation::TwoSpace));
171    }
172
173    #[test]
174    fn ignore_whitespace_in_string() {
175        let json = "\" hallo \"\n";
176        assert_eq!(json, format(json, Indentation::TwoSpace));
177    }
178
179    #[test]
180    fn remove_leading_whitespace() {
181        let json = "   0";
182        let expected = "0\n";
183        assert_eq!(expected, format(json, Indentation::TwoSpace));
184    }
185
186    #[test]
187    fn handle_escaped_strings() {
188        let json = "  \" hallo \\\" \" ";
189        let expected = "\" hallo \\\" \"\n";
190        assert_eq!(expected, format(json, Indentation::TwoSpace));
191    }
192
193    #[test]
194    fn simple_object() {
195        let json = "{\"a\":0}";
196        let expected = "{
197  \"a\": 0
198}
199";
200        assert_eq!(expected, format(json, Indentation::TwoSpace));
201    }
202
203    #[test]
204    fn simple_array() {
205        let json = "[1,2,null]";
206        let expected = "[
207  1,
208  2,
209  null
210]
211";
212        assert_eq!(expected, format(json, Indentation::TwoSpace));
213    }
214
215    #[test]
216    fn array_of_object() {
217        let json = "[{\"a\": 0}, {}, {\"a\": null}]";
218        let expected = "[
219  {
220    \"a\": 0
221  },
222  {},
223  {
224    \"a\": null
225  }
226]
227";
228
229        assert_eq!(expected, format(json, Indentation::TwoSpace));
230    }
231
232    #[test]
233    fn already_formatted() {
234        let expected = "[
235  {
236    \"a\": 0
237  },
238  {},
239  {
240    \"a\": null
241  }
242]
243";
244
245        assert_eq!(expected, format(expected, Indentation::TwoSpace));
246    }
247
248    #[test]
249    fn contains_crlf() {
250        let json = "[\r\n{\r\n\"a\":0\r\n},\r\n{},\r\n{\r\n\"a\": null\r\n}\r\n]\r\n";
251        let expected = "[
252  {
253    \"a\": 0
254  },
255  {},
256  {
257    \"a\": null
258  }
259]
260";
261
262        assert_eq!(expected, format(json, Indentation::TwoSpace));
263    }
264}