use insta::assert_snapshot;
use proptest::prelude::*;
use toon::options::{DecodeOptions, EncodeOptions, ExpandPathsMode, KeyFoldingMode};
use toon::{JsonValue, decode, encode, try_decode};
fn decode_err(input: &str) -> String {
try_decode(input, None)
.err()
.map_or_else(|| "<unexpectedly succeeded>".to_string(), |e| e.to_string())
}
fn decode_strict_err(input: &str) -> String {
try_decode(
input,
Some(DecodeOptions {
indent: None,
strict: Some(true),
expand_paths: None,
}),
)
.err()
.map_or_else(|| "<unexpectedly succeeded>".to_string(), |e| e.to_string())
}
#[test]
fn unicode_emoji_in_values() {
let json: serde_json::Value = serde_json::json!({
"message": "Hello \u{1F600} World \u{1F4BB}",
"hearts": "\u{2764}\u{FE0F}\u{2764}\u{FE0F}\u{2764}\u{FE0F}"
});
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn unicode_emoji_in_keys() {
let json: serde_json::Value = serde_json::json!({
"\u{1F600}": "smile",
"\u{1F4BB}": "laptop"
});
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn unicode_rtl_text() {
let json: serde_json::Value = serde_json::json!({
"arabic": "\u{0627}\u{0644}\u{0639}\u{0631}\u{0628}\u{064A}\u{0629}",
"hebrew": "\u{05E2}\u{05D1}\u{05E8}\u{05D9}\u{05EA}"
});
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn unicode_zero_width_chars() {
let json: serde_json::Value = serde_json::json!({
"zwj": "a\u{200D}b",
"zwnj": "a\u{200C}b",
"zwsp": "a\u{200B}b"
});
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn unicode_combining_chars() {
let json: serde_json::Value = serde_json::json!({
"combined": "e\u{0301}",
"precomposed": "\u{00E9}"
});
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn unicode_surrogate_pairs() {
let json: serde_json::Value = serde_json::json!({
"mathematical": "\u{1D400}\u{1D401}\u{1D402}",
"musical": "\u{1D11E}"
});
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn deeply_nested_objects_100_levels() {
let mut value = serde_json::json!({"leaf": "value"});
for i in 0..100 {
value = serde_json::json!({ format!("level{}", i): value });
}
let toon = encode(value.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(value, decoded_json);
}
#[test]
fn deeply_nested_arrays_100_levels() {
let mut value = serde_json::json!(["leaf"]);
for _ in 0..100 {
value = serde_json::json!([value]);
}
let toon = encode(value.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(value, decoded_json);
}
#[test]
fn deeply_nested_mixed_100_levels() {
let mut value: serde_json::Value = serde_json::json!(42.0);
for i in 0..50 {
if i % 2 == 0 {
value = serde_json::json!({ "obj": value });
} else {
value = serde_json::json!([value]);
}
}
let toon = encode(value.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(value, decoded_json);
}
#[test]
fn very_long_string_value() {
let long_string = "x".repeat(100_000);
let json: serde_json::Value = serde_json::json!({ "content": long_string });
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn very_long_key() {
let long_key = "k".repeat(10_000);
let mut obj = serde_json::Map::new();
obj.insert(long_key, serde_json::json!("value"));
let json = serde_json::Value::Object(obj);
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn string_with_many_escapes() {
let json: serde_json::Value = serde_json::json!({
"escapes": "tab\there\nnewline\rcarriage\\backslash\"quote"
});
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn string_with_control_chars() {
let json: serde_json::Value = serde_json::json!({
"controls": "\u{0001}\u{0002}\u{001F}"
});
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn numeric_max_min_values() {
let json: serde_json::Value = serde_json::json!({
"max_f64": f64::MAX,
"min_positive": f64::MIN_POSITIVE,
"neg_max": -f64::MAX
});
let toon = encode(json, None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
let max = decoded_json["max_f64"].as_f64().unwrap();
assert!((max - f64::MAX).abs() / f64::MAX < 1e-10);
}
#[test]
fn numeric_subnormal() {
let subnormal = f64::MIN_POSITIVE / 2.0;
let json: serde_json::Value = serde_json::json!({ "subnormal": subnormal });
let toon = encode(json, None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
let result = decoded_json["subnormal"].as_f64().unwrap();
assert!((result - subnormal).abs() < 1e-320 || result.abs() < f64::EPSILON);
}
#[test]
fn numeric_zero_variants() {
let json: serde_json::Value = serde_json::json!({
"zero": 0.0,
"neg_zero": -0.0,
"small_pos": 0.000_000_1,
"small_neg": -0.000_000_1
});
let toon = encode(json, None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert!(decoded_json["zero"].as_f64().unwrap().abs() < f64::EPSILON);
}
#[test]
fn numeric_nan_becomes_null() {
let value = JsonValue::Object(vec![
(
"nan".to_string(),
JsonValue::Primitive(toon::StringOrNumberOrBoolOrNull::from_f64(f64::NAN)),
),
(
"valid".to_string(),
JsonValue::Primitive(toon::StringOrNumberOrBoolOrNull::Number(42.0)),
),
]);
let toon = toon::encode::encode(value, None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert!(decoded_json["nan"].is_null());
let valid = decoded_json["valid"].as_f64().unwrap();
assert!((valid - 42.0).abs() < f64::EPSILON);
}
#[test]
fn numeric_infinity_becomes_null() {
let value = JsonValue::Object(vec![
(
"pos_inf".to_string(),
JsonValue::Primitive(toon::StringOrNumberOrBoolOrNull::from_f64(f64::INFINITY)),
),
(
"neg_inf".to_string(),
JsonValue::Primitive(toon::StringOrNumberOrBoolOrNull::from_f64(
f64::NEG_INFINITY,
)),
),
]);
let toon = toon::encode::encode(value, None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert!(decoded_json["pos_inf"].is_null());
assert!(decoded_json["neg_inf"].is_null());
}
#[test]
fn empty_object_at_root() {
let json: serde_json::Value = serde_json::json!({});
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn empty_array_at_root() {
let json: serde_json::Value = serde_json::json!([]);
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn empty_nested_containers() {
let json: serde_json::Value = serde_json::json!({
"empty_obj": {},
"empty_arr": [],
"nested": {
"more_empty": {},
"arr_of_empty": [{}, [], {}]
}
});
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn array_with_mixed_empties() {
let json: serde_json::Value = serde_json::json!([
{},
[],
{"nested": []},
[{}],
null,
"",
[]
]);
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn string_with_only_spaces() {
let json: serde_json::Value = serde_json::json!({
"spaces": " ",
"single": " ",
"mixed": " a b "
});
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn string_with_only_tabs() {
let json: serde_json::Value = serde_json::json!({
"tabs": "\t\t\t",
"mixed": "\ta\tb\t"
});
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn string_with_newlines() {
let json: serde_json::Value = serde_json::json!({
"newlines": "line1\nline2\nline3",
"crlf": "line1\r\nline2"
});
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn array_with_comma_in_strings() {
let json: serde_json::Value = serde_json::json!({
"items": ["a,b", "c,d,e", "f"]
});
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn array_with_pipe_delimiter() {
let json: serde_json::Value = serde_json::json!({
"items": ["a", "b", "c"]
});
let options = Some(EncodeOptions {
indent: None,
delimiter: Some('|'),
key_folding: None,
flatten_depth: None,
replacer: None,
});
let toon = encode(json.clone(), options);
assert!(toon.contains('|'));
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn tabular_array_with_special_chars() {
let json: serde_json::Value = serde_json::json!([
{"name": "Alice, Jr.", "city": "New York"},
{"name": "Bob", "city": "Los Angeles, CA"}
]);
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn key_folding_simple() {
let json: serde_json::Value = serde_json::json!({
"a": {"b": {"c": "value"}}
});
let options = Some(EncodeOptions {
indent: None,
delimiter: None,
key_folding: Some(KeyFoldingMode::Safe),
flatten_depth: None,
replacer: None,
});
let toon = encode(json.clone(), options);
assert!(toon.contains("a.b.c"));
let decode_options = Some(DecodeOptions {
indent: None,
strict: None,
expand_paths: Some(ExpandPathsMode::Safe),
});
let decoded = decode(&toon, decode_options);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn key_folding_with_sibling() {
let json: serde_json::Value = serde_json::json!({
"a": {
"b": {"c": "deep"},
"sibling": "value"
}
});
let options = Some(EncodeOptions {
indent: None,
delimiter: None,
key_folding: Some(KeyFoldingMode::Safe),
flatten_depth: None,
replacer: None,
});
let toon = encode(json.clone(), options);
let decode_options = Some(DecodeOptions {
indent: None,
strict: None,
expand_paths: Some(ExpandPathsMode::Safe),
});
let decoded = decode(&toon, decode_options);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn key_folding_depth_limit() {
let json: serde_json::Value = serde_json::json!({
"a": {"b": {"c": {"d": {"e": "deep"}}}}
});
let options = Some(EncodeOptions {
indent: None,
delimiter: None,
key_folding: Some(KeyFoldingMode::Safe),
flatten_depth: Some(2), replacer: None,
});
let toon = encode(json.clone(), options);
let decode_options = Some(DecodeOptions {
indent: None,
strict: None,
expand_paths: Some(ExpandPathsMode::Safe),
});
let decoded = decode(&toon, decode_options);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn key_with_dots_literal() {
let json: serde_json::Value = serde_json::json!({
"a.b": "literal dot key",
"normal": "value"
});
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn roundtrip_arbitrary_strings(s in ".*") {
let json: serde_json::Value = serde_json::json!({ "value": s });
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
prop_assert_eq!(json, decoded_json);
}
#[test]
fn roundtrip_arbitrary_numbers(n in proptest::num::f64::NORMAL) {
let json: serde_json::Value = serde_json::json!({ "value": n });
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
let orig = json["value"].as_f64().unwrap();
let result = decoded_json["value"].as_f64().unwrap();
if orig.abs() > 1e-10 {
prop_assert!((orig - result).abs() / orig.abs() < 1e-10);
} else {
prop_assert!((orig - result).abs() < 1e-15);
}
}
#[test]
fn roundtrip_string_array(v in proptest::collection::vec(".*", 0..20)) {
let json: serde_json::Value = serde_json::json!({ "items": v });
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
prop_assert_eq!(json, decoded_json);
}
#[test]
fn roundtrip_nested_depth(depth in 1usize..50) {
let mut value = serde_json::json!({"leaf": "value"});
for i in 0..depth {
value = serde_json::json!({ format!("l{}", i): value });
}
let toon = encode(value.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
prop_assert_eq!(value, decoded_json);
}
}
#[test]
fn strict_mode_rejects_tabs() {
let msg = decode_strict_err("\tname: value");
assert_snapshot!(msg);
}
#[test]
fn err_strict_blank_line_inside_list_array() {
let input = "items[2]:\n - a\n\n - b";
assert_snapshot!(decode_strict_err(input));
}
#[test]
fn err_strict_blank_line_inside_tabular() {
let input = "rows[2]{id}:\n 1\n\n 2";
assert_snapshot!(decode_strict_err(input));
}
#[test]
fn err_strict_extra_list_items() {
let input = "items[1]:\n - a\n - b";
assert_snapshot!(decode_strict_err(input));
}
#[test]
fn err_strict_extra_tabular_rows() {
let input = "rows[1]{id}:\n 1\n 2";
assert_snapshot!(decode_strict_err(input));
}
#[test]
fn err_strict_too_few_list_items() {
let input = "items[3]:\n - a";
assert_snapshot!(decode_strict_err(input));
}
#[test]
fn err_strict_too_few_inline_items() {
let input = "items[3]: a,b";
assert_snapshot!(decode_strict_err(input));
}
#[test]
fn err_strict_tabular_row_width_mismatch() {
let input = "rows[1]{a,b,c}:\n 1,2";
assert_snapshot!(decode_strict_err(input));
}
#[test]
fn err_huge_declared_length_rejected() {
let input = "items[9999999999]:";
assert_snapshot!(decode_err(input));
}
#[test]
fn err_unterminated_quoted_string_value() {
let input = "name: \"unterminated";
assert_snapshot!(decode_err(input));
}
#[test]
fn err_unterminated_quoted_key() {
let input = "\"unterminated key: value";
assert_snapshot!(decode_err(input));
}
#[test]
fn err_invalid_escape_in_string() {
let input = "name: \"bad \\x escape\"";
assert_snapshot!(decode_err(input));
}
#[test]
fn err_path_expansion_conflict() {
let input = "a.b: 1\na:\n b: 2";
let msg = try_decode(
input,
Some(DecodeOptions {
indent: None,
strict: Some(true),
expand_paths: Some(ExpandPathsMode::Safe),
}),
)
.err()
.map_or_else(|| "<unexpectedly succeeded>".to_string(), |e| e.to_string());
assert_snapshot!(msg);
}
#[test]
fn err_strict_rejects_tab_indent_in_list() {
let input = "items[1]:\n\t- a";
assert_snapshot!(decode_strict_err(input));
}
#[test]
fn err_strict_indentation_not_multiple() {
let input = "outer:\n a: 1";
assert_snapshot!(decode_strict_err(input));
}
#[test]
fn err_invalid_json_input_to_json_to_toon() {
let msg = toon::json_to_toon("{not: valid json}")
.err()
.map_or_else(|| "<unexpectedly succeeded>".to_string(), |e| e.to_string());
assert_snapshot!(msg);
}
#[test]
fn err_invalid_toon_input_to_toon_to_json() {
let msg = toon::toon_to_json("items[9999999999]:")
.err()
.map_or_else(|| "<unexpectedly succeeded>".to_string(), |e| e.to_string());
assert_snapshot!(msg);
}
#[test]
fn err_expand_paths_exceeds_depth_limit() {
let segments: Vec<&str> = std::iter::repeat_n("a", 400).collect();
let key = segments.join(".");
let input = format!("{key}: 1");
let msg = try_decode(
&input,
Some(DecodeOptions {
indent: None,
strict: Some(true),
expand_paths: Some(ExpandPathsMode::Safe),
}),
)
.err()
.map_or_else(|| "<unexpectedly succeeded>".to_string(), |e| e.to_string());
assert_snapshot!(msg);
}
#[test]
fn err_malformed_array_header_missing_close_bracket() {
let input = "items[3: a,b,c";
assert_snapshot!(decode_err(input));
}
#[test]
fn err_bare_key_missing_colon_treated_as_primitive() {
let result = try_decode("no_colon", None).map(|v| format!("{v:?}"));
let out = match result {
Ok(s) => format!("Ok: {s}"),
Err(e) => format!("Err: {e}"),
};
assert_snapshot!(out);
}
#[test]
fn err_bracket_segment_invalid_length_falls_back() {
let result = try_decode("key[abc]: value", None);
let out = match result {
Ok(v) => format!("Ok: {v:?}"),
Err(e) => format!("Err: {e}"),
};
assert_snapshot!(out);
}
#[test]
fn non_strict_mode_accepts_tabs() {
let toon_with_tabs = "\tname: value";
let result = try_decode(
toon_with_tabs,
Some(DecodeOptions {
indent: None,
strict: Some(false),
expand_paths: None,
}),
);
let _ = result;
}
#[test]
fn key_with_colon() {
let json: serde_json::Value = serde_json::json!({
"time:zone": "UTC",
"key:with:colons": "value"
});
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn key_with_brackets() {
let json: serde_json::Value = serde_json::json!({
"array[0]": "first",
"obj{key}": "value"
});
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn key_with_quotes() {
let json: serde_json::Value = serde_json::json!({
"said \"hello\"": "greeting",
"it's": "fine"
});
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn numeric_keys() {
let json: serde_json::Value = serde_json::json!({
"123": "numeric",
"0": "zero",
"-1": "negative"
});
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}
#[test]
fn boolean_like_keys() {
let json: serde_json::Value = serde_json::json!({
"true": "not a bool",
"false": "also not a bool",
"null": "not null"
});
let toon = encode(json.clone(), None);
let decoded = decode(&toon, None);
let decoded_json: serde_json::Value = decoded.into();
assert_eq!(json, decoded_json);
}