#[test]
fn utf16_property_sorting() -> Result<(), vr_jcs::JcsError> {
let input = serde_json::json!({
"\u{20ac}": "Euro",
"\r": "CR",
"1": "One",
"\u{0080}": "Ctrl"
});
let canon = vr_jcs::to_canon_string(&input)?;
let expected = "{\"\\r\":\"CR\",\"1\":\"One\",\"\u{0080}\":\"Ctrl\",\"\u{20ac}\":\"Euro\"}";
assert_eq!(canon, expected);
Ok(())
}
#[test]
fn empty_structures() -> Result<(), vr_jcs::JcsError> {
let input = serde_json::json!({"obj": {}, "arr": []});
let canon = vr_jcs::to_canon_string(&input)?;
assert_eq!(canon, "{\"arr\":[],\"obj\":{}}");
Ok(())
}
#[test]
fn nested_sorting() -> Result<(), vr_jcs::JcsError> {
let input = serde_json::json!({"z": {"z": 1, "a": 2}, "a": 3});
let canon = vr_jcs::to_canon_string(&input)?;
assert_eq!(canon, "{\"a\":3,\"z\":{\"a\":2,\"z\":1}}");
Ok(())
}
#[test]
fn literals() -> Result<(), vr_jcs::JcsError> {
let input = serde_json::json!([null, true, false]);
let canon = vr_jcs::to_canon_string(&input)?;
assert_eq!(canon, "[null,true,false]");
Ok(())
}
#[test]
fn integer_rendering() -> Result<(), vr_jcs::JcsError> {
let input = serde_json::json!([0, 1, -1, 42]);
let canon = vr_jcs::to_canon_string(&input)?;
assert_eq!(canon, "[0,1,-1,42]");
Ok(())
}
#[test]
fn negative_zero_renders_as_zero() -> Result<(), vr_jcs::JcsError> {
let canon = vr_jcs::to_canon_string(&serde_json::json!([-0.0]))?;
assert_eq!(canon, "[0]");
Ok(())
}
#[test]
fn reject_duplicate_keys() {
let result = vr_jcs::to_canon_bytes_from_slice(br#"{"a": 1, "a": 2}"#);
assert!(result.is_err());
assert!(result
.err()
.is_some_and(|e| e.to_string().contains("duplicate property name")));
}
#[test]
fn reject_nested_duplicate_keys() {
let result = vr_jcs::to_canon_bytes_from_slice(br#"{"outer": {"a": 1, "a": 2}}"#);
assert!(result.is_err());
assert!(result
.err()
.is_some_and(|e| e.to_string().contains("duplicate property name")));
}
#[test]
fn reject_noncharacter_u_fdd0() {
let result = vr_jcs::to_canon_string_from_str("{\"bad\":\"\u{fdd0}\"}");
assert!(result.is_err());
assert!(result
.err()
.is_some_and(|e| e.to_string().contains("forbidden noncharacter")));
}
#[test]
fn rfc8785_number_zero() -> Result<(), vr_jcs::JcsError> {
assert_eq!(vr_jcs::to_canon_string(&serde_json::json!(0))?, "0");
assert_eq!(vr_jcs::to_canon_string(&serde_json::json!(0.0))?, "0");
assert_eq!(vr_jcs::to_canon_string(&serde_json::json!(-0.0))?, "0");
Ok(())
}
#[test]
fn rfc8785_number_integers() -> Result<(), vr_jcs::JcsError> {
assert_eq!(vr_jcs::to_canon_string(&serde_json::json!(1))?, "1");
assert_eq!(vr_jcs::to_canon_string(&serde_json::json!(-1))?, "-1");
assert_eq!(vr_jcs::to_canon_string(&serde_json::json!(42))?, "42");
assert_eq!(
vr_jcs::to_canon_string(&serde_json::json!(999_999_999_999_i64))?,
"999999999999"
);
Ok(())
}
#[test]
fn rfc8785_number_fractions() -> Result<(), vr_jcs::JcsError> {
assert_eq!(vr_jcs::to_canon_string(&serde_json::json!(0.5))?, "0.5");
assert_eq!(vr_jcs::to_canon_string(&serde_json::json!(-0.5))?, "-0.5");
assert_eq!(vr_jcs::to_canon_string(&serde_json::json!(1.5))?, "1.5");
assert_eq!(vr_jcs::to_canon_string(&serde_json::json!(0.1))?, "0.1");
Ok(())
}
#[test]
fn rfc8785_shortest_representation() -> Result<(), vr_jcs::JcsError> {
assert_eq!(
vr_jcs::to_canon_string(&serde_json::json!(1e20))?,
"100000000000000000000"
);
assert_eq!(vr_jcs::to_canon_string(&serde_json::json!(1e21))?, "1e+21");
Ok(())
}
#[test]
fn rfc8785_exponential_notation() -> Result<(), vr_jcs::JcsError> {
assert_eq!(
vr_jcs::to_canon_string(&serde_json::json!(1e100))?,
"1e+100"
);
assert_eq!(vr_jcs::to_canon_string(&serde_json::json!(1e-7))?, "1e-7");
assert_eq!(
vr_jcs::to_canon_string(&serde_json::json!(2.220_446_049_250_313e-16))?,
"2.220446049250313e-16"
);
Ok(())
}
#[test]
fn rfc8785_string_escaping() -> Result<(), vr_jcs::JcsError> {
let input = serde_json::json!({"tab": "\t", "newline": "\n", "cr": "\r"});
let canon = vr_jcs::to_canon_string(&input)?;
assert!(canon.contains("\\t"), "tab must be escaped: {canon}");
assert!(canon.contains("\\n"), "newline must be escaped: {canon}");
assert!(canon.contains("\\r"), "cr must be escaped: {canon}");
Ok(())
}
#[test]
fn rfc8785_backslash_and_quote_escaping() -> Result<(), vr_jcs::JcsError> {
let input = serde_json::json!({"bs": "a\\b", "qt": "a\"b"});
let canon = vr_jcs::to_canon_string(&input)?;
assert!(
canon.contains("a\\\\b"),
"backslash must be escaped: {canon}"
);
assert!(canon.contains("a\\\"b"), "quote must be escaped: {canon}");
Ok(())
}
#[test]
fn rfc8785_property_ordering_ascii() -> Result<(), vr_jcs::JcsError> {
let canon = vr_jcs::to_canon_string(&serde_json::json!({"z": 1, "a": 2}))?;
assert_eq!(canon, r#"{"a":2,"z":1}"#);
Ok(())
}
#[test]
fn rfc8785_property_ordering_unicode() -> Result<(), vr_jcs::JcsError> {
let input = serde_json::json!({
"\u{20ac}": "Euro Sign",
"\r": "Carriage Return",
"\u{000a}": "Newline",
"1": "One"
});
let canon = vr_jcs::to_canon_string(&input)?;
assert!(
canon.find("Newline") < canon.find("Carriage Return"),
"\\n before \\r: {canon}"
);
assert!(
canon.find("Carriage Return") < canon.find("One"),
"\\r before 1: {canon}"
);
assert!(
canon.find("One") < canon.find("Euro Sign"),
"1 before €: {canon}"
);
Ok(())
}
#[test]
fn rfc8785_duplicate_rejection() {
let result = vr_jcs::to_canon_bytes_from_slice(br#"{"dup": 1, "dup": 2}"#);
assert!(result.is_err());
}
#[test]
fn rfc8785_literals() -> Result<(), vr_jcs::JcsError> {
assert_eq!(vr_jcs::to_canon_string(&serde_json::json!(null))?, "null");
assert_eq!(vr_jcs::to_canon_string(&serde_json::json!(true))?, "true");
assert_eq!(vr_jcs::to_canon_string(&serde_json::json!(false))?, "false");
Ok(())
}
#[test]
#[allow(clippy::excessive_precision)]
fn rfc8785_composite_example() -> Result<(), vr_jcs::JcsError> {
let input = serde_json::json!({
"numbers": [333_333_333.333_333_29, 1e30, 4.50, 2e-3, 0.000_000_000_000_000_000_000_000_001],
"string": "\u{20ac}$\u{000f}\u{000a}A'\u{0042}\\\\\"/",
"literals": [null, true, false]
});
let canon = vr_jcs::to_canon_string(&input)?;
let _: serde_json::Value = serde_json::from_str(&canon).map_err(vr_jcs::JcsError::from)?;
assert!(
canon.find("\"literals\"") < canon.find("\"numbers\""),
"literals before numbers: {canon}"
);
assert!(
canon.find("\"numbers\"") < canon.find("\"string\""),
"numbers before string: {canon}"
);
Ok(())
}