Skip to main content

structio/json/
prettify.rs

1//! Laying out JSON text that is already JSON.
2//!
3//! What a caller can rely on is documented on [`prettify`], which the other
4//! entry points here refer back to. Why the walk goes through the value path's
5//! writer rather than carrying layout rules of its own, and why a number is
6//! stepped over by its alphabet rather than held to the grammar, is in
7//! [docs/design.md](https://github.com/matrix-research-inc/structio/blob/main/docs/design.md#prettifying-is-the-writer-not-a-second-layout).
8//!
9//! Every entry point is [`prettify_value_into`] underneath. It is the one that
10//! lays out into a writer already part-way through a document; the others hand
11//! it a fresh writer, at depth zero, and return what it wrote.
12
13use crate::error::{Error, ErrorCode, PResult, Result};
14use crate::json::parser::Parser;
15use crate::json::writer::Writer;
16use crate::options::{Options, Pretty};
17
18/// Lay out a JSON document across indented lines.
19///
20/// Two spaces per level, one member or element per line: [`Pretty`], the same
21/// policy [`to_string_with`](crate::to_string_with) takes.
22///
23/// ```
24/// let out = structio::prettify(r#"{"a":[1,2],"b":{}}"#).unwrap();
25/// assert_eq!(out, "{\n  \"a\": [\n    1,\n    2\n  ],\n  \"b\": {}\n}");
26/// ```
27///
28/// This is for a document that arrived as text, such as a log line, a response
29/// body or a file on disk, rather than out of a [`Write`](crate::json::Write)
30/// impl. Its whitespace goes through the same writer such an impl writes
31/// through, so the output is byte-identical to what writing the same data under
32/// the same policy would have produced.
33///
34/// Values are copied, not re-encoded. A number keeps the spelling the input
35/// gave it and a string keeps its escapes, so `1.50` stays `1.50` and
36/// `"\u0041"` stays `"\u0041"`. The output is the input's data laid out again,
37/// not a round trip through this crate's number and string formatters.
38///
39/// ```
40/// let out = structio::prettify(r#"[1.50,"\u0041"]"#).unwrap();
41/// assert_eq!(out, "[\n  1.50,\n  \"\\u0041\"\n]");
42/// ```
43///
44/// Structure is checked as the walk goes, because it has to be known to be laid
45/// out at all: which container a value is in decides whether it gets a line or
46/// a space, and how deep it is decides the indent. The input must be one
47/// complete JSON document whose structure holds up. Anything else is an
48/// [`Error`] naming the byte that stopped the walk, rather than output that is
49/// quietly wrong:
50///
51/// ```
52/// use structio::ErrorCode;
53///
54/// let e = structio::prettify(r#"{"a":}"#).unwrap_err();
55/// assert_eq!(e.code, ErrorCode::UnexpectedCharacter);
56/// assert_eq!(e.index, 5);
57/// ```
58///
59/// Tokens are not checked past what stepping over them requires. A number is
60/// taken by its alphabet, so `01` and `1.2.3` lay out unchanged rather than
61/// being refused:
62///
63/// ```
64/// assert_eq!(structio::prettify("[01,1.2.3]").unwrap(), "[\n  01,\n  1.2.3\n]");
65/// ```
66///
67/// This is not a validator, and [`from_str`](crate::from_str) is the thing to
68/// reach for when the question is whether a document is good.
69#[inline]
70pub fn prettify(input: &str) -> Result<String> {
71    prettify_with::<Pretty>(input)
72}
73
74/// [`prettify`] under an explicit [write policy](crate::Options).
75///
76/// The policy decides the layout exactly as it does when a value is written:
77/// [`INDENT`](Options::INDENT) sets the width and
78/// [`NEW_LINES_IN_ARRAYS`](Options::NEW_LINES_IN_ARRAYS) decides whether an
79/// array gets a line per element.
80///
81/// ```
82/// use structio::{PrettyInlineArrays, json::prettify_with};
83///
84/// let out = prettify_with::<PrettyInlineArrays>(r#"{"v":[1,2,3]}"#).unwrap();
85/// assert_eq!(out, "{\n  \"v\": [1, 2, 3]\n}");
86/// ```
87///
88/// [`PRETTY`](Options::PRETTY) is honoured too, rather than assumed, so a
89/// compact policy compacts. That makes [`Standard`](crate::Standard) a
90/// minifier, which is the same walk with nothing to emit between tokens:
91///
92/// ```
93/// use structio::{Standard, json::prettify_with};
94///
95/// let out = prettify_with::<Standard>("{\n  \"a\": [1, 2]\n}").unwrap();
96/// assert_eq!(out, r#"{"a":[1,2]}"#);
97/// ```
98///
99/// [`minify`](crate::minify) reaches the same bytes on any document that is
100/// really JSON, and much faster, because compacting needs none of the structure
101/// this walks. Reach for that unless you want the walk's checking too.
102///
103/// The reading settings do not apply, there being no schema here to have an
104/// opinion about: an unknown key is every key. The exception is
105/// [`ALLOW_COMMENTS`](Options::ALLOW_COMMENTS), which decides whether a
106/// document may carry comments at all. It drops them, as everything in this
107/// crate does, a comment being no part of what a writer can emit.
108#[inline]
109pub fn prettify_with<O: Options>(input: &str) -> Result<String> {
110    let mut out = String::new();
111    prettify_into_with::<O>(input, &mut out)?;
112    Ok(out)
113}
114
115/// [`prettify`] into an existing `String`, replacing its contents and keeping
116/// its allocation.
117///
118/// Prefer this in a loop, the way [`write_into`](crate::write_into) is
119/// preferred over [`to_string`](crate::to_string).
120///
121/// On failure `out` holds however much was laid out before the error, which is
122/// the same bargain [`read_into`](crate::read_into) makes: recovering the
123/// original would mean copying it first, on every call, to serve the failing
124/// case.
125#[inline]
126pub fn prettify_into(input: &str, out: &mut String) -> Result<()> {
127    prettify_into_with::<Pretty>(input, out)
128}
129
130/// [`prettify_into`] under an explicit [write policy](crate::Options).
131pub fn prettify_into_with<O: Options>(input: &str, out: &mut String) -> Result<()> {
132    let mut buf = core::mem::take(out).into_bytes();
133    // Emptied before the reserve for `minify_into_with`'s reason: `reserve`
134    // counts from the length, so clearing has to happen first rather than
135    // being left to `Writer::from_vec`.
136    buf.clear();
137    // Indenting roughly doubles a document of small values, which is the shape
138    // that gets prettified; compacting never grows one. Either way this is the
139    // only allocation the common case makes.
140    buf.reserve(if O::PRETTY {
141        input.len().saturating_mul(2)
142    } else {
143        input.len()
144    });
145
146    let mut w = Writer::<O>::from_vec(buf);
147    // The same call a `Write` impl makes, over a writer that happens to be
148    // fresh and so at depth zero. One implementation, and it is the public one
149    // that every test of this function exercises.
150    let result = prettify_value_into::<O>(input, &mut w);
151    // Hand the buffer back whether or not the walk got to the end, so the
152    // caller keeps the allocation either way.
153    *out = w.into_string();
154    result
155}
156
157/// Lay one JSON value out into a writer that is already part-way through a
158/// document.
159///
160/// Every other entry point here begins a fresh document at depth zero and
161/// hands back the text of it, which is everything a caller holding a document
162/// wants and nothing a [`Write`](crate::json::Write) impl can use. This one
163/// takes the writer instead, and lays `input` out at that writer's current
164/// nesting depth under the policy it carries, so the value arrives indented
165/// against its neighbours rather than as a blob starting again at column zero.
166/// It is what a type holding JSON *text* where the document expects a *value*
167/// needs; [`Raw`](crate::json::Raw) is that type here, and this is the call its
168/// [`Write`](crate::json::Write) impl makes.
169///
170/// ```
171/// use structio::{Options, Pretty, json};
172///
173/// /// A value this program forwards rather than reads, kept as its text.
174/// struct Body(&'static str);
175///
176/// impl json::Write for Body {
177///     fn write<O: Options>(&self, w: &mut json::Writer<'_, O>) {
178///         // The span was settled when the `Body` was built, so it lays out.
179///         json::prettify_value_into(self.0, w).unwrap();
180///     }
181/// }
182///
183/// // Laid out where the element actually sits, not at column zero.
184/// assert_eq!(
185///     structio::to_string_with::<Pretty, _>(&vec![Body(r#"{"a":[1]}"#)]),
186///     "[\n  {\n    \"a\": [\n      1\n    ]\n  }\n]"
187/// );
188/// ```
189///
190/// `input` has to be exactly one JSON value, whitespace on either side and
191/// nothing else, which is [`prettify`]'s rule too, a document being one value.
192/// Anything after it is [`TrailingContent`](crate::ErrorCode::TrailingContent)
193/// rather than something copied through, a tail landing in the middle of the
194/// surrounding document being the one place it could land.
195///
196/// The tokens are what survive. A number keeps the spelling `input` gave it and
197/// a string keeps its escapes, exactly as under [`prettify`], because the walk
198/// copies tokens rather than decoding and re-encoding them. The whitespace
199/// between them does not: the input's own layout is dropped and the policy's is
200/// written in its place, which is the whole of what the call is for. Under
201/// [`ALLOW_COMMENTS`](Options::ALLOW_COMMENTS) a comment is whitespace and goes
202/// the same way, there being no writer here that can emit one.
203///
204/// A compact policy is honoured rather than overridden, as it is by
205/// [`prettify_with`], so this minifies `input` into the writer under
206/// [`Standard`](crate::Standard). That is why `Raw` reaches here only under
207/// [`PRETTY`](Options::PRETTY): compacted output and the span it arrived as
208/// differ only in whitespace, and rewriting that whitespace would give up the
209/// bytes it exists to preserve for nothing.
210///
211/// **A call that fails leaves behind what it had already written.** A writer
212/// cannot be rewound, for the reason [`Writer`] gives, so a span that turns out
213/// to be malformed halfway through has its first half in the document and no
214/// way to take it back. An impl that must not do that has to settle the
215/// question before it emits a byte, by walking `input` first with
216/// [`Parser::skip_value`](crate::json::Parser::skip_value) and
217/// [`Parser::finish`](crate::json::Parser::finish), or by having accepted it
218/// earlier through [`Raw::new`](crate::json::Raw::new). `Raw`'s own
219/// [`Write`](crate::json::Write) impl takes the first of those routes, and the
220/// probe pass it pays for is what a second layout walk would cost anyway.
221///
222/// The error's offset is a position in `input`. An impl composing this into one
223/// of its own has nowhere to put that: an offset into a span of a larger
224/// document names the wrong byte of that document, so the code travels and the
225/// offset is dropped. See
226/// [docs/errors.md](https://github.com/matrix-research-inc/structio/blob/main/docs/errors.md#two-error-currencies-and-why)
227/// for why the seam is there.
228///
229/// This is the one member of the `prettify` family not also re-exported at the
230/// crate root. The root carries the entry points you reach for with a document
231/// in hand; reaching for this one means having a [`Writer`], which happens
232/// inside a `Write` impl and nowhere else, and `Writer` is not at the root
233/// either.
234pub fn prettify_value_into<O: Options>(input: &str, w: &mut Writer<'_, O>) -> Result<()> {
235    let mut p = Parser::<O>::with_options(input);
236    walk(&mut p, w).map_err(|code| Error::new(code, p.position()))
237}
238
239/// One whole document: the value, then nothing but whitespace.
240fn walk<O: Options>(p: &mut Parser<'_, O>, w: &mut Writer<'_, O>) -> PResult<()> {
241    p.skip_ws();
242    value(p, w)?;
243    p.finish()
244}
245
246/// Lay out one value, whatever it is.
247///
248/// The cursor must be on the value's first byte, whitespace already behind it.
249/// That is the parser's own convention and every route here keeps it: the walk
250/// above skips the leading run once, an opening bracket skips what follows it,
251/// and `colon` and `comma_or_close` skip what follows them. Skipping again per
252/// value would be a third of the whitespace work in a document that has none.
253///
254/// Private, and [`prettify_value_into`] is what a caller outside this module
255/// reaches for: the same walk with the leading whitespace skipped for it, the
256/// tail checked, and the error given a position, rather than a cursor contract
257/// stated in prose and enforced by a debug assertion.
258fn value<O: Options>(p: &mut Parser<'_, O>, w: &mut Writer<'_, O>) -> PResult<()> {
259    debug_assert!(
260        !matches!(p.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')),
261        "the cursor is not on a token: whitespace was left unskipped"
262    );
263    match p.peek() {
264        Some(b'{') => object(p, w),
265        Some(b'[') => array(p, w),
266        _ => copy_token(p, w),
267    }
268}
269
270/// `{ "key": value, ... }`, one member per line.
271fn object<O: Options>(p: &mut Parser<'_, O>, w: &mut Writer<'_, O>) -> PResult<()> {
272    p.expect(b'{', ErrorCode::ExpectedBrace)?;
273    p.nested(|p| {
274        w.open(b'{');
275        p.skip_ws();
276        if !p.try_byte(b'}') {
277            loop {
278                // The cursor is on a token: the `{` above skipped its
279                // whitespace, and so does `comma_or_close` at the foot of the
280                // loop.
281                match p.peek() {
282                    Some(b'"') => {}
283                    None => return Err(ErrorCode::UnexpectedEnd),
284                    Some(_) => return Err(ErrorCode::ExpectedQuote),
285                }
286                w.line();
287                copy_token(p, w)?;
288                p.colon()?;
289                w.colon();
290                value(p, w)?;
291                // The unconditional trailing comma every container in this
292                // crate writes; `close` below either overwrites it or takes it
293                // back.
294                w.push(b',');
295                if !p.comma_or_close(b'}')? {
296                    break;
297                }
298            }
299        }
300        w.close(b'}');
301        Ok(())
302    })
303}
304
305/// `[value, ...]`, one element per line or all on one, as the policy says.
306fn array<O: Options>(p: &mut Parser<'_, O>, w: &mut Writer<'_, O>) -> PResult<()> {
307    p.expect(b'[', ErrorCode::ExpectedBracket)?;
308    p.nested(|p| {
309        w.open(b'[');
310        p.skip_ws();
311        if !p.try_byte(b']') {
312            loop {
313                w.item();
314                value(p, w)?;
315                w.push(b',');
316                if !p.comma_or_close(b']')? {
317                    break;
318                }
319            }
320        }
321        w.close(b']');
322        Ok(())
323    })
324}
325
326/// Copy one scalar token through, exactly as the input spelled it.
327///
328/// A string or a number is the input's own bytes, and its length is not known
329/// until the parser has walked it, so it is copied. The three literals are
330/// known text and go through the writer's fixed-size appenders instead, which
331/// store a compile-time-constant run rather than calling out to a copy of four
332/// bytes whose length the compiler cannot see.
333///
334/// Either way the parser is what finds the token's end, and it checks only what
335/// finding the end requires: a string's escapes and its closing quote, a
336/// literal's spelling, and a number's alphabet. Object keys come through here
337/// too, a key being a string like any other.
338///
339/// The cursor must already be on the token, as it must be for [`value`].
340#[inline]
341fn copy_token<O: Options>(p: &mut Parser<'_, O>, w: &mut Writer<'_, O>) -> PResult<()> {
342    match p.peek() {
343        Some(b't') => {
344            p.expect_lit(b"true", ErrorCode::ExpectedTrue)?;
345            w.write_bool(true);
346        }
347        Some(b'f') => {
348            p.expect_lit(b"false", ErrorCode::ExpectedFalse)?;
349            w.write_bool(false);
350        }
351        Some(b'n') => {
352            p.expect_lit(b"null", ErrorCode::ExpectedNull)?;
353            w.write_null();
354        }
355        _ => {
356            // `rest` is tied to the input's lifetime rather than to the borrow,
357            // so the token stays reachable across the walk that measures it.
358            let from = p.rest();
359            let start = p.position();
360            p.skip_scalar()?;
361            // Whole tokens out of a `&str`, so the buffer stays valid UTF-8.
362            w.raw_bytes(&from[..p.position() - start]);
363        }
364    }
365    Ok(())
366}