accumulate_client/codec/
canonical.rs1#![allow(clippy::unwrap_used)]
8
9use serde_json::{Map, Value};
10use std::collections::BTreeMap;
11
12#[derive(Debug, Clone, Copy)]
14pub struct CanonicalEncoder;
15
16impl CanonicalEncoder {
17 pub fn encode(value: &Value) -> String {
19 Self::encode_value(value)
20 }
21
22 fn encode_value(value: &Value) -> String {
23 match value {
24 Value::Null => "null".to_string(),
25 Value::Bool(b) => b.to_string(),
26 Value::Number(n) => n.to_string(),
27 Value::String(s) => serde_json::to_string(s).unwrap(),
28 Value::Array(arr) => Self::encode_array(arr),
29 Value::Object(obj) => Self::encode_object(obj),
30 }
31 }
32
33 fn encode_array(arr: &[Value]) -> String {
34 let elements: Vec<String> = arr.iter().map(Self::encode_value).collect();
35 format!("[{}]", elements.join(","))
36 }
37
38 fn encode_object(obj: &Map<String, Value>) -> String {
39 let mut sorted_keys: Vec<&String> = obj.keys().collect();
41 sorted_keys.sort();
42
43 let pairs: Vec<String> = sorted_keys
44 .iter()
45 .map(|key| {
46 let key_json = serde_json::to_string(key).unwrap();
47 let value_json = Self::encode_value(obj.get(*key).unwrap());
48 format!("{}:{}", key_json, value_json)
49 })
50 .collect();
51
52 format!("{{{}}}", pairs.join(","))
53 }
54
55 pub fn canonicalize(value: &Value) -> Value {
57 match value {
58 Value::Object(map) => {
59 let mut btree: BTreeMap<String, Value> = BTreeMap::new();
60 for (k, v) in map {
61 btree.insert(k.clone(), Self::canonicalize(v));
62 }
63 Value::Object(Map::from_iter(btree.into_iter()))
64 }
65 Value::Array(arr) => Value::Array(arr.iter().map(Self::canonicalize).collect()),
66 _ => value.clone(),
67 }
68 }
69}
70
71pub fn to_canonical_string(value: &Value) -> String {
73 CanonicalEncoder::encode(value)
74}
75
76pub fn canonicalize(value: &Value) -> Value {
78 CanonicalEncoder::canonicalize(value)
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use serde_json::json;
85
86 #[test]
87 fn test_simple_object_ordering() {
88 let value = json!({ "z": 3, "a": 1, "m": 2 });
89 let canonical = to_canonical_string(&value);
90 assert_eq!(canonical, r#"{"a":1,"m":2,"z":3}"#);
91 }
92
93 #[test]
94 fn test_nested_object_ordering() {
95 let value = json!({
96 "outer": {
97 "z": "last",
98 "a": "first",
99 "m": "middle"
100 },
101 "simple": 42
102 });
103 let canonical = to_canonical_string(&value);
104 assert_eq!(
105 canonical,
106 r#"{"outer":{"a":"first","m":"middle","z":"last"},"simple":42}"#
107 );
108 }
109
110 #[test]
111 fn test_array_preservation() {
112 let value = json!({
113 "array": [
114 { "b": 2, "a": 1 },
115 { "d": 4, "c": 3 }
116 ]
117 });
118 let canonical = to_canonical_string(&value);
119 assert_eq!(canonical, r#"{"array":[{"a":1,"b":2},{"c":3,"d":4}]}"#);
120 }
121
122 #[test]
123 fn test_all_json_types() {
124 let value = json!({
125 "string": "hello",
126 "number": 42.5,
127 "integer": 123,
128 "boolean": true,
129 "null_value": null,
130 "array": [1, 2, 3],
131 "object": { "nested": "value" }
132 });
133
134 let canonical = to_canonical_string(&value);
135 let expected = r#"{"array":[1,2,3],"boolean":true,"integer":123,"null_value":null,"number":42.5,"object":{"nested":"value"},"string":"hello"}"#;
136 assert_eq!(canonical, expected);
137 }
138
139 #[test]
140 fn test_deep_nesting() {
141 let value = json!({
142 "level1": {
143 "z_last": {
144 "z_deeply_nested": "value",
145 "a_deeply_nested": "another"
146 },
147 "a_first": "simple"
148 }
149 });
150
151 let canonical = to_canonical_string(&value);
152 let expected = r#"{"level1":{"a_first":"simple","z_last":{"a_deeply_nested":"another","z_deeply_nested":"value"}}}"#;
153 assert_eq!(canonical, expected);
154 }
155
156 #[test]
157 fn test_empty_structures() {
158 let value = json!({
159 "empty_object": {},
160 "empty_array": [],
161 "filled": { "key": "value" }
162 });
163
164 let canonical = to_canonical_string(&value);
165 let expected = r#"{"empty_array":[],"empty_object":{},"filled":{"key":"value"}}"#;
166 assert_eq!(canonical, expected);
167 }
168
169 #[test]
170 fn test_unicode_strings() {
171 let value = json!({
172 "unicode": "Hello world",
173 "multibyte": "cafe resume",
174 "escape": "line1\nline2\ttab"
175 });
176
177 let canonical = to_canonical_string(&value);
178 assert!(canonical.contains(r#""unicode":"Hello world""#));
180 assert!(canonical.contains(r#""multibyte":"cafe resume""#));
181 assert!(canonical.contains(r#""escape":"line1\nline2\ttab""#));
182 }
183}