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