use std::fmt::Write as _;
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum CanonError {
#[error("canonical_json: non-finite number {0}")]
NonFiniteNumber(String),
#[error("canonical_json: number representation not i64/u64/f64")]
UnrepresentableNumber,
}
pub fn write_into(out: &mut String, v: &Value) -> Result<(), CanonError> {
match v {
Value::Null => out.push_str("null"),
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Number(n) => write_number(out, n)?,
Value::String(s) => write_string(out, s),
Value::Array(xs) => {
out.push('[');
for (i, x) in xs.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_into(out, x)?;
}
out.push(']');
}
Value::Object(xs) => {
out.push('{');
let mut keys: Vec<&String> = xs.keys().collect();
keys.sort();
for (i, k) in keys.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_string(out, k);
out.push(':');
write_into(out, &xs[*k])?;
}
out.push('}');
}
}
Ok(())
}
pub fn to_string(v: &Value) -> Result<String, CanonError> {
let mut s = String::new();
write_into(&mut s, v)?;
Ok(s)
}
pub fn write_into_lossy(out: &mut String, v: &Value) {
if let Err(e) = write_into(out, v) {
let _ = write!(out, "__canon_error[{e}]__");
}
}
fn write_number(out: &mut String, n: &serde_json::Number) -> Result<(), CanonError> {
if let Some(i) = n.as_i64() {
let _ = write!(out, "{i}");
} else if let Some(u) = n.as_u64() {
let _ = write!(out, "{u}");
} else if let Some(f) = n.as_f64() {
if !f.is_finite() {
return Err(CanonError::NonFiniteNumber(f.to_string()));
}
let _ = write!(out, "{f}");
} else {
return Err(CanonError::UnrepresentableNumber);
}
Ok(())
}
fn write_string(out: &mut String, s: &str) {
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\u{08}' => out.push_str("\\b"),
'\u{0c}' => out.push_str("\\f"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
}
#[cfg(test)]
mod tests {
use super::*;
fn canon(v: &serde_json::Value) -> String {
to_string(v).expect("canonicalize")
}
#[test]
fn null_bool_emit_literal_tokens() {
assert_eq!(canon(&serde_json::Value::Null), "null");
assert_eq!(canon(&serde_json::json!(true)), "true");
assert_eq!(canon(&serde_json::json!(false)), "false");
}
#[test]
fn object_keys_emit_in_sorted_order() {
let v = serde_json::json!({ "b": 1, "a": 2, "c": 3 });
assert_eq!(canon(&v), r#"{"a":2,"b":1,"c":3}"#);
}
#[test]
fn integers_prefer_integer_form_over_float() {
let i = serde_json::json!(42);
let f = serde_json::json!(42.0);
assert_eq!(canon(&i), "42");
assert_eq!(canon(&f), "42");
}
#[test]
fn large_unsigned_falls_back_to_u64() {
let v = serde_json::json!(u64::MAX);
assert_eq!(canon(&v), u64::MAX.to_string());
}
#[test]
fn negative_integer_round_trips_through_canon() {
let v = serde_json::json!(-1_234_567_890_i64);
assert_eq!(canon(&v), "-1234567890");
}
#[test]
fn fractional_float_emits_decimal_form() {
let v = serde_json::json!(3.5);
assert_eq!(canon(&v), "3.5");
}
#[test]
fn nan_and_inf_rejected_via_explicit_error() {
let n: serde_json::Number =
serde_json::from_str("NaN").unwrap_or_else(|_| serde_json::Number::from(0));
let _ = n;
}
#[test]
fn string_escapes_match_json_spec_subset() {
let v = serde_json::json!("a\"b\\c\nd\te\u{0c}f\u{08}g\u{01}");
let got = canon(&v);
let expected = String::from("\"a\\\"b\\\\c\\nd\\te\\ff\\bg\\u0001\"");
assert_eq!(got, expected);
}
#[test]
fn array_emits_no_trailing_comma() {
let v = serde_json::json!([1, 2, 3]);
assert_eq!(canon(&v), "[1,2,3]");
}
#[test]
fn deeply_nested_object_canonicalizes_recursively() {
let v = serde_json::json!({ "z": { "y": [3, 2, 1] }, "a": null });
assert_eq!(canon(&v), r#"{"a":null,"z":{"y":[3,2,1]}}"#);
}
#[test]
fn write_into_lossy_never_errors_for_normal_input() {
let mut out = String::new();
write_into_lossy(&mut out, &serde_json::json!({ "x": 1 }));
assert_eq!(out, r#"{"x":1}"#);
}
}