use std::collections::BTreeMap;
use structio::json::{minify_into, minify_into_with, minify_with};
use structio::{
AllowComments, ErrorCode, Options, Pretty, PrettyInlineArrays, Standard, from_str, minify,
prettify_with, to_string, to_string_with,
};
#[derive(Clone, Copy)]
struct Wide;
impl Options for Wide {
const PRETTY: bool = true;
const INDENT: usize = 4;
}
#[derive(Clone, Copy)]
struct WideInline;
impl Options for WideInline {
const PRETTY: bool = true;
const INDENT: usize = 4;
const NEW_LINES_IN_ARRAYS: bool = false;
}
#[derive(Default)]
struct Inner {
name: String,
vals: Vec<f64>,
flags: Vec<bool>,
}
structio::object!(Inner { name, vals, flags });
#[derive(Default)]
struct Outer {
id: u32,
rows: Vec<Inner>,
empty_array: Vec<u8>,
empty_object: BTreeMap<String, u8>,
nothing: Option<u8>,
nested: Vec<Vec<i32>>,
labels: BTreeMap<String, u8>,
}
structio::object!(Outer {
id,
rows,
empty_array,
empty_object,
nothing,
nested,
labels
});
fn sample() -> Outer {
Outer {
id: 7,
rows: vec![
Inner {
name: "a \"b\" c\t".into(),
vals: vec![1.5, -2.0, 0.0],
flags: vec![true, false],
},
Inner {
name: String::new(),
vals: vec![],
flags: vec![true],
},
],
empty_array: vec![],
empty_object: BTreeMap::new(),
nothing: None,
nested: vec![vec![1, 2], vec![], vec![3]],
labels: [("j k".to_string(), 2), ("l".to_string(), 1)]
.into_iter()
.collect(),
}
}
fn inverts_the_writer<O: Options>(label: &str) {
let value = sample();
let compact = to_string(&value);
let laid_out = to_string_with::<O, _>(&value);
assert_eq!(
minify(&laid_out).unwrap(),
compact,
"{label}: minified text differs from the writer's compact output"
);
assert_eq!(
minify(&laid_out).unwrap(),
prettify_with::<Standard>(&laid_out).unwrap(),
"{label}: the minifier and the compact writer disagree"
);
}
#[test]
fn minifying_is_the_writers_compact_form() {
inverts_the_writer::<Pretty>("Pretty");
inverts_the_writer::<PrettyInlineArrays>("PrettyInlineArrays");
inverts_the_writer::<Wide>("Wide");
inverts_the_writer::<WideInline>("WideInline");
assert_eq!(minify(&to_string(&sample())).unwrap(), to_string(&sample()));
}
#[test]
fn whitespace_between_tokens_is_dropped_wherever_it_is() {
let spaced = " \t\r\n{ \"a\" : [ 1 , 2 ] , \"b\" : { } , \"c\" : [ ] } \n";
assert_eq!(minify(spaced).unwrap(), r#"{"a":[1,2],"b":{},"c":[]}"#);
}
#[test]
fn whitespace_inside_a_string_is_the_documents_own() {
assert_eq!(
minify("[ \" a\\tb \\n c \" ]").unwrap(),
"[\" a\\tb \\n c \"]"
);
for doc in ["[\"a\tb\"]", "[\"a\nb\"]"] {
assert_eq!(minify(doc).unwrap(), doc, "{doc:?}");
assert!(from_str::<Vec<String>>(doc).is_err(), "{doc:?}");
}
}
#[test]
fn structural_bytes_inside_a_string_are_not_structure() {
let doc = r#"{ "{[,:]}" : " } ] " }"#;
assert_eq!(minify(doc).unwrap(), r#"{"{[,:]}":" } ] "}"#);
}
#[test]
fn an_escaped_quote_does_not_end_the_string() {
assert_eq!(minify(r#"[ "a\" b" ]"#).unwrap(), r#"["a\" b"]"#);
assert_eq!(minify(r#"[ "a\\" , 1 ]"#).unwrap(), r#"["a\\",1]"#);
}
#[test]
fn a_string_long_enough_to_cross_the_word_boundary_is_still_one_token() {
let body = "the quick brown fox jumps over the lazy dog, twice over";
let doc = format!("[ \"{body}\" , \"{body}\" ]");
assert_eq!(minify(&doc).unwrap(), format!("[\"{body}\",\"{body}\"]"));
}
#[test]
fn a_token_longer_than_the_largest_copy_block_still_comes_through() {
let long = "x".repeat(300);
let doc = format!("[ \"{long}\" , 1 ]");
assert_eq!(minify(&doc).unwrap(), format!("[\"{long}\",1]"));
let digits = "1".repeat(300);
assert_eq!(
minify(&format!("[ {digits} ]")).unwrap(),
format!("[{digits}]")
);
}
#[test]
fn a_number_keeps_the_spelling_the_input_gave_it() {
let doc = "[ 1.50 , 1e3 , -0.0 , 1E+2 , 00.10 ]";
assert_eq!(minify(doc).unwrap(), "[1.50,1e3,-0.0,1E+2,00.10]");
}
#[test]
fn non_ascii_text_survives_the_copy() {
let doc = "{ \"kéy\" : \"välue \u{1F600}\" }";
assert_eq!(minify(doc).unwrap(), "{\"kéy\":\"välue \u{1F600}\"}");
}
#[test]
fn a_top_level_scalar_is_a_document_too() {
assert_eq!(minify(" 42 ").unwrap(), "42");
assert_eq!(minify(" \"a b\" ").unwrap(), "\"a b\"");
assert_eq!(minify(" true ").unwrap(), "true");
}
#[test]
fn empty_input_has_no_whitespace_to_remove() {
assert_eq!(minify("").unwrap(), "");
assert_eq!(minify(" \n\t ").unwrap(), "");
}
#[test]
fn broken_structure_is_shortened_rather_than_refused() {
for (doc, want) in [
("{ \"a\" : 1 , , , }", "{\"a\":1,,,}"),
("[ 1 , 2", "[1,2"),
("} ] , :", "}],:"),
("{ \"a\" \"b\" }", "{\"a\"\"b\"}"),
("[ 01 ]", "[01]"),
("[ tru ]", "[tru]"),
] {
assert_eq!(minify(doc).unwrap(), want, "{doc:?}");
}
}
#[test]
fn whitespace_holding_two_tokens_apart_is_not_the_layouts_to_remove() {
for (doc, at) in [
("[1 2]", 3),
("[true false]", 6),
("[1\n2]", 3),
("1 2", 2),
("[1 , 2 3]", 7),
] {
let e = minify(doc).unwrap_err();
assert_eq!(e.code, ErrorCode::UnexpectedCharacter, "{doc:?}");
assert_eq!(e.index, at, "{doc:?}");
}
for doc in ["[1 , 2]", "[ 1,2 ]", "{\"a\" : 1}", "[\"a\" \"b\"]"] {
assert!(minify(doc).is_ok(), "{doc:?}");
}
}
#[test]
fn an_unterminated_string_is_an_error_against_the_string() {
let e = minify(r#"{"a": "unclosed"#).unwrap_err();
assert_eq!(e.code, ErrorCode::UnexpectedEnd);
assert_eq!(e.index, 7);
}
#[test]
fn comments_are_read_only_when_asked_for_and_never_written_back() {
let doc = "{ // which\n \"a\" : /* one */ 1 }";
assert_eq!(minify_with::<AllowComments>(doc).unwrap(), r#"{"a":1}"#);
assert_eq!(
minify_with::<Standard>("[ 1 , /*x*/ 2 ]").unwrap(),
"[1,/*x*/2]"
);
}
#[test]
fn a_comment_between_two_bare_tokens_is_holding_them_apart_too() {
let e = minify_with::<AllowComments>("[1 /* two */ 2]").unwrap_err();
assert_eq!(e.code, ErrorCode::UnexpectedCharacter);
assert_eq!(e.index, 13);
}
#[test]
fn a_slash_that_begins_no_comment_is_refused_rather_than_guessed_at() {
for doc in ["[ 1 /* never closed", "[1, /x]", "[1] /"] {
let e = minify_with::<AllowComments>(doc).unwrap_err();
assert_eq!(e.code, ErrorCode::UnexpectedCharacter, "{doc:?}");
assert_eq!(e.index, doc.find('/').unwrap(), "{doc:?}");
}
assert_eq!(minify_with::<Standard>("[ 1 /*x").unwrap(), "[1/*x");
}
#[test]
fn a_slash_inside_a_string_is_not_a_comment() {
let doc = r#"{ "url" : "https://example.com/a//b" }"#;
for out in [
minify_with::<AllowComments>(doc).unwrap(),
minify_with::<Standard>(doc).unwrap(),
] {
assert_eq!(out, r#"{"url":"https://example.com/a//b"}"#);
}
}
#[test]
fn minify_into_replaces_the_contents_and_keeps_the_allocation() {
let mut out = String::with_capacity(4096);
let before = out.capacity();
out.push_str("stale");
minify_into("{ \"a\" : 1 }", &mut out).unwrap();
assert_eq!(out, r#"{"a":1}"#);
minify_into_with::<AllowComments>("[ 1 /* c */ , 2 ]", &mut out).unwrap();
assert_eq!(out, "[1,2]");
assert_eq!(out.capacity(), before, "the allocation was not reused");
}
#[test]
fn a_failed_minify_into_still_hands_the_string_back() {
let mut out = String::new();
let e = minify_into("[1, \"unclosed", &mut out).unwrap_err();
assert_eq!(e.code, ErrorCode::UnexpectedEnd);
assert_eq!(out, "[1,");
minify_into("[ 2 ]", &mut out).unwrap();
assert_eq!(out, "[2]");
}
#[test]
fn a_large_document_minifies_to_what_it_was_written_as() {
let value = Outer {
rows: (0..2000)
.map(|i| Inner {
name: format!("row {i} of many"),
vals: vec![i as f64 / 8.0, -1.0],
flags: vec![i % 2 == 0],
})
.collect(),
..sample()
};
let compact = to_string(&value);
let laid_out = to_string_with::<Pretty, _>(&value);
assert!(laid_out.len() > compact.len());
assert_eq!(minify(&laid_out).unwrap(), compact);
}