use serde_json::Value;
use unicode_normalization::UnicodeNormalization;
pub fn canonicalize(value: &Value) -> Vec<u8> {
let mut out = String::new();
write_value(value, &mut out);
out.into_bytes()
}
fn write_value(value: &Value, out: &mut String) {
match value {
Value::Null => out.push_str("null"),
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Number(n) => write_number(&n.to_string(), out),
Value::String(s) => write_string(s, out),
Value::Array(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_value(item, out);
}
out.push(']');
}
Value::Object(map) => {
let mut entries: Vec<(String, &Value)> = map
.iter()
.map(|(k, v)| (k.nfc().collect::<String>(), v))
.collect();
entries.sort_by(|a, b| a.0.cmp(&b.0));
out.push('{');
for (i, (k, v)) in entries.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_string(k, out);
out.push(':');
write_value(v, out);
}
out.push('}');
}
}
}
fn write_number(token: &str, out: &mut String) {
let is_float = token.contains('.') || token.contains('e') || token.contains('E');
if !is_float {
out.push_str(token);
return;
}
match token.parse::<f64>() {
Ok(f) if f.is_finite() && f == f.trunc() && !(f == 0.0 && token.starts_with('-')) => {
out.push_str(&format_whole_float(f));
}
Ok(f) if f.is_finite() => {
out.push_str(&format_fraction_float(f));
}
_ => {
out.push_str(token);
}
}
}
fn format_whole_float(f: f64) -> String {
(f as i128).to_string()
}
fn format_fraction_float(f: f64) -> String {
f.to_string()
}
fn write_string(s: &str, out: &mut String) {
out.push('"');
for ch in s.nfc() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\u{08}' => out.push_str("\\b"),
'\u{09}' => out.push_str("\\t"),
'\u{0A}' => out.push_str("\\n"),
'\u{0C}' => out.push_str("\\f"),
'\u{0D}' => out.push_str("\\r"),
c if (c as u32) < 0x20 => {
out.push_str(&format!("\\u{:04x}", c as u32));
}
c => out.push(c),
}
}
out.push('"');
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn whole_float_collapses_to_int() {
let v: Value = serde_json::from_str(r#"{"a":1.0}"#).unwrap();
assert_eq!(canonicalize(&v), br#"{"a":1}"#.to_vec());
}
#[test]
fn fraction_float_preserved() {
let v: Value = serde_json::from_str(r#"{"a":1.5}"#).unwrap();
assert_eq!(canonicalize(&v), br#"{"a":1.5}"#.to_vec());
}
#[test]
fn big_int_full_precision() {
let v: Value = serde_json::from_str(r#"{"a":9007199254740993}"#).unwrap();
assert_eq!(canonicalize(&v), br#"{"a":9007199254740993}"#.to_vec());
}
#[test]
fn keys_sorted_by_codepoint() {
let v = json!({"b": 1, "a": 2});
assert_eq!(canonicalize(&v), br#"{"a":2,"b":1}"#.to_vec());
}
}