use std::collections::HashMap;
use serde_json::{Map, Value};
pub const TRANSFORM_ID: &str = "json_value_dict";
pub const TRANSFORM_VERSION: &str = "1.0.0";
const DICT: &str = "__tf_dict__";
const DATA: &str = "__tf_data__";
const REF: &str = "__tf_ref__";
const MIN_VALUE_BYTES: usize = 24;
const MIN_COUNT: usize = 2;
#[derive(Debug, thiserror::Error)]
pub enum JsonDictError {
#[error("invalid json: {0}")]
Invalid(#[from] serde_json::Error),
}
pub fn dict_json(input: &[u8]) -> Result<Vec<u8>, JsonDictError> {
if input.is_empty() {
return Ok(Vec::new());
}
let value: Value = serde_json::from_slice(input)?;
let mut counts: HashMap<String, usize> = HashMap::new();
count_dictable(&value, &mut counts);
let mut index: Vec<String> = Vec::new();
let mut index_of: HashMap<String, usize> = HashMap::new();
let data = replace(&value, &counts, &mut index, &mut index_of);
if index.is_empty() {
return Ok(input.to_vec());
}
let dict: Vec<Value> = index
.iter()
.map(|s| serde_json::from_str(s).expect("canonical string from our own to_string"))
.collect();
let mut wrapper = Map::new();
wrapper.insert(DICT.to_string(), Value::Array(dict));
wrapper.insert(DATA.to_string(), data);
Ok(serde_json::to_vec(&Value::Object(wrapper))?)
}
pub fn undict_json(input: &[u8]) -> Result<Vec<u8>, JsonDictError> {
if input.is_empty() {
return Ok(Vec::new());
}
let value: Value = serde_json::from_slice(input)?;
let restored = restore(&value);
Ok(serde_json::to_vec(&restored)?)
}
pub fn round_trips(before: &[u8], after: &[u8]) -> bool {
let (Ok(before_v), Ok(after_v)) = (
serde_json::from_slice::<Value>(before),
serde_json::from_slice::<Value>(after),
) else {
return false;
};
restore(&after_v) == before_v
}
fn dict_key(v: &Value) -> Option<String> {
if !matches!(v, Value::Object(_) | Value::Array(_) | Value::String(_)) {
return None;
}
let key = serde_json::to_string(v).ok()?;
(key.len() >= MIN_VALUE_BYTES).then_some(key)
}
fn count_dictable(v: &Value, counts: &mut HashMap<String, usize>) {
if let Some(k) = dict_key(v) {
*counts.entry(k).or_insert(0) += 1;
}
match v {
Value::Object(m) => m.values().for_each(|val| count_dictable(val, counts)),
Value::Array(a) => a.iter().for_each(|item| count_dictable(item, counts)),
_ => {}
}
}
fn replace(
v: &Value,
counts: &HashMap<String, usize>,
index: &mut Vec<String>,
index_of: &mut HashMap<String, usize>,
) -> Value {
if let Some(k) = dict_key(v)
&& counts.get(&k).copied().unwrap_or(0) >= MIN_COUNT
{
let idx = *index_of.entry(k.clone()).or_insert_with(|| {
index.push(k);
index.len() - 1
});
let mut r = Map::new();
r.insert(REF.to_string(), Value::from(idx as u64));
return Value::Object(r);
}
match v {
Value::Object(m) => {
let mut o = Map::new();
for (kk, val) in m {
o.insert(kk.clone(), replace(val, counts, index, index_of));
}
Value::Object(o)
}
Value::Array(a) => Value::Array(a.iter().map(|it| replace(it, counts, index, index_of)).collect()),
_ => v.clone(),
}
}
fn restore(v: &Value) -> Value {
let Some((dict, data)) = as_wrapper(v) else {
return v.clone();
};
expand(data, dict)
}
fn as_wrapper(v: &Value) -> Option<(&Vec<Value>, &Value)> {
let m = v.as_object()?;
if m.len() != 2 {
return None;
}
let dict = m.get(DICT)?.as_array()?;
let data = m.get(DATA)?;
Some((dict, data))
}
fn expand(v: &Value, dict: &[Value]) -> Value {
if let Some(idx) = as_ref(v) {
return dict.get(idx).cloned().unwrap_or_else(|| v.clone());
}
match v {
Value::Object(m) => {
let mut o = Map::new();
for (k, val) in m {
o.insert(k.clone(), expand(val, dict));
}
Value::Object(o)
}
Value::Array(a) => Value::Array(a.iter().map(|it| expand(it, dict)).collect()),
_ => v.clone(),
}
}
fn as_ref(v: &Value) -> Option<usize> {
let m = v.as_object()?;
if m.len() != 1 {
return None;
}
Some(m.get(REF)?.as_u64()? as usize)
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(b: &[u8]) -> Value {
serde_json::from_slice(b).unwrap()
}
#[test]
fn dedups_a_repeated_large_object() {
let obj = r#"{"kind":"widget","color":"blue","size":"large"}"#;
let input = format!(r#"{{"a":{obj},"b":{obj},"c":{obj}}}"#);
let out = dict_json(input.as_bytes()).unwrap();
let s = String::from_utf8(out.clone()).unwrap();
assert!(s.contains("__tf_dict__"), "expected dict form, got {s}");
assert_eq!(s.matches("widget").count(), 1);
assert!(round_trips(input.as_bytes(), &out));
}
#[test]
fn dict_then_undict_reproduces_original() {
let obj = r#"{"kind":"widget","color":"blue","size":"large"}"#;
let input = format!(r#"[{obj},{obj},{obj},{obj}]"#);
let folded = dict_json(input.as_bytes()).unwrap();
let back = undict_json(&folded).unwrap();
assert_eq!(parse(&back), parse(input.as_bytes()));
}
#[test]
fn short_repeated_values_are_left_alone() {
let input = br#"{"a":"member","b":"member","c":"member"}"#;
let out = dict_json(input).unwrap();
assert_eq!(parse(&out), parse(input));
assert!(!String::from_utf8(out).unwrap().contains("__tf_dict__"));
}
#[test]
fn single_occurrence_is_not_dictionaried() {
let input = br#"{"only":{"a":"quite a long value here indeed yes"}}"#;
let out = dict_json(input).unwrap();
assert!(!String::from_utf8(out).unwrap().contains("__tf_dict__"));
}
#[test]
fn nested_repeats_do_not_create_dead_entries() {
let inner = r#"{"deeply":"nested repeated value that is long"}"#;
let parent = format!(r#"{{"p":{inner},"q":"x"}}"#);
let input = format!(r#"[{parent},{parent}]"#);
let out = dict_json(input.as_bytes()).unwrap();
assert!(round_trips(input.as_bytes(), &out));
}
#[test]
fn empty_and_invalid_inputs() {
assert!(dict_json(b"").unwrap().is_empty());
assert!(dict_json(b"{bad").is_err());
}
use proptest::prelude::*;
fn arb_json() -> impl Strategy<Value = Value> {
let leaf = prop_oneof![
Just(Value::Null),
any::<bool>().prop_map(Value::Bool),
any::<i64>().prop_map(|n| Value::Number(n.into())),
"[^\"\\\\]{0,20}".prop_map(Value::String),
];
leaf.prop_recursive(4, 48, 6, |inner| {
prop_oneof![
prop::collection::vec(inner.clone(), 0..6).prop_map(Value::Array),
prop::collection::hash_map("[a-z]{1,6}", inner, 0..6)
.prop_map(|m| Value::Object(m.into_iter().collect())),
]
})
}
proptest! {
#[test]
fn dict_then_undict_is_the_identity_on_arbitrary_json(v in arb_json()) {
let bytes = serde_json::to_vec(&v).unwrap();
let dicted = dict_json(&bytes).unwrap();
prop_assert!(round_trips(&bytes, &dicted));
let back: Value = serde_json::from_slice(&undict_json(&dicted).unwrap()).unwrap();
prop_assert_eq!(back, v);
}
}
}