agentstatedeveloper_core/
ser_helpers.rs1use std::collections::BTreeMap;
15
16use serde_json::Value;
17
18pub fn drop_empty_top_level(v: Value) -> Value {
32 if let Value::Object(map) = v {
33 let filtered: serde_json::Map<String, Value> = map
34 .into_iter()
35 .filter(|(_, val)| !is_empty_signal(val))
36 .collect();
37 Value::Object(filtered)
38 } else {
39 v
40 }
41}
42
43fn is_empty_signal(v: &Value) -> bool {
44 match v {
45 Value::Null => true,
46 Value::Array(a) => a.is_empty(),
47 Value::Object(o) => o.is_empty(),
48 _ => false,
49 }
50}
51
52pub fn drop_empty_recursive(v: Value) -> Value {
70 match v {
71 Value::Object(map) => {
72 let cleaned: serde_json::Map<String, Value> = map
73 .into_iter()
74 .map(|(k, val)| (k, drop_empty_recursive(val)))
75 .filter(|(_, val)| !is_empty_signal(val))
76 .collect();
77 Value::Object(cleaned)
78 }
79 Value::Array(arr) => {
80 Value::Array(arr.into_iter().map(drop_empty_recursive).collect())
83 }
84 other => other,
85 }
86}
87
88pub fn is_zero_usize(n: &usize) -> bool {
93 *n == 0
94}
95
96pub fn is_all_zero_string_usize_map(m: &BTreeMap<String, usize>) -> bool {
100 m.values().all(|v| *v == 0)
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn zero_usize_skipped() {
109 assert!(is_zero_usize(&0));
110 assert!(!is_zero_usize(&1));
111 }
112
113 #[test]
114 fn empty_map_is_all_zero() {
115 let m: BTreeMap<String, usize> = BTreeMap::new();
116 assert!(is_all_zero_string_usize_map(&m));
117 }
118
119 #[test]
120 fn all_zero_map_is_skipped() {
121 let mut m = BTreeMap::new();
122 m.insert("a".into(), 0usize);
123 m.insert("b".into(), 0);
124 assert!(is_all_zero_string_usize_map(&m));
125 }
126
127 #[test]
128 fn map_with_any_nonzero_value_is_not_skipped() {
129 let mut m = BTreeMap::new();
130 m.insert("a".into(), 0usize);
131 m.insert("b".into(), 1);
132 assert!(!is_all_zero_string_usize_map(&m));
133 }
134
135 #[test]
136 fn drop_empty_removes_null_empty_array_empty_object() {
137 let v = serde_json::json!({
138 "kept": 1,
139 "kept_str": "value",
140 "drop_null": serde_json::Value::Null,
141 "drop_empty_arr": [],
142 "drop_empty_obj": {},
143 "kept_arr": [1],
144 "kept_obj": {"x": 1},
145 "kept_zero": 0, "kept_false": false, });
148 let out = drop_empty_top_level(v);
149 let obj = out.as_object().unwrap();
150 assert!(obj.contains_key("kept"));
151 assert!(obj.contains_key("kept_str"));
152 assert!(obj.contains_key("kept_arr"));
153 assert!(obj.contains_key("kept_obj"));
154 assert!(obj.contains_key("kept_zero"));
155 assert!(obj.contains_key("kept_false"));
156 assert!(!obj.contains_key("drop_null"));
157 assert!(!obj.contains_key("drop_empty_arr"));
158 assert!(!obj.contains_key("drop_empty_obj"));
159 }
160
161 #[test]
162 fn drop_empty_only_touches_top_level() {
163 let v = serde_json::json!({
165 "wrapper": { "nested_empty": [] }
166 });
167 let out = drop_empty_top_level(v);
168 assert!(out["wrapper"]["nested_empty"].is_array());
170 }
171
172 #[test]
173 fn drop_empty_no_op_on_non_object() {
174 let v = serde_json::json!([1, 2, 3]);
175 let out = drop_empty_top_level(v.clone());
176 assert_eq!(out, v);
177 }
178
179 #[test]
180 fn drop_empty_recursive_strips_nested_empties() {
181 let v = serde_json::json!({
186 "safe_change_recipe": {
187 "inspect": [{"file": "a.py"}],
188 "preserve": [],
189 "reference_only": [],
190 "likely_omitted_files": [],
191 "blast_radius": {
192 "total_callers": 95,
193 "callee_layer_distribution": {},
194 }
195 }
196 });
197 let out = drop_empty_recursive(v);
198 let recipe = &out["safe_change_recipe"];
199 assert!(recipe["inspect"].is_array());
200 assert!(recipe.get("preserve").is_none(), "empty preserve dropped");
201 assert!(
202 recipe.get("reference_only").is_none(),
203 "empty ref_only dropped"
204 );
205 assert!(
206 recipe.get("likely_omitted_files").is_none(),
207 "empty likely_omitted dropped"
208 );
209 assert!(
210 recipe["blast_radius"]["total_callers"].is_number(),
211 "non-empty kept"
212 );
213 assert!(
214 recipe["blast_radius"]
215 .get("callee_layer_distribution")
216 .is_none(),
217 "nested empty object dropped"
218 );
219 }
220
221 #[test]
222 fn drop_empty_recursive_preserves_array_positions() {
223 let v = serde_json::json!({
226 "items": [
227 {"keep": 1, "drop_null": null},
228 {"keep": 2, "drop_empty_obj": {}}
229 ]
230 });
231 let out = drop_empty_recursive(v);
232 let items = out["items"].as_array().unwrap();
233 assert_eq!(items.len(), 2, "array length preserved");
234 assert_eq!(items[0]["keep"], 1);
235 assert!(items[0].get("drop_null").is_none());
236 assert_eq!(items[1]["keep"], 2);
237 assert!(items[1].get("drop_empty_obj").is_none());
238 }
239}