use serde_json::{json, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct BudgetOutcome {
pub truncated: bool,
pub dropped: usize,
pub stub: bool,
}
fn encoded_len(value: &Value) -> usize {
serde_json::to_string(value).map_or(usize::MAX, |s| s.len())
}
fn array_mut<'a>(value: &'a mut Value, array_key: Option<&str>) -> Option<&'a mut Vec<Value>> {
match array_key {
None => value.as_array_mut(),
Some(key) => value.as_object_mut()?.get_mut(key)?.as_array_mut(),
}
}
fn secondary_array_keys(value: &Value, array_key: Option<&str>) -> Vec<String> {
let Some(map) = value.as_object() else {
return Vec::new();
};
map.iter()
.filter(|(k, v)| v.is_array() && Some(k.as_str()) != array_key)
.map(|(k, _)| k.clone())
.collect()
}
fn take_array(value: &mut Value, array_key: Option<&str>) -> Option<Vec<Value>> {
array_mut(value, array_key).map(std::mem::take)
}
fn put_array(value: &mut Value, array_key: Option<&str>, items: Vec<Value>) {
if let Some(slot) = array_mut(value, array_key) {
*slot = items;
}
}
fn drain_secondary_arrays(value: &mut Value, keys: &[String]) -> usize {
let Some(map) = value.as_object_mut() else {
return 0;
};
let mut dropped = 0usize;
for key in keys {
if let Some(Value::Array(items)) = map.get_mut(key) {
dropped += items.len();
items.clear();
}
}
dropped
}
pub fn enforce(value: &mut Value, array_key: Option<&str>, max: usize) -> BudgetOutcome {
if max == 0 || encoded_len(value) <= max {
return BudgetOutcome::default();
}
let mut items = take_array(value, array_key).unwrap_or_default();
let original_len = items.len();
let secondary = secondary_array_keys(value, array_key);
let mut dropped_secondary = 0usize;
if !secondary.is_empty() && encoded_len(value) > max {
dropped_secondary = drain_secondary_arrays(value, &secondary);
}
if original_len > 0 {
let skeleton = encoded_len(value);
let mut used = 0usize;
let mut best = 0usize;
for (index, item) in items.iter().enumerate() {
let separator = usize::from(index > 0);
let cost = encoded_len(item).saturating_add(separator);
if skeleton.saturating_add(used).saturating_add(cost) > max {
break;
}
used = used.saturating_add(cost);
best = index + 1;
}
if best > 0 {
items.truncate(best);
put_array(value, array_key, items);
return BudgetOutcome {
truncated: true,
dropped: original_len - best + dropped_secondary,
stub: false,
};
}
}
if encoded_len(value) <= max {
return BudgetOutcome {
truncated: true,
dropped: original_len + dropped_secondary,
stub: false,
};
}
*value = json!({
"truncated": true,
"truncated_reason": "max_output_bytes",
"max_output_bytes": max,
});
BudgetOutcome {
truncated: true,
dropped: original_len + dropped_secondary,
stub: true,
}
}