use async_trait::async_trait;
use serde_json::Value;
use std::collections::BTreeSet;
use std::fmt::Write as _;
use super::Compressor;
use super::signals::has_error_indicators;
use crate::types::{CompressInput, CompressOptions, CompressOutput, CompressorKind};
pub const MIN_ROWS: usize = 3;
pub const ROW_DROP_THRESHOLD: usize = 40;
pub const HEAD_ROWS: usize = 20;
pub const TAIL_ROWS: usize = 10;
pub const OUTLIER_SIGMA: f64 = 2.0;
pub struct JsonCompressor;
#[async_trait]
impl Compressor for JsonCompressor {
fn kind(&self) -> CompressorKind {
CompressorKind::SmartCrusher
}
async fn compress(
&self,
input: &CompressInput<'_>,
_opts: &CompressOptions,
) -> Option<CompressOutput> {
compress(input.content)
}
}
pub fn compress(content: &str) -> Option<CompressOutput> {
let value: Value = serde_json::from_str(content.trim()).ok()?;
let array = value.as_array()?;
if array.len() < MIN_ROWS {
return None;
}
if !array.iter().all(Value::is_object) {
return None;
}
let mut columns: Vec<String> = Vec::new();
for item in array {
if let Some(obj) = item.as_object() {
for key in obj.keys() {
if !columns.iter().any(|c| c == key) {
columns.push(key.clone());
}
}
}
}
if columns.len() < 2 {
return None;
}
let mut rows: Vec<String> = Vec::with_capacity(array.len());
for item in array {
let obj = item.as_object()?;
let cells: Vec<String> = columns
.iter()
.map(|col| match obj.get(col) {
None => String::new(),
Some(v) => render_cell(v),
})
.collect();
rows.push(cells.join(" | "));
}
let lossy = rows.len() > ROW_DROP_THRESHOLD;
let mut out = String::with_capacity(content.len());
let _ = writeln!(
out,
"[json table: {} rows × {} cols · blank=absent key · exact original via retrieve footer]",
rows.len(),
columns.len()
);
let _ = writeln!(out, "{}", columns.join(" | "));
if lossy {
let keep = rows_to_keep(array, &columns, rows.len());
let mut prev: Option<usize> = None;
for &i in &keep {
if let Some(p) = prev {
let gap = i - p - 1;
if gap > 0 {
let _ = writeln!(out, "[... {gap} row(s) omitted ...]");
}
} else if i > 0 {
let _ = writeln!(out, "[... {i} row(s) omitted ...]");
}
let _ = writeln!(out, "{}", rows[i]);
prev = Some(i);
}
if let Some(p) = prev {
let tail = rows.len().saturating_sub(p + 1);
if tail > 0 {
let _ = writeln!(out, "[... {tail} row(s) omitted ...]");
}
}
} else {
for row in &rows {
let _ = writeln!(out, "{row}");
}
}
let out = out.trim_end().to_string();
if out.len() >= content.len() {
return None;
}
log::debug!(
"[tokenjuice][json] {} rows × {} cols, lossy={} ({} -> {} bytes)",
rows.len(),
columns.len(),
lossy,
content.len(),
out.len(),
);
if lossy {
Some(CompressOutput::lossy(out, CompressorKind::SmartCrusher))
} else {
Some(CompressOutput::reformatted(
out,
CompressorKind::SmartCrusher,
))
}
}
fn rows_to_keep(array: &[Value], columns: &[String], n: usize) -> Vec<usize> {
let mut keep: BTreeSet<usize> = BTreeSet::new();
for i in 0..HEAD_ROWS.min(n) {
keep.insert(i);
}
for i in n.saturating_sub(TAIL_ROWS)..n {
keep.insert(i);
}
for (i, item) in array.iter().enumerate() {
if let Some(obj) = item.as_object() {
let row_has_error = obj.values().any(|v| match v {
Value::String(s) => has_error_indicators(s),
other => has_error_indicators(&other.to_string()),
});
if row_has_error {
keep.insert(i);
}
}
}
for col in columns {
let nums: Vec<(usize, f64)> = array
.iter()
.enumerate()
.filter_map(|(i, item)| {
item.as_object()
.and_then(|o| o.get(col))
.and_then(Value::as_f64)
.map(|x| (i, x))
})
.collect();
if nums.len() < 4 {
continue;
}
let mean = nums.iter().map(|(_, x)| x).sum::<f64>() / nums.len() as f64;
let var = nums.iter().map(|(_, x)| (x - mean).powi(2)).sum::<f64>() / nums.len() as f64;
let std = var.sqrt();
if std <= f64::EPSILON {
continue;
}
for (i, x) in nums {
if ((x - mean) / std).abs() >= OUTLIER_SIGMA {
keep.insert(i);
}
}
}
keep.into_iter().collect()
}
fn render_cell(v: &Value) -> String {
match v {
Value::String(s) if !s.contains('|') && !s.contains('\n') => s.clone(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => n.to_string(),
other => serde_json::to_string(other).unwrap_or_default(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crushes_uniform_array() {
let mut rows = Vec::new();
for i in 0..20 {
rows.push(format!(
r#"{{"id":{i},"name":"item number {i}","status":"active","owner":"team-alpha"}}"#
));
}
let input = format!("[{}]", rows.join(","));
let out = compress(&input).expect("compresses").text;
assert_eq!(out.matches("status").count(), 1, "{out}");
assert!(out.contains("item number 7"));
assert!(out.len() < input.len(), "expected shrink");
}
#[test]
fn large_array_row_drops_and_is_marked_lossy() {
let mut rows = Vec::new();
for i in 0..200 {
rows.push(format!(
r#"{{"id":{i},"name":"record number {i}","status":"active","note":"some detail {i}"}}"#
));
}
let input = format!("[{}]", rows.join(","));
let c = compress(&input).expect("compresses");
assert!(c.lossy, "row-dropped output must be lossy");
assert!(c.text.contains("record number 0"), "{}", c.text);
assert!(c.text.contains("record number 199"), "{}", c.text);
assert!(c.text.contains("omitted"));
assert!(c.text.len() < input.len());
}
#[test]
fn keeps_error_row_in_dropped_middle() {
let mut rows = Vec::new();
for i in 0..120 {
let status = if i == 75 { "error: timeout" } else { "ok" };
rows.push(format!(
r#"{{"id":{i},"name":"job {i}","status":"{status}","note":"detail {i}"}}"#
));
}
let input = format!("[{}]", rows.join(","));
let c = compress(&input).expect("compresses");
assert!(c.lossy);
assert!(
c.text.contains("job 75"),
"error row must survive:\n{}",
c.text
);
assert!(c.text.contains("error: timeout"));
}
#[test]
fn keeps_numeric_outlier_row() {
let mut rows = Vec::new();
for i in 0..120 {
let latency = if i == 88 { 9999 } else { 10 + (i % 3) };
rows.push(format!(
r#"{{"id":{i},"endpoint":"/api/{i}","latency_ms":{latency},"region":"us"}}"#
));
}
let input = format!("[{}]", rows.join(","));
let c = compress(&input).expect("compresses");
assert!(
c.text.contains("9999"),
"outlier row must survive:\n{}",
c.text
);
}
#[test]
fn non_array_returns_none() {
assert!(compress(r#"{"a":1}"#).is_none());
assert!(compress("[1,2,3]").is_none());
assert!(compress(r#"[{"a":1}]"#).is_none());
}
}