use serde_json::Value;
use crate::path::{JsonPath, PathSeg};
pub type CanonFn = fn(&Value) -> Value;
#[derive(Clone)]
pub enum Mask {
Drop(JsonPath),
Canonicalize(JsonPath, CanonFn),
Sort(JsonPath, String),
}
#[derive(Clone, Default)]
pub struct Normalizer {
masks: Vec<Mask>,
}
impl Normalizer {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with(mut self, m: Mask) -> Self {
self.masks.push(m);
self
}
#[must_use]
pub fn masks(&self) -> &[Mask] {
&self.masks
}
#[must_use]
pub fn apply(&self, v: &Value) -> Value {
let mut out = v.clone();
for m in &self.masks {
match m {
Mask::Drop(p) => walk_drop(&mut out, p.segs()),
Mask::Canonicalize(p, f) => walk_canon(&mut out, p.segs(), *f),
Mask::Sort(p, key) => walk_sort(&mut out, p.segs(), key),
}
}
out
}
}
fn walk_drop(v: &mut Value, segs: &[PathSeg]) {
let Some((seg, rest)) = segs.split_first() else {
return;
};
if rest.is_empty() {
match seg {
PathSeg::Key(k) => {
if let Some(o) = v.as_object_mut() {
o.remove(k);
}
}
PathSeg::Index(i) => {
if let Some(a) = v.as_array_mut() {
if *i < a.len() {
a.remove(*i);
}
}
}
PathSeg::Wildcard => {
if let Some(a) = v.as_array_mut() {
a.clear();
}
}
}
return;
}
descend_each(v, seg, |child| walk_drop(child, rest));
}
fn walk_canon(v: &mut Value, segs: &[PathSeg], f: CanonFn) {
let Some((seg, rest)) = segs.split_first() else {
*v = f(v);
return;
};
if rest.is_empty() {
match seg {
PathSeg::Key(k) => {
if let Some(child) = v.as_object_mut().and_then(|o| o.get_mut(k)) {
*child = f(child);
}
}
PathSeg::Index(i) => {
if let Some(child) = v.as_array_mut().and_then(|a| a.get_mut(*i)) {
*child = f(child);
}
}
PathSeg::Wildcard => {
if let Some(a) = v.as_array_mut() {
for child in a.iter_mut() {
*child = f(child);
}
}
}
}
return;
}
descend_each(v, seg, |child| walk_canon(child, rest, f));
}
fn walk_sort(v: &mut Value, segs: &[PathSeg], key: &str) {
let Some((seg, rest)) = segs.split_first() else {
sort_array_by_key(v, key);
return;
};
if rest.is_empty() {
match seg {
PathSeg::Key(k) => {
if let Some(child) = v.as_object_mut().and_then(|o| o.get_mut(k)) {
sort_array_by_key(child, key);
}
}
PathSeg::Index(i) => {
if let Some(child) = v.as_array_mut().and_then(|a| a.get_mut(*i)) {
sort_array_by_key(child, key);
}
}
PathSeg::Wildcard => {
if let Some(a) = v.as_array_mut() {
for child in a.iter_mut() {
sort_array_by_key(child, key);
}
}
}
}
return;
}
descend_each(v, seg, |child| walk_sort(child, rest, key));
}
fn descend_each(v: &mut Value, seg: &PathSeg, mut f: impl FnMut(&mut Value)) {
match seg {
PathSeg::Key(k) => {
if let Some(child) = v.as_object_mut().and_then(|o| o.get_mut(k)) {
f(child);
}
}
PathSeg::Index(i) => {
if let Some(child) = v.as_array_mut().and_then(|a| a.get_mut(*i)) {
f(child);
}
}
PathSeg::Wildcard => {
if let Some(a) = v.as_array_mut() {
for child in a.iter_mut() {
f(child);
}
}
}
}
}
fn sort_array_by_key(v: &mut Value, key: &str) {
if let Some(arr) = v.as_array_mut() {
arr.sort_by(|a, b| {
let ak = a.get(key).and_then(Value::as_str).unwrap_or_default();
let bk = b.get(key).and_then(Value::as_str).unwrap_or_default();
ak.cmp(bk)
});
}
}
#[must_use]
pub fn volatile_meta() -> Normalizer {
Normalizer::new()
.with(Mask::Drop(JsonPath::parse("metadata.uid")))
.with(Mask::Drop(JsonPath::parse("metadata.resourceVersion")))
.with(Mask::Drop(JsonPath::parse("metadata.generation")))
.with(Mask::Drop(JsonPath::parse("metadata.creationTimestamp")))
.with(Mask::Drop(JsonPath::parse("metadata.selfLink")))
.with(Mask::Drop(JsonPath::parse("metadata.managedFields")))
.with(Mask::Canonicalize(JsonPath::parse("status"), |_| {
Value::String("<present>".to_owned())
}))
.with(Mask::Drop(JsonPath::parse("spec.clusterIP")))
.with(Mask::Drop(JsonPath::parse("spec.clusterIPs")))
.with(Mask::Drop(JsonPath::parse("spec.nodeName")))
.with(Mask::Drop(JsonPath::parse("metadata.continue")))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn drop_removes_nested_key() {
let mut norm = Normalizer::new().with(Mask::Drop(JsonPath::parse("metadata.uid")));
let v = json!({"metadata": {"name": "x", "uid": "abc"}});
let out = norm.apply(&v);
assert_eq!(out, json!({"metadata": {"name": "x"}}));
assert!(v["metadata"]["uid"].is_string());
norm = norm.with(Mask::Drop(JsonPath::parse("status")));
let out2 = norm.apply(&json!({"metadata": {"uid": "z"}, "status": {"phase": "Active"}}));
assert_eq!(out2, json!({"metadata": {}}));
}
#[test]
fn wildcard_drops_time_in_each_element() {
let norm = Normalizer::new().with(Mask::Drop(JsonPath::parse(
"metadata.managedFields[*].time",
)));
let v = json!({"metadata": {"managedFields": [
{"manager": "a", "time": "t1"},
{"manager": "b", "time": "t2"},
]}});
let out = norm.apply(&v);
assert_eq!(
out,
json!({"metadata": {"managedFields": [{"manager": "a"}, {"manager": "b"}]}})
);
}
#[test]
fn volatile_meta_leaves_finalizers_and_data() {
let norm = volatile_meta();
let v = json!({
"metadata": {"name": "ns", "uid": "u", "finalizers": ["kubernetes"]},
"spec": {"finalizers": ["kubernetes"]},
"data": {"k": "v"},
"status": {"phase": "Active"},
});
let out = norm.apply(&v);
assert!(out["metadata"]["finalizers"].is_array());
assert!(out["spec"]["finalizers"].is_array());
assert_eq!(out["data"]["k"], json!("v"));
assert_eq!(
out["status"],
json!("<present>"),
"a present status must collapse to the marker, never vanish",
);
assert!(out["metadata"].get("uid").is_none());
}
#[test]
fn a_missing_status_is_distinguishable_from_a_present_one() {
let norm = volatile_meta();
let with = norm.apply(&json!({"kind":"Pod","status":{"phase":"Running"}}));
let without = norm.apply(&json!({"kind":"Pod"}));
assert_ne!(
with, without,
"an absent status MUST diverge from a present one — this is the \
single largest engenho gap and the harness has to be able to see it",
);
assert!(without.get("status").is_none());
}
}