use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
pub(crate) fn canonicalize(value: &serde_json::Value) -> serde_json::Value {
match value {
serde_json::Value::Object(map) => {
let sorted: BTreeMap<String, serde_json::Value> = map
.iter()
.map(|(k, v)| (k.clone(), canonicalize(v)))
.collect();
let mut out = serde_json::Map::new();
for (k, v) in sorted {
out.insert(k, v);
}
serde_json::Value::Object(out)
}
serde_json::Value::Array(items) => {
serde_json::Value::Array(items.iter().map(canonicalize).collect())
}
_ => value.clone(),
}
}
pub(crate) fn canonicalize_to_string(value: &serde_json::Value) -> Result<String, String> {
let canon = canonicalize(value);
serde_json::to_string(&canon).map_err(|e| e.to_string())
}
pub(crate) fn canonicalize_string(s: &str) -> String {
match serde_json::from_str::<serde_json::Value>(s) {
Ok(value) => match canonicalize_to_string(&value) {
Ok(canon) => canon,
Err(_) => s.to_string(),
},
Err(_) => s.to_string(),
}
}
pub(crate) fn serialize_verbatim(value: &serde_json::Value) -> Result<String, String> {
serde_json::to_string(value).map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_sorts_top_level() {
let src: serde_json::Value = serde_json::from_str("{\"z\":1,\"a\":2}").unwrap();
assert_eq!(canonicalize_to_string(&src).unwrap(), "{\"a\":2,\"z\":1}");
}
#[test]
fn canonical_recurses() {
let src: serde_json::Value =
serde_json::from_str("{\"z\":{\"b\":1,\"a\":2},\"a\":[3,4]}").unwrap();
assert_eq!(
canonicalize_to_string(&src).unwrap(),
"{\"a\":[3,4],\"z\":{\"a\":2,\"b\":1}}"
);
}
#[test]
fn canonicalize_string_parses_and_reorders() {
assert_eq!(
canonicalize_string("{\"z\":1,\"a\":2}"),
"{\"a\":2,\"z\":1}"
);
}
#[test]
fn canonicalize_string_falls_back_verbatim_on_invalid() {
assert_eq!(canonicalize_string("not json"), "not json");
}
}