use serde_json::Value;
pub fn assert_no_floats(value: &Value) -> Result<(), String> {
match value {
Value::Number(n) => {
if n.as_i64().is_some() || n.as_u64().is_some() {
return Ok(());
}
if let Some(f) = n.as_f64() {
if !f.is_finite() {
return Err(format!("Non-finite number in V2 canonical form: {}", n));
}
if f.fract() == 0.0 {
return Ok(());
}
return Err(format!("Non-integer number in V2 canonical form: {}", n));
}
Err(format!("Unrepresentable number in V2 canonical form: {}", n))
}
Value::Array(arr) => {
for (i, v) in arr.iter().enumerate() {
assert_no_floats(v).map_err(|e| format!("array[{}]: {}", i, e))?;
}
Ok(())
}
Value::Object(map) => {
for (k, v) in map.iter() {
assert_no_floats(v).map_err(|e| format!("key \"{}\": {}", k, e))?;
}
Ok(())
}
_ => Ok(()),
}
}
pub fn canonicalize_v2(value: &Value) -> Result<String, String> {
assert_no_floats(value)?;
Ok(canonical_recurse(value))
}
fn canonical_recurse(value: &Value) -> String {
match value {
Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
let pairs: Vec<String> = keys
.iter()
.map(|k| {
format!("{}:{}", serde_json::to_string(k).unwrap(), canonical_recurse(&map[*k]))
})
.collect();
format!("{{{}}}", pairs.join(","))
}
Value::Array(arr) => {
let items: Vec<String> = arr.iter().map(canonical_recurse).collect();
format!("[{}]", items.join(","))
}
Value::Number(n) => format_number(n),
_ => serde_json::to_string(value).unwrap(),
}
}
fn format_number(n: &serde_json::Number) -> String {
if let Some(i) = n.as_i64() {
return i.to_string();
}
if let Some(u) = n.as_u64() {
return u.to_string();
}
if let Some(f) = n.as_f64() {
let as_i128 = f as i128;
if (as_i128 as f64) == f {
return as_i128.to_string();
}
return n.to_string();
}
n.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_sorted_keys() {
let val = json!({"z": 1, "a": 2, "m": 3});
let result = canonicalize_v2(&val).unwrap();
assert_eq!(result, r#"{"a":2,"m":3,"z":1}"#);
}
#[test]
fn test_rejects_non_integer_floats() {
assert!(canonicalize_v2(&json!({"score": 0.5})).is_err());
assert!(canonicalize_v2(&json!({"score": 1.5})).is_err());
let v: Value = serde_json::from_str(r#"{"n":1.0e-1}"#).unwrap();
assert!(canonicalize_v2(&v).is_err());
}
#[test]
fn test_integer_ok() {
let val = json!({"score": 500});
assert!(canonicalize_v2(&val).is_ok());
}
#[test]
fn test_nested() {
let val = json!({"b": [3, 1], "a": {"y": true, "x": null}});
let result = canonicalize_v2(&val).unwrap();
assert_eq!(result, r#"{"a":{"x":null,"y":true},"b":[3,1]}"#);
}
#[test]
fn test_accepts_integer_valued_float() {
let v: Value = serde_json::from_str(r#"{"n":1.0}"#).unwrap();
assert_eq!(canonicalize_v2(&v).unwrap(), r#"{"n":1}"#);
}
#[test]
fn test_accepts_trailing_zeros() {
let v: Value = serde_json::from_str(r#"{"n":1.00}"#).unwrap();
assert_eq!(canonicalize_v2(&v).unwrap(), r#"{"n":1}"#);
}
#[test]
fn test_accepts_scientific_notation_with_integer_value() {
let v: Value = serde_json::from_str(r#"{"n":1e2}"#).unwrap();
assert_eq!(canonicalize_v2(&v).unwrap(), r#"{"n":100}"#);
}
#[test]
fn test_rejects_non_finite() {
let inf = serde_json::Number::from_f64(f64::INFINITY);
assert!(inf.is_none());
}
}