use std::collections::BTreeMap;
use serde_json::Value;
pub fn drop_empty_top_level(v: Value) -> Value {
if let Value::Object(map) = v {
let filtered: serde_json::Map<String, Value> = map
.into_iter()
.filter(|(_, val)| !is_empty_signal(val))
.collect();
Value::Object(filtered)
} else {
v
}
}
fn is_empty_signal(v: &Value) -> bool {
match v {
Value::Null => true,
Value::Array(a) => a.is_empty(),
Value::Object(o) => o.is_empty(),
_ => false,
}
}
pub fn drop_empty_recursive(v: Value) -> Value {
match v {
Value::Object(map) => {
let cleaned: serde_json::Map<String, Value> = map
.into_iter()
.map(|(k, val)| (k, drop_empty_recursive(val)))
.filter(|(_, val)| !is_empty_signal(val))
.collect();
Value::Object(cleaned)
}
Value::Array(arr) => {
Value::Array(arr.into_iter().map(drop_empty_recursive).collect())
}
other => other,
}
}
pub fn is_zero_usize(n: &usize) -> bool {
*n == 0
}
pub fn is_all_zero_string_usize_map(m: &BTreeMap<String, usize>) -> bool {
m.values().all(|v| *v == 0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_usize_skipped() {
assert!(is_zero_usize(&0));
assert!(!is_zero_usize(&1));
}
#[test]
fn empty_map_is_all_zero() {
let m: BTreeMap<String, usize> = BTreeMap::new();
assert!(is_all_zero_string_usize_map(&m));
}
#[test]
fn all_zero_map_is_skipped() {
let mut m = BTreeMap::new();
m.insert("a".into(), 0usize);
m.insert("b".into(), 0);
assert!(is_all_zero_string_usize_map(&m));
}
#[test]
fn map_with_any_nonzero_value_is_not_skipped() {
let mut m = BTreeMap::new();
m.insert("a".into(), 0usize);
m.insert("b".into(), 1);
assert!(!is_all_zero_string_usize_map(&m));
}
#[test]
fn drop_empty_removes_null_empty_array_empty_object() {
let v = serde_json::json!({
"kept": 1,
"kept_str": "value",
"drop_null": serde_json::Value::Null,
"drop_empty_arr": [],
"drop_empty_obj": {},
"kept_arr": [1],
"kept_obj": {"x": 1},
"kept_zero": 0, "kept_false": false, });
let out = drop_empty_top_level(v);
let obj = out.as_object().unwrap();
assert!(obj.contains_key("kept"));
assert!(obj.contains_key("kept_str"));
assert!(obj.contains_key("kept_arr"));
assert!(obj.contains_key("kept_obj"));
assert!(obj.contains_key("kept_zero"));
assert!(obj.contains_key("kept_false"));
assert!(!obj.contains_key("drop_null"));
assert!(!obj.contains_key("drop_empty_arr"));
assert!(!obj.contains_key("drop_empty_obj"));
}
#[test]
fn drop_empty_only_touches_top_level() {
let v = serde_json::json!({
"wrapper": { "nested_empty": [] }
});
let out = drop_empty_top_level(v);
assert!(out["wrapper"]["nested_empty"].is_array());
}
#[test]
fn drop_empty_no_op_on_non_object() {
let v = serde_json::json!([1, 2, 3]);
let out = drop_empty_top_level(v.clone());
assert_eq!(out, v);
}
#[test]
fn drop_empty_recursive_strips_nested_empties() {
let v = serde_json::json!({
"safe_change_recipe": {
"inspect": [{"file": "a.py"}],
"preserve": [],
"reference_only": [],
"likely_omitted_files": [],
"blast_radius": {
"total_callers": 95,
"callee_layer_distribution": {},
}
}
});
let out = drop_empty_recursive(v);
let recipe = &out["safe_change_recipe"];
assert!(recipe["inspect"].is_array());
assert!(recipe.get("preserve").is_none(), "empty preserve dropped");
assert!(
recipe.get("reference_only").is_none(),
"empty ref_only dropped"
);
assert!(
recipe.get("likely_omitted_files").is_none(),
"empty likely_omitted dropped"
);
assert!(
recipe["blast_radius"]["total_callers"].is_number(),
"non-empty kept"
);
assert!(
recipe["blast_radius"]
.get("callee_layer_distribution")
.is_none(),
"nested empty object dropped"
);
}
#[test]
fn drop_empty_recursive_preserves_array_positions() {
let v = serde_json::json!({
"items": [
{"keep": 1, "drop_null": null},
{"keep": 2, "drop_empty_obj": {}}
]
});
let out = drop_empty_recursive(v);
let items = out["items"].as_array().unwrap();
assert_eq!(items.len(), 2, "array length preserved");
assert_eq!(items[0]["keep"], 1);
assert!(items[0].get("drop_null").is_none());
assert_eq!(items[1]["keep"], 2);
assert!(items[1].get("drop_empty_obj").is_none());
}
}