use serde_json::{Map, Value};
use std::collections::{BTreeMap, BTreeSet};
const MARKER: &str = "_lc_crush";
const MARKER_ARRAY: &str = "arr";
const DEFAULTS_KEY: &str = "_defaults";
const DROPPED_KEY: &str = "_dropped";
const ITEMS_KEY: &str = "_items";
const MIN_ITEMS: usize = 3;
const MIN_DOMINANCE: f64 = 0.5;
pub const KEEP_DATA_DIVISOR: usize = 2;
#[derive(Debug, Clone)]
pub struct CrushResult {
pub text: String,
pub lossless: bool,
}
#[derive(Debug, Clone)]
pub struct CrushOpts {
pub drop_entropy: f64,
pub max_depth: usize,
pub max_items: usize,
}
impl Default for CrushOpts {
fn default() -> Self {
Self {
drop_entropy: 1.0,
max_depth: 64,
max_items: 100_000,
}
}
}
impl CrushOpts {
pub fn lossless() -> Self {
Self::default()
}
pub fn lossy(drop_entropy: f64) -> Self {
Self {
drop_entropy: drop_entropy.clamp(0.0, 1.0),
..Self::default()
}
}
}
pub fn crush_lossless(value: &Value) -> Option<CrushResult> {
crush_with(value, &CrushOpts::lossless())
}
pub fn crush_lossy(value: &Value, opts: &CrushOpts) -> Option<CrushResult> {
crush_with(value, opts)
}
pub fn crush_value_if_beneficial(value: &Value, raw_len: usize) -> Option<String> {
let crushed = crush_lossless(value)?;
(crushed.text.len().saturating_mul(KEEP_DATA_DIVISOR) <= raw_len).then_some(crushed.text)
}
pub fn crush_text_if_beneficial(text: &str) -> Option<String> {
let trimmed = text.trim();
if !trimmed.starts_with('{') && !trimmed.starts_with('[') {
return None;
}
let val: Value = serde_json::from_str(trimmed).ok()?;
crush_value_if_beneficial(&val, trimmed.len())
}
pub fn crush_text_lossy_if_beneficial(text: &str, drop_entropy: f64) -> Option<CrushResult> {
let trimmed = text.trim();
if !trimmed.starts_with('{') && !trimmed.starts_with('[') {
return None;
}
let val: Value = serde_json::from_str(trimmed).ok()?;
let res = crush_lossy(&val, &CrushOpts::lossy(drop_entropy))?;
(!res.lossless && res.text.len().saturating_mul(KEEP_DATA_DIVISOR) <= trimmed.len())
.then_some(res)
}
pub fn reconstruct(crushed_text: &str) -> Option<Value> {
let v: Value = serde_json::from_str(crushed_text).ok()?;
Some(uncrush_node(&v))
}
fn crush_with(value: &Value, opts: &CrushOpts) -> Option<CrushResult> {
if contains_marker(value) {
return None;
}
let crushed = crush_node(value, opts, 0);
if !crushed.changed {
return None;
}
let text = serde_json::to_string(&crushed.value).ok()?;
Some(CrushResult {
text,
lossless: crushed.lossless,
})
}
struct Crushed {
value: Value,
changed: bool,
lossless: bool,
}
impl Crushed {
fn unchanged(value: Value) -> Self {
Self {
value,
changed: false,
lossless: true,
}
}
}
fn crush_node(value: &Value, opts: &CrushOpts, depth: usize) -> Crushed {
if depth > opts.max_depth {
return Crushed::unchanged(value.clone());
}
match value {
Value::Array(arr) => crush_array(arr, opts, depth),
Value::Object(map) => {
let mut out = Map::new();
let mut changed = false;
let mut lossless = true;
for (key, val) in map {
let child = crush_node(val, opts, depth + 1);
changed |= child.changed;
lossless &= child.lossless;
out.insert(key.clone(), child.value);
}
Crushed {
value: Value::Object(out),
changed,
lossless,
}
}
other => Crushed::unchanged(other.clone()),
}
}
fn crush_array(arr: &[Value], opts: &CrushOpts, depth: usize) -> Crushed {
let mut items: Vec<Value> = Vec::with_capacity(arr.len());
let mut child_changed = false;
let mut child_lossless = true;
for el in arr {
let child = crush_node(el, opts, depth + 1);
child_changed |= child.changed;
child_lossless &= child.lossless;
items.push(child.value);
}
let factorable = items.len() >= MIN_ITEMS
&& items.len() <= opts.max_items
&& items.iter().all(Value::is_object);
if !factorable {
return Crushed {
value: Value::Array(items),
changed: child_changed,
lossless: child_lossless,
};
}
let mut candidates: BTreeSet<String> = items[0]
.as_object()
.map(|o| o.keys().cloned().collect())
.unwrap_or_default();
for item in &items[1..] {
if let Some(obj) = item.as_object() {
candidates.retain(|k| obj.contains_key(k));
}
}
let n = items.len();
let mut defaults = Map::new();
let mut dropped: BTreeSet<String> = BTreeSet::new();
for key in &candidates {
let values: Vec<&Value> = items.iter().filter_map(|it| it.get(key)).collect();
let (dominant, dominant_count, distinct) = dominant_value(&values);
let entropy = distinct as f64 / n as f64;
if opts.drop_entropy < 1.0 && entropy >= opts.drop_entropy {
dropped.insert(key.clone());
continue;
}
if dominant_count >= min_dominant_count(n) {
defaults.insert(key.clone(), dominant);
}
}
if defaults.is_empty() && dropped.is_empty() {
return Crushed {
value: Value::Array(items),
changed: child_changed,
lossless: child_lossless,
};
}
let new_items: Vec<Value> = items
.iter()
.map(|item| {
let mut slim = Map::new();
if let Some(obj) = item.as_object() {
for (key, val) in obj {
if dropped.contains(key) {
continue;
}
if defaults.get(key) == Some(val) {
continue;
}
slim.insert(key.clone(), val.clone());
}
}
Value::Object(slim)
})
.collect();
let had_drops = !dropped.is_empty();
let mut crushed = Map::new();
crushed.insert(MARKER.to_string(), Value::String(MARKER_ARRAY.to_string()));
if !defaults.is_empty() {
crushed.insert(DEFAULTS_KEY.to_string(), Value::Object(defaults));
}
if had_drops {
crushed.insert(
DROPPED_KEY.to_string(),
Value::Array(dropped.into_iter().map(Value::String).collect()),
);
}
crushed.insert(ITEMS_KEY.to_string(), Value::Array(new_items));
Crushed {
value: Value::Object(crushed),
changed: true,
lossless: child_lossless && !had_drops,
}
}
fn min_dominant_count(n: usize) -> usize {
(((n as f64) * MIN_DOMINANCE).ceil() as usize).max(2)
}
fn dominant_value(values: &[&Value]) -> (Value, usize, usize) {
let mut freq: BTreeMap<String, (usize, Value)> = BTreeMap::new();
for v in values {
let key = serde_json::to_string(v).unwrap_or_default();
let entry = freq.entry(key).or_insert_with(|| (0, (*v).clone()));
entry.0 += 1;
}
let distinct = freq.len();
let mut best_count = 0usize;
let mut best_value = Value::Null;
for (count, value) in freq.values() {
if *count > best_count {
best_count = *count;
best_value = value.clone();
}
}
(best_value, best_count, distinct)
}
fn contains_marker(value: &Value) -> bool {
match value {
Value::Object(map) => map.contains_key(MARKER) || map.values().any(contains_marker),
Value::Array(arr) => arr.iter().any(contains_marker),
_ => false,
}
}
fn uncrush_node(value: &Value) -> Value {
match value {
Value::Object(map) => {
if map.get(MARKER) == Some(&Value::String(MARKER_ARRAY.to_string())) {
let defaults = map.get(DEFAULTS_KEY).and_then(Value::as_object);
let items = map.get(ITEMS_KEY).and_then(Value::as_array);
let mut out = Vec::new();
if let Some(items) = items {
for item in items {
let mut full = Map::new();
if let Some(defaults) = defaults {
for (key, val) in defaults {
full.insert(key.clone(), uncrush_node(val));
}
}
if let Some(obj) = item.as_object() {
for (key, val) in obj {
full.insert(key.clone(), uncrush_node(val));
}
}
out.push(Value::Object(full));
}
}
Value::Array(out)
} else {
let mut out = Map::new();
for (key, val) in map {
out.insert(key.clone(), uncrush_node(val));
}
Value::Object(out)
}
}
Value::Array(arr) => Value::Array(arr.iter().map(uncrush_node).collect()),
other => other.clone(),
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use serde_json::json;
fn homogeneous() -> Value {
json!([
{"status": "success", "region": "eu", "id": 1},
{"status": "success", "region": "eu", "id": 2},
{"status": "success", "region": "eu", "id": 3},
{"status": "success", "region": "eu", "id": 4}
])
}
#[test]
fn lossless_factors_constant_columns() {
let v = homogeneous();
let crushed = crush_lossless(&v).expect("should crush");
assert!(crushed.lossless);
assert!(crushed.text.contains("_defaults"));
assert_eq!(crushed.text.matches("success").count(), 1);
}
#[test]
fn lossless_roundtrips_exactly() {
let v = homogeneous();
let crushed = crush_lossless(&v).unwrap();
let restored = reconstruct(&crushed.text).unwrap();
assert_eq!(restored, v);
}
#[test]
fn output_is_byte_stable_across_calls() {
let v = homogeneous();
let run = || crush_lossless(&v).unwrap().text;
assert_eq!(run(), run(), "crush output must be deterministic (#498)");
}
#[test]
fn never_inflates_compressible_payload() {
let v = homogeneous();
let crushed = crush_lossless(&v).unwrap();
let compact = serde_json::to_string(&v).unwrap();
assert!(
crushed.text.len() < compact.len(),
"crushed {} should be shorter than {}",
crushed.text.len(),
compact.len()
);
}
#[test]
fn small_or_heterogeneous_arrays_are_skipped() {
assert!(crush_lossless(&json!([{"a": 1}, {"a": 2}])).is_none()); assert!(crush_lossless(&json!([1, 2, 3, 4])).is_none()); assert!(crush_lossless(&json!([])).is_none());
assert!(
crush_lossless(&json!([
{"a": 1}, {"b": 2}, {"c": 3}, {"d": 4}
]))
.is_none()
);
}
#[test]
fn nested_arrays_crush_and_roundtrip() {
let v = json!({
"total": 2,
"data": [
{"kind": "node", "ready": true, "name": "a"},
{"kind": "node", "ready": true, "name": "b"},
{"kind": "node", "ready": true, "name": "c"}
]
});
let crushed = crush_lossless(&v).unwrap();
assert!(crushed.lossless);
assert_eq!(reconstruct(&crushed.text).unwrap(), v);
}
#[test]
fn unicode_and_escapes_survive_roundtrip() {
let v = json!([
{"tag": "café", "note": "line\nbreak", "id": 1},
{"tag": "café", "note": "quote\"x", "id": 2},
{"tag": "café", "note": "tab\tend", "id": 3}
]);
let crushed = crush_lossless(&v).unwrap();
assert_eq!(reconstruct(&crushed.text).unwrap(), v);
}
#[test]
fn dominant_value_factoring_keeps_deviations() {
let v = json!([
{"state": "ok", "code": 200},
{"state": "ok", "code": 200},
{"state": "ok", "code": 200},
{"state": "err", "code": 500}
]);
let crushed = crush_lossless(&v).unwrap();
assert!(crushed.lossless);
assert!(crushed.text.contains("err"));
assert_eq!(reconstruct(&crushed.text).unwrap(), v);
}
#[test]
fn lossy_drops_high_entropy_columns() {
let v = json!([
{"status": "ok", "uuid": "a1b2"},
{"status": "ok", "uuid": "c3d4"},
{"status": "ok", "uuid": "e5f6"},
{"status": "ok", "uuid": "g7h8"}
]);
let crushed = crush_lossy(&v, &CrushOpts::lossy(0.9)).unwrap();
assert!(!crushed.lossless, "dropping a column is lossy");
assert!(crushed.text.contains("_dropped"));
assert!(!crushed.text.contains("a1b2"));
let restored = reconstruct(&crushed.text).unwrap();
let arr = restored.as_array().unwrap();
assert_eq!(arr.len(), 4);
assert_eq!(arr[0]["status"], json!("ok"));
assert!(arr[0].get("uuid").is_none());
}
#[test]
fn lossy_is_byte_stable() {
let v = json!([
{"status": "ok", "uuid": "a1b2"},
{"status": "ok", "uuid": "c3d4"},
{"status": "ok", "uuid": "e5f6"},
{"status": "ok", "uuid": "g7h8"}
]);
let opts = CrushOpts::lossy(0.9);
let run = || crush_lossy(&v, &opts).unwrap().text;
assert_eq!(run(), run());
}
#[test]
fn beneficial_helpers_share_one_core_and_threshold() {
let items: Vec<Value> = (0..16)
.map(|i| {
json!({"status": "active", "region": "eu-central-1", "tier": "standard", "id": i})
})
.collect();
let v = Value::Array(items);
let raw = serde_json::to_string(&v).unwrap();
let by_value = crush_value_if_beneficial(&v, raw.len()).expect("value gate crushes");
let by_text = crush_text_if_beneficial(&raw).expect("text gate crushes");
assert_eq!(by_value, by_text, "both gates use one core + threshold");
assert!(by_text.len() * KEEP_DATA_DIVISOR <= raw.len());
assert_eq!(reconstruct(&by_text).unwrap(), v);
let hetero = r#"[{"id":1,"k":"aaa"},{"id":2,"k":"bbb"},{"id":3,"k":"ccc"}]"#;
assert!(crush_text_if_beneficial(hetero).is_none());
assert!(crush_text_if_beneficial("not json").is_none());
assert!(crush_text_if_beneficial("\"just a string\"").is_none());
assert!(crush_text_if_beneficial("").is_none());
}
#[test]
fn lossy_gate_drops_high_entropy_columns_and_flags_lossy() {
let items: Vec<Value> = (0..40)
.map(|i| json!({"status": "ok", "ts": format!("2026-06-22T10:00:{i:02}.{i:09}Z")}))
.collect();
let raw = serde_json::to_string(&Value::Array(items)).unwrap();
let res = crush_text_lossy_if_beneficial(&raw, 0.9).expect("lossy gate fires");
assert!(!res.lossless, "dropping a column must report lossy");
assert!(
res.text.contains(DROPPED_KEY),
"dropped columns are recorded"
);
assert!(
res.text.len() * KEEP_DATA_DIVISOR <= raw.len(),
"must at least halve"
);
assert!(crush_text_lossy_if_beneficial(&raw, 1.0).is_none());
assert!(crush_text_lossy_if_beneficial("not json", 0.5).is_none());
}
#[test]
fn input_with_marker_key_is_left_alone() {
let v = json!([
{"_lc_crush": "x", "id": 1},
{"_lc_crush": "y", "id": 2},
{"_lc_crush": "z", "id": 3}
]);
assert!(crush_lossless(&v).is_none());
}
}