use std::sync::OnceLock;
use rmpv::Value;
use crate::util::app_config_reader::AppConfigReader;
pub fn null_transport() -> bool {
static CACHE: OnceLock<bool> = OnceLock::new();
*CACHE.get_or_init(|| {
AppConfigReader::get_instance()
.get_property_or("serializer.null.transport", "false")
.eq_ignore_ascii_case("true")
})
}
pub fn strip_nulls(value: &Value) -> Value {
if null_transport() || !has_nil_map_entry(value) {
value.clone()
} else {
strip_nulls_always(value)
}
}
pub fn has_nil_map_entry(value: &Value) -> bool {
match value {
Value::Map(entries) => entries
.iter()
.any(|(_, v)| matches!(v, Value::Nil) || has_nil_map_entry(v)),
Value::Array(items) => items.iter().any(has_nil_map_entry),
_ => false,
}
}
pub fn strip_nulls_always(value: &Value) -> Value {
match value {
Value::Map(entries) => Value::Map(
entries
.iter()
.filter(|(_, v)| !matches!(v, Value::Nil))
.map(|(k, v)| (k.clone(), strip_nulls_always(v)))
.collect(),
),
Value::Array(items) => Value::Array(items.iter().map(strip_nulls_always).collect()),
other => other.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use rmpv::Value;
fn s(text: &str) -> Value {
Value::from(text)
}
#[test]
fn drops_nil_map_entries() {
let input = Value::Map(vec![
(s("ok"), Value::Boolean(true)),
(s("error"), Value::Nil),
(s("name"), s("Peter")),
]);
let out = strip_nulls_always(&input);
let Value::Map(entries) = out else {
panic!("expected map");
};
let keys: Vec<&str> = entries.iter().filter_map(|(k, _)| k.as_str()).collect();
assert_eq!(keys, vec!["ok", "name"], "the Nil `error` entry is dropped");
}
#[test]
fn recurses_into_nested_maps() {
let input = Value::Map(vec![(
s("output"),
Value::Map(vec![(s("body"), s("hello")), (s("meta"), Value::Nil)]),
)]);
let Value::Map(entries) = strip_nulls_always(&input) else {
panic!("map");
};
let Value::Map(inner) = &entries[0].1 else {
panic!("nested map");
};
let keys: Vec<&str> = inner.iter().filter_map(|(k, _)| k.as_str()).collect();
assert_eq!(keys, vec!["body"], "nested Nil dropped, non-null kept");
}
#[test]
fn preserves_array_elements_including_null() {
let input = Value::Array(vec![s("a"), Value::Nil, s("b")]);
let Value::Array(items) = strip_nulls_always(&input) else {
panic!("array");
};
assert_eq!(
items.len(),
3,
"array length (with the null slot) is preserved"
);
assert!(matches!(items[1], Value::Nil));
}
#[test]
fn strips_maps_inside_arrays() {
let input = Value::Array(vec![Value::Map(vec![
(s("keep"), s("x")),
(s("drop"), Value::Nil),
])]);
let Value::Array(items) = strip_nulls_always(&input) else {
panic!("array");
};
let Value::Map(inner) = &items[0] else {
panic!("map in array");
};
assert_eq!(
inner.len(),
1,
"Nil entry in an array's map element is dropped"
);
}
#[test]
fn scalars_pass_through() {
assert_eq!(strip_nulls_always(&s("x")), s("x"));
assert_eq!(strip_nulls_always(&Value::Nil), Value::Nil);
}
#[test]
fn empty_collections_are_not_null() {
let input = Value::Map(vec![
(s("empty_list"), Value::Array(vec![])),
(s("empty_map"), Value::Map(vec![])),
(s("gone"), Value::Nil),
]);
let Value::Map(entries) = strip_nulls_always(&input) else {
panic!("map");
};
let keys: Vec<&str> = entries.iter().filter_map(|(k, _)| k.as_str()).collect();
assert_eq!(
keys,
vec!["empty_list", "empty_map"],
"empty [] and {{}} are kept; only the Nil entry is dropped"
);
}
#[test]
fn has_nil_map_entry_matches_what_strip_removes() {
assert!(has_nil_map_entry(&Value::Map(vec![(s("a"), Value::Nil)])));
assert!(has_nil_map_entry(&Value::Map(vec![(
s("a"),
Value::Map(vec![(s("b"), Value::Nil)]),
)])));
assert!(has_nil_map_entry(&Value::Array(vec![Value::Map(vec![(
s("k"),
Value::Nil,
)])])));
assert!(!has_nil_map_entry(&Value::Map(vec![(s("a"), s("x"))])));
assert!(!has_nil_map_entry(&Value::Array(vec![Value::Nil, s("x")])));
assert!(!has_nil_map_entry(&Value::Array(vec![])));
assert!(!has_nil_map_entry(&Value::Map(vec![])));
assert!(!has_nil_map_entry(&Value::Nil));
assert!(!has_nil_map_entry(&s("x")));
}
#[test]
fn array_ordering_preserved_with_interior_null() {
let input = Value::Array(vec![
Value::from(0),
Value::Nil,
Value::from(2),
Value::Nil,
Value::from(4),
]);
let Value::Array(items) = strip_nulls_always(&input) else {
panic!("array");
};
assert_eq!(items.len(), 5, "no element dropped");
assert_eq!(items[0], Value::from(0));
assert!(matches!(items[1], Value::Nil));
assert_eq!(items[2], Value::from(2));
assert!(matches!(items[3], Value::Nil));
assert_eq!(items[4], Value::from(4));
}
}