mirror of
https://github.com/Noratrieb/jsonformat.git
synced 2026-01-14 14:15:03 +01:00
248 lines
6.1 KiB
Rust
248 lines
6.1 KiB
Rust
//!
|
|
//! jsonformat is a library for formatting json.
|
|
//!
|
|
//! It does not do anything more than that, which makes it so fast.
|
|
|
|
use std::{
|
|
io,
|
|
io::{Read, Write},
|
|
};
|
|
|
|
/// Set the indentation used for the formatting.
|
|
///
|
|
/// Note: It is *not* recommended to set indentation to anything oder than some spaces or some tabs,
|
|
/// but nothing is stopping you from doing that.
|
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
|
pub enum Indentation<'a> {
|
|
/// Fast path for two spaces
|
|
TwoSpace,
|
|
/// Fast path for four spaces
|
|
FourSpace,
|
|
/// Fast path for tab
|
|
Tab,
|
|
/// Use a custom indentation String
|
|
Custom(&'a str),
|
|
}
|
|
|
|
impl Default for Indentation<'_> {
|
|
fn default() -> Self {
|
|
Self::TwoSpace
|
|
}
|
|
}
|
|
|
|
/// # Formats a json string
|
|
///
|
|
/// The indentation can be set to any value using [`Indentation`]
|
|
/// The default value is two spaces
|
|
/// The default indentation is faster than a custom one
|
|
pub fn format(json: &str, indentation: Indentation) -> String {
|
|
let mut reader = json.as_bytes();
|
|
let mut writer = Vec::with_capacity(json.len());
|
|
|
|
format_reader_writer(&mut reader, &mut writer, indentation).unwrap();
|
|
String::from_utf8(writer).unwrap()
|
|
}
|
|
|
|
/// # Formats a json string
|
|
///
|
|
/// The indentation can be set to any value using [`Indentation`]
|
|
/// The default value is two spaces
|
|
/// The default indentation is faster than a custom one
|
|
pub fn format_reader_writer<R, W>(
|
|
reader: R,
|
|
mut writer: W,
|
|
indentation: Indentation,
|
|
) -> io::Result<()>
|
|
where
|
|
R: Read,
|
|
W: Write,
|
|
{
|
|
let mut escaped = false;
|
|
let mut in_string = false;
|
|
let mut indent_level = 0usize;
|
|
let mut newline_requested = false; // invalidated if next character is ] or }
|
|
|
|
for char in reader.bytes() {
|
|
let char = char?;
|
|
if in_string {
|
|
let mut escape_here = false;
|
|
match char {
|
|
b'"' => {
|
|
if !escaped {
|
|
in_string = false;
|
|
}
|
|
}
|
|
b'\\' => {
|
|
if !escaped {
|
|
escape_here = true;
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
writer.write_all(&[char])?;
|
|
escaped = escape_here;
|
|
} else {
|
|
let mut auto_push = true;
|
|
let mut request_newline = false;
|
|
let old_level = indent_level;
|
|
|
|
match char {
|
|
b'"' => in_string = true,
|
|
b' ' | b'\n' | b'\t' => continue,
|
|
b'[' => {
|
|
indent_level += 1;
|
|
request_newline = true;
|
|
}
|
|
b'{' => {
|
|
indent_level += 1;
|
|
request_newline = true;
|
|
}
|
|
b'}' | b']' => {
|
|
indent_level = indent_level.saturating_sub(1);
|
|
if !newline_requested {
|
|
// see comment below about newline_requested
|
|
writer.write_all(b"\n")?;
|
|
indent(&mut writer, indent_level, indentation)?;
|
|
}
|
|
}
|
|
b':' => {
|
|
auto_push = false;
|
|
writer.write_all(&[char])?;
|
|
writer.write_all(&[b' '])?;
|
|
}
|
|
b',' => {
|
|
request_newline = true;
|
|
}
|
|
_ => {}
|
|
}
|
|
if newline_requested && char != b']' && char != b'}' {
|
|
// newline only happens after { [ and ,
|
|
// this means we can safely assume that it being followed up by } or ]
|
|
// means an empty object/array
|
|
writer.write_all(b"\n")?;
|
|
indent(&mut writer, old_level, indentation)?;
|
|
}
|
|
|
|
if auto_push {
|
|
writer.write_all(&[char])?;
|
|
}
|
|
|
|
newline_requested = request_newline;
|
|
}
|
|
}
|
|
|
|
// trailing newline
|
|
writer.write_all(b"\n")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn indent<W>(writer: &mut W, level: usize, indent_str: Indentation) -> io::Result<()>
|
|
where
|
|
W: Write,
|
|
{
|
|
for _ in 0..level {
|
|
match indent_str {
|
|
Indentation::TwoSpace => {
|
|
writer.write_all(b" ")?;
|
|
}
|
|
Indentation::FourSpace => {
|
|
writer.write_all(b" ")?;
|
|
}
|
|
Indentation::Tab => {
|
|
writer.write_all(b"\t")?;
|
|
}
|
|
Indentation::Custom(indent) => {
|
|
writer.write_all(indent.as_bytes())?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn echoes_primitive() {
|
|
let json = "1.35\n";
|
|
assert_eq!(json, format(json, Indentation::TwoSpace));
|
|
}
|
|
|
|
#[test]
|
|
fn ignore_whitespace_in_string() {
|
|
let json = "\" hallo \"\n";
|
|
assert_eq!(json, format(json, Indentation::TwoSpace));
|
|
}
|
|
|
|
#[test]
|
|
fn remove_leading_whitespace() {
|
|
let json = " 0";
|
|
let expected = "0\n";
|
|
assert_eq!(expected, format(json, Indentation::TwoSpace));
|
|
}
|
|
|
|
#[test]
|
|
fn handle_escaped_strings() {
|
|
let json = " \" hallo \\\" \" ";
|
|
let expected = "\" hallo \\\" \"\n";
|
|
assert_eq!(expected, format(json, Indentation::TwoSpace));
|
|
}
|
|
|
|
#[test]
|
|
fn simple_object() {
|
|
let json = "{\"a\":0}";
|
|
let expected = "{
|
|
\"a\": 0
|
|
}
|
|
";
|
|
assert_eq!(expected, format(json, Indentation::TwoSpace));
|
|
}
|
|
|
|
#[test]
|
|
fn simple_array() {
|
|
let json = "[1,2,null]";
|
|
let expected = "[
|
|
1,
|
|
2,
|
|
null
|
|
]
|
|
";
|
|
assert_eq!(expected, format(json, Indentation::TwoSpace));
|
|
}
|
|
|
|
#[test]
|
|
fn array_of_object() {
|
|
let json = "[{\"a\": 0}, {}, {\"a\": null}]";
|
|
let expected = "[
|
|
{
|
|
\"a\": 0
|
|
},
|
|
{},
|
|
{
|
|
\"a\": null
|
|
}
|
|
]
|
|
";
|
|
|
|
assert_eq!(expected, format(json, Indentation::TwoSpace));
|
|
}
|
|
|
|
#[test]
|
|
fn already_formatted() {
|
|
let expected = "[
|
|
{
|
|
\"a\": 0
|
|
},
|
|
{},
|
|
{
|
|
\"a\": null
|
|
}
|
|
]
|
|
";
|
|
|
|
assert_eq!(expected, format(expected, Indentation::TwoSpace));
|
|
}
|
|
}
|