Skip to main content

ocpi_tariffs/json/
write.rs

1//! Tools for writing JSON.
2
3#[cfg(test)]
4mod test_tree_writer;
5
6use std::fmt::{self, Write};
7
8use super::{Element, Field, Value};
9
10const TAB: &str = "    ";
11const ARRAY_OPEN: char = '[';
12const ARRAY_CLOSE: char = ']';
13const OBJECT_OPEN: char = '{';
14const OBJECT_CLOSE: char = '}';
15const COMMA: char = ',';
16const NEWLINE: char = '\n';
17
18/// Write a parsed and potentially modified `json::Element` tree to a buffer formatted
19/// for human readability.
20///
21/// The JSON is formatted so that each element is indented and put on its own line.
22pub struct Pretty<'a, 'buf> {
23    elem: &'a Element<'buf>,
24}
25
26impl<'a, 'buf> Pretty<'a, 'buf> {
27    pub fn new(elem: &'a Element<'buf>) -> Self {
28        Pretty { elem }
29    }
30}
31
32impl fmt::Display for Pretty<'_, '_> {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        let mut stack = vec![State::Root(self.elem)];
35        let mut write_comma = WriteComma::Skip;
36
37        // This outer loop drives the pushing of elements on to the stack.
38        loop {
39            // This inner loop drives the popping of states off the stack.
40            let elem = loop {
41                let depth = stack.len();
42
43                let Some(mut state) = stack.pop() else {
44                    // If the stack is empty, we're done writing.
45                    return Ok(());
46                };
47
48                let elem = match &mut state {
49                    State::Root(elem) => {
50                        // The root `Element` never needs a comma written after it.
51                        // It's either an opening compound object such as an array or object; or it's
52                        // a single Value.
53                        write_elem(elem, f)?;
54                        elem
55                    }
56                    State::Array(iter) => {
57                        let Some(elem) = iter.next() else {
58                            // If there is no next `Element` then we need to move up the stack
59                            // and check there for a next `Element`.
60                            let Some(depth) = depth.checked_sub(1) else {
61                                return Ok(());
62                            };
63                            write_nl_and_indent(depth, f)?;
64                            f.write_char(ARRAY_CLOSE)?;
65                            continue;
66                        };
67
68                        if let WriteComma::Write = write_comma {
69                            f.write_char(COMMA)?;
70                        }
71                        write_nl_and_indent(depth, f)?;
72                        write_comma = write_elem(elem, f)?;
73                        elem
74                    }
75                    State::Object(iter) => {
76                        let Some(field) = iter.next() else {
77                            // If there is no next `Element` then we need to move up the stack
78                            // and check there for a next `Element`.
79                            let Some(depth) = depth.checked_sub(1) else {
80                                return Ok(());
81                            };
82                            write_nl_and_indent(depth, f)?;
83                            f.write_char(OBJECT_CLOSE)?;
84                            continue;
85                        };
86
87                        if let WriteComma::Write = write_comma {
88                            f.write_char(COMMA)?;
89                        }
90                        write_nl_and_indent(depth, f)?;
91                        write_comma = write_field(field, f)?;
92                        field.element()
93                    }
94                };
95
96                match &state {
97                    State::Array(_) | State::Object(_) => stack.push(state),
98                    State::Root(_) => (),
99                }
100
101                break elem;
102            };
103
104            match elem.value() {
105                Value::Array(elements) => stack.push(State::Array(elements.iter())),
106                Value::Object(fields) => stack.push(State::Object(fields.iter())),
107                _ => (),
108            }
109        }
110    }
111}
112
113/// The single stack level.
114#[derive(Debug)]
115enum State<'a, 'buf> {
116    /// The root element.
117    Root(&'a Element<'buf>),
118
119    /// A collection of Array `Element`s.
120    Array(std::slice::Iter<'a, Element<'buf>>),
121
122    /// A collection of Object `Field`s.
123    Object(std::slice::Iter<'a, Field<'buf>>),
124}
125
126/// Write a newline and indent ready for the next `Element`'s `Value` to be written.
127fn write_nl_and_indent(depth: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128    f.write_char(NEWLINE)?;
129
130    for _ in 0..depth {
131        f.write_str(TAB)?;
132    }
133
134    Ok(())
135}
136
137/// Whether or not a comma should be interspersed between values.
138#[derive(Copy, Clone)]
139enum WriteComma {
140    /// Write a comma on the next iteration.
141    Write,
142
143    /// Skip writing a comma on the next iteration.
144    Skip,
145}
146
147/// Shallow write an `Element` and return whether a comma should be written on the next iteration.
148///
149/// If the `Element` is a compound object like an `Array` or `Object`, only the opening brace is written.
150/// This is done to avoid implementing a recursive write fn.
151fn write_elem(elem: &Element<'_>, f: &mut fmt::Formatter<'_>) -> Result<WriteComma, fmt::Error> {
152    match elem.value() {
153        Value::Null => {
154            f.write_str("null")?;
155            Ok(WriteComma::Write)
156        }
157        Value::True => {
158            f.write_str("true")?;
159            Ok(WriteComma::Write)
160        }
161        Value::False => {
162            f.write_str("false")?;
163            Ok(WriteComma::Write)
164        }
165        Value::String(s) => {
166            write!(f, "\"{s}\"")?;
167            Ok(WriteComma::Write)
168        }
169        Value::Number(n) => {
170            write!(f, "{n}")?;
171            Ok(WriteComma::Write)
172        }
173        Value::Array(_) => {
174            f.write_char(ARRAY_OPEN)?;
175            Ok(WriteComma::Skip)
176        }
177        Value::Object(_) => {
178            f.write_char(OBJECT_OPEN)?;
179            Ok(WriteComma::Skip)
180        }
181    }
182}
183
184/// Write a `Field`'s key and value and return whether a comma should be written on the next iteration.
185fn write_field(field: &Field<'_>, f: &mut fmt::Formatter<'_>) -> Result<WriteComma, fmt::Error> {
186    write!(f, "\"{}\": ", field.key())?;
187    write_elem(field.element(), f)
188}