tokenfold_core/transforms/
json_dict.rs1use std::collections::HashMap;
23
24use serde_json::{Map, Value};
25
26pub const TRANSFORM_ID: &str = "json_value_dict";
28
29pub const TRANSFORM_VERSION: &str = "1.0.0";
31
32const DICT: &str = "__tf_dict__";
33const DATA: &str = "__tf_data__";
34const REF: &str = "__tf_ref__";
35
36const MIN_VALUE_BYTES: usize = 24;
40
41const MIN_COUNT: usize = 2;
43
44#[derive(Debug, thiserror::Error)]
45pub enum JsonDictError {
46 #[error("invalid json: {0}")]
47 Invalid(#[from] serde_json::Error),
48}
49
50pub fn dict_json(input: &[u8]) -> Result<Vec<u8>, JsonDictError> {
53 if input.is_empty() {
54 return Ok(Vec::new());
55 }
56 let value: Value = serde_json::from_slice(input)?;
57
58 let mut counts: HashMap<String, usize> = HashMap::new();
59 count_dictable(&value, &mut counts);
60
61 let mut index: Vec<String> = Vec::new();
62 let mut index_of: HashMap<String, usize> = HashMap::new();
63 let data = replace(&value, &counts, &mut index, &mut index_of);
64
65 if index.is_empty() {
66 return Ok(input.to_vec());
67 }
68 let dict: Vec<Value> = index
69 .iter()
70 .map(|s| serde_json::from_str(s).expect("canonical string from our own to_string"))
71 .collect();
72 let mut wrapper = Map::new();
73 wrapper.insert(DICT.to_string(), Value::Array(dict));
74 wrapper.insert(DATA.to_string(), data);
75 Ok(serde_json::to_vec(&Value::Object(wrapper))?)
76}
77
78pub fn undict_json(input: &[u8]) -> Result<Vec<u8>, JsonDictError> {
80 if input.is_empty() {
81 return Ok(Vec::new());
82 }
83 let value: Value = serde_json::from_slice(input)?;
84 let restored = restore(&value);
85 Ok(serde_json::to_vec(&restored)?)
86}
87
88pub fn round_trips(before: &[u8], after: &[u8]) -> bool {
91 let (Ok(before_v), Ok(after_v)) = (
92 serde_json::from_slice::<Value>(before),
93 serde_json::from_slice::<Value>(after),
94 ) else {
95 return false;
96 };
97 restore(&after_v) == before_v
98}
99
100fn dict_key(v: &Value) -> Option<String> {
103 if !matches!(v, Value::Object(_) | Value::Array(_) | Value::String(_)) {
104 return None;
105 }
106 let key = serde_json::to_string(v).ok()?;
107 (key.len() >= MIN_VALUE_BYTES).then_some(key)
108}
109
110fn count_dictable(v: &Value, counts: &mut HashMap<String, usize>) {
111 if let Some(k) = dict_key(v) {
112 *counts.entry(k).or_insert(0) += 1;
113 }
114 match v {
115 Value::Object(m) => m.values().for_each(|val| count_dictable(val, counts)),
116 Value::Array(a) => a.iter().for_each(|item| count_dictable(item, counts)),
117 _ => {}
118 }
119}
120
121fn replace(
126 v: &Value,
127 counts: &HashMap<String, usize>,
128 index: &mut Vec<String>,
129 index_of: &mut HashMap<String, usize>,
130) -> Value {
131 if let Some(k) = dict_key(v)
132 && counts.get(&k).copied().unwrap_or(0) >= MIN_COUNT
133 {
134 let idx = *index_of.entry(k.clone()).or_insert_with(|| {
135 index.push(k);
136 index.len() - 1
137 });
138 let mut r = Map::new();
139 r.insert(REF.to_string(), Value::from(idx as u64));
140 return Value::Object(r);
141 }
142 match v {
143 Value::Object(m) => {
144 let mut o = Map::new();
145 for (kk, val) in m {
146 o.insert(kk.clone(), replace(val, counts, index, index_of));
147 }
148 Value::Object(o)
149 }
150 Value::Array(a) => Value::Array(a.iter().map(|it| replace(it, counts, index, index_of)).collect()),
151 _ => v.clone(),
152 }
153}
154
155fn restore(v: &Value) -> Value {
158 let Some((dict, data)) = as_wrapper(v) else {
159 return v.clone();
160 };
161 expand(data, dict)
162}
163
164fn as_wrapper(v: &Value) -> Option<(&Vec<Value>, &Value)> {
165 let m = v.as_object()?;
166 if m.len() != 2 {
167 return None;
168 }
169 let dict = m.get(DICT)?.as_array()?;
170 let data = m.get(DATA)?;
171 Some((dict, data))
172}
173
174fn expand(v: &Value, dict: &[Value]) -> Value {
175 if let Some(idx) = as_ref(v) {
176 return dict.get(idx).cloned().unwrap_or_else(|| v.clone());
179 }
180 match v {
181 Value::Object(m) => {
182 let mut o = Map::new();
183 for (k, val) in m {
184 o.insert(k.clone(), expand(val, dict));
185 }
186 Value::Object(o)
187 }
188 Value::Array(a) => Value::Array(a.iter().map(|it| expand(it, dict)).collect()),
189 _ => v.clone(),
190 }
191}
192
193fn as_ref(v: &Value) -> Option<usize> {
194 let m = v.as_object()?;
195 if m.len() != 1 {
196 return None;
197 }
198 Some(m.get(REF)?.as_u64()? as usize)
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 fn parse(b: &[u8]) -> Value {
206 serde_json::from_slice(b).unwrap()
207 }
208
209 #[test]
210 fn dedups_a_repeated_large_object() {
211 let obj = r#"{"kind":"widget","color":"blue","size":"large"}"#;
212 let input = format!(r#"{{"a":{obj},"b":{obj},"c":{obj}}}"#);
213 let out = dict_json(input.as_bytes()).unwrap();
214 let s = String::from_utf8(out.clone()).unwrap();
215 assert!(s.contains("__tf_dict__"), "expected dict form, got {s}");
216 assert_eq!(s.matches("widget").count(), 1);
218 assert!(round_trips(input.as_bytes(), &out));
219 }
220
221 #[test]
222 fn dict_then_undict_reproduces_original() {
223 let obj = r#"{"kind":"widget","color":"blue","size":"large"}"#;
224 let input = format!(r#"[{obj},{obj},{obj},{obj}]"#);
225 let folded = dict_json(input.as_bytes()).unwrap();
226 let back = undict_json(&folded).unwrap();
227 assert_eq!(parse(&back), parse(input.as_bytes()));
228 }
229
230 #[test]
231 fn short_repeated_values_are_left_alone() {
232 let input = br#"{"a":"member","b":"member","c":"member"}"#;
234 let out = dict_json(input).unwrap();
235 assert_eq!(parse(&out), parse(input));
236 assert!(!String::from_utf8(out).unwrap().contains("__tf_dict__"));
237 }
238
239 #[test]
240 fn single_occurrence_is_not_dictionaried() {
241 let input = br#"{"only":{"a":"quite a long value here indeed yes"}}"#;
242 let out = dict_json(input).unwrap();
243 assert!(!String::from_utf8(out).unwrap().contains("__tf_dict__"));
244 }
245
246 #[test]
247 fn nested_repeats_do_not_create_dead_entries() {
248 let inner = r#"{"deeply":"nested repeated value that is long"}"#;
249 let parent = format!(r#"{{"p":{inner},"q":"x"}}"#);
250 let input = format!(r#"[{parent},{parent}]"#);
251 let out = dict_json(input.as_bytes()).unwrap();
252 assert!(round_trips(input.as_bytes(), &out));
253 }
254
255 #[test]
256 fn empty_and_invalid_inputs() {
257 assert!(dict_json(b"").unwrap().is_empty());
258 assert!(dict_json(b"{bad").is_err());
259 }
260
261 use proptest::prelude::*;
262
263 fn arb_json() -> impl Strategy<Value = Value> {
264 let leaf = prop_oneof![
265 Just(Value::Null),
266 any::<bool>().prop_map(Value::Bool),
267 any::<i64>().prop_map(|n| Value::Number(n.into())),
268 "[^\"\\\\]{0,20}".prop_map(Value::String),
269 ];
270 leaf.prop_recursive(4, 48, 6, |inner| {
271 prop_oneof![
272 prop::collection::vec(inner.clone(), 0..6).prop_map(Value::Array),
273 prop::collection::hash_map("[a-z]{1,6}", inner, 0..6)
274 .prop_map(|m| Value::Object(m.into_iter().collect())),
275 ]
276 })
277 }
278
279 proptest! {
280 #[test]
281 fn dict_then_undict_is_the_identity_on_arbitrary_json(v in arb_json()) {
282 let bytes = serde_json::to_vec(&v).unwrap();
283 let dicted = dict_json(&bytes).unwrap();
284 prop_assert!(round_trips(&bytes, &dicted));
285 let back: Value = serde_json::from_slice(&undict_json(&dicted).unwrap()).unwrap();
286 prop_assert_eq!(back, v);
287 }
288 }
289}