goldilocks-json-fmt 0.2.0

A simple, portable, fast, pretty JSON formatter.
Documentation
//! Code to convert an AST into a pretty-formatted string

// `str::push` is non-const, so pushing single-char strings gets compiled down to plain data writes
// and is therefore faster.
#![allow(clippy::single_char_add_str)]

use crate::{Config, Node, NodeKind};

impl Node<'_> {
    /// Convert the AST into a pretty-formatted string
    pub(crate) fn format(&self, config: &Config) -> String {
        let mut s = String::new();
        let mut indentation_str = String::new();
        self.write_to_string(0, &mut indentation_str, &mut s, config);
        s
    }

    fn write_to_string(
        &self,
        indentation: usize,
        indentation_str: &mut String,
        out: &mut String,
        config: &Config,
    ) {
        if self.unsplit_width + indentation < config.width_limit {
            self.fmt_wide(out, config); // If this can fit in one line, then make it 'wide'
        } else {
            self.fmt_tall(indentation_str, out, config);
        }
    }

    fn fmt_wide(&self, out: &mut String, config: &Config) {
        match &self.kind {
            NodeKind::Atom(s) => out.push_str(s), // Atoms are always formatted as-is
            NodeKind::Array(values) => {
                out.push_str("[");
                let mut is_first_time = true;
                for v in values {
                    if !is_first_time {
                        out.push_str(", ");
                    }
                    v.fmt_wide(out, config); // Format all children as also wide
                    is_first_time = false;
                }
                out.push_str("]");
            }
            NodeKind::Object(fields) => {
                out.push_str("{ ");
                let mut is_first_time = true;
                for (key, value) in fields {
                    // Comma for the last value (if it exists)
                    if !is_first_time {
                        out.push_str(", ");
                    }
                    is_first_time = false;
                    // `key: value`
                    out.push_str(key);
                    out.push_str(": ");
                    value.fmt_wide(out, config); // Format all children as also wide
                }
                out.push_str(" }");
            }
        }
    }

    fn fmt_tall(&self, indentation_str: &mut String, out: &mut String, config: &Config) {
        match &self.kind {
            NodeKind::Atom(s) => out.push_str(s), // Atoms are always formatted as-is
            NodeKind::Array(values) => {
                // Leading '['
                out.push_str("[");
                // First value
                let mut val_iter = values.iter();
                if let Some(first_val) = val_iter.next() {
                    // Add an extra indent level
                    for _ in 0..config.indent_width {
                        indentation_str.push_str(" ");
                    }
                    // Add the first value
                    out.push_str("\n");
                    out.push_str(indentation_str);
                    first_val.write_to_string(indentation_str.len(), indentation_str, out, config);
                    // Other values
                    for val in val_iter {
                        // Comma for the previous value
                        out.push_str(",\n");
                        out.push_str(indentation_str);
                        // Next value
                        val.write_to_string(indentation_str.len(), indentation_str, out, config)
                    }
                    // Remove indent level before ']'
                    for _ in 0..config.indent_width {
                        assert!(indentation_str.pop().is_some());
                    }
                }
                // Final `]` on a its own line
                out.push_str("\n");
                out.push_str(indentation_str);
                out.push_str("]");
            }
            NodeKind::Object(fields) => {
                // Add an extra indent level
                for _ in 0..config.indent_width {
                    indentation_str.push_str(" ");
                }
                // Leading '{'
                out.push_str("{");
                // Other values
                let mut is_first_field = true;
                for (key, value) in fields {
                    // Comma for previous value
                    if !is_first_field {
                        out.push_str(",");
                    }
                    is_first_field = false;
                    // New line for the next key
                    out.push_str("\n");
                    out.push_str(indentation_str);
                    // `key: value`
                    out.push_str(key);
                    out.push_str(": ");
                    value.write_to_string(
                        indentation_str.len() + key.len() + 2, // '{indentation}{key}: '
                        indentation_str,
                        out,
                        config,
                    );
                }
                // Remove indent level before '}'
                for _ in 0..config.indent_width {
                    assert!(indentation_str.pop().is_some());
                }
                // Final `]` on a its own line
                out.push_str("\n");
                out.push_str(indentation_str);
                out.push_str("}");
            }
        }
    }
}