engenho_diff/
normalize.rs1use serde_json::Value;
9
10use crate::path::{JsonPath, PathSeg};
11
12pub type CanonFn = fn(&Value) -> Value;
14
15#[derive(Clone)]
17pub enum Mask {
18 Drop(JsonPath),
20 Canonicalize(JsonPath, CanonFn),
22 Sort(JsonPath, String),
24}
25
26#[derive(Clone, Default)]
28pub struct Normalizer {
29 masks: Vec<Mask>,
30}
31
32impl Normalizer {
33 #[must_use]
35 pub fn new() -> Self {
36 Self::default()
37 }
38
39 #[must_use]
41 pub fn with(mut self, m: Mask) -> Self {
42 self.masks.push(m);
43 self
44 }
45
46 #[must_use]
48 pub fn masks(&self) -> &[Mask] {
49 &self.masks
50 }
51
52 #[must_use]
54 pub fn apply(&self, v: &Value) -> Value {
55 let mut out = v.clone();
56 for m in &self.masks {
57 match m {
58 Mask::Drop(p) => walk_drop(&mut out, p.segs()),
59 Mask::Canonicalize(p, f) => walk_canon(&mut out, p.segs(), *f),
60 Mask::Sort(p, key) => walk_sort(&mut out, p.segs(), key),
61 }
62 }
63 out
64 }
65}
66
67fn walk_drop(v: &mut Value, segs: &[PathSeg]) {
68 let Some((seg, rest)) = segs.split_first() else {
69 return;
70 };
71 if rest.is_empty() {
72 match seg {
73 PathSeg::Key(k) => {
74 if let Some(o) = v.as_object_mut() {
75 o.remove(k);
76 }
77 }
78 PathSeg::Index(i) => {
79 if let Some(a) = v.as_array_mut() {
80 if *i < a.len() {
81 a.remove(*i);
82 }
83 }
84 }
85 PathSeg::Wildcard => {
86 if let Some(a) = v.as_array_mut() {
87 a.clear();
88 }
89 }
90 }
91 return;
92 }
93 descend_each(v, seg, |child| walk_drop(child, rest));
94}
95
96fn walk_canon(v: &mut Value, segs: &[PathSeg], f: CanonFn) {
97 let Some((seg, rest)) = segs.split_first() else {
98 *v = f(v);
99 return;
100 };
101 if rest.is_empty() {
102 match seg {
103 PathSeg::Key(k) => {
104 if let Some(child) = v.as_object_mut().and_then(|o| o.get_mut(k)) {
105 *child = f(child);
106 }
107 }
108 PathSeg::Index(i) => {
109 if let Some(child) = v.as_array_mut().and_then(|a| a.get_mut(*i)) {
110 *child = f(child);
111 }
112 }
113 PathSeg::Wildcard => {
114 if let Some(a) = v.as_array_mut() {
115 for child in a.iter_mut() {
116 *child = f(child);
117 }
118 }
119 }
120 }
121 return;
122 }
123 descend_each(v, seg, |child| walk_canon(child, rest, f));
124}
125
126fn walk_sort(v: &mut Value, segs: &[PathSeg], key: &str) {
127 let Some((seg, rest)) = segs.split_first() else {
128 sort_array_by_key(v, key);
129 return;
130 };
131 if rest.is_empty() {
132 match seg {
133 PathSeg::Key(k) => {
134 if let Some(child) = v.as_object_mut().and_then(|o| o.get_mut(k)) {
135 sort_array_by_key(child, key);
136 }
137 }
138 PathSeg::Index(i) => {
139 if let Some(child) = v.as_array_mut().and_then(|a| a.get_mut(*i)) {
140 sort_array_by_key(child, key);
141 }
142 }
143 PathSeg::Wildcard => {
144 if let Some(a) = v.as_array_mut() {
145 for child in a.iter_mut() {
146 sort_array_by_key(child, key);
147 }
148 }
149 }
150 }
151 return;
152 }
153 descend_each(v, seg, |child| walk_sort(child, rest, key));
154}
155
156fn descend_each(v: &mut Value, seg: &PathSeg, mut f: impl FnMut(&mut Value)) {
157 match seg {
158 PathSeg::Key(k) => {
159 if let Some(child) = v.as_object_mut().and_then(|o| o.get_mut(k)) {
160 f(child);
161 }
162 }
163 PathSeg::Index(i) => {
164 if let Some(child) = v.as_array_mut().and_then(|a| a.get_mut(*i)) {
165 f(child);
166 }
167 }
168 PathSeg::Wildcard => {
169 if let Some(a) = v.as_array_mut() {
170 for child in a.iter_mut() {
171 f(child);
172 }
173 }
174 }
175 }
176}
177
178fn sort_array_by_key(v: &mut Value, key: &str) {
179 if let Some(arr) = v.as_array_mut() {
180 arr.sort_by(|a, b| {
181 let ak = a.get(key).and_then(Value::as_str).unwrap_or_default();
182 let bk = b.get(key).and_then(Value::as_str).unwrap_or_default();
183 ak.cmp(bk)
184 });
185 }
186}
187
188#[must_use]
197pub fn volatile_meta() -> Normalizer {
198 Normalizer::new()
199 .with(Mask::Drop(JsonPath::parse("metadata.uid")))
201 .with(Mask::Drop(JsonPath::parse("metadata.resourceVersion")))
202 .with(Mask::Drop(JsonPath::parse("metadata.generation")))
203 .with(Mask::Drop(JsonPath::parse("metadata.creationTimestamp")))
204 .with(Mask::Drop(JsonPath::parse("metadata.selfLink")))
205 .with(Mask::Drop(JsonPath::parse("metadata.managedFields")))
209 .with(Mask::Drop(JsonPath::parse("status")))
211 .with(Mask::Drop(JsonPath::parse("spec.clusterIP")))
213 .with(Mask::Drop(JsonPath::parse("spec.clusterIPs")))
214 .with(Mask::Drop(JsonPath::parse("spec.nodeName")))
216 .with(Mask::Drop(JsonPath::parse("metadata.continue")))
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use serde_json::json;
224
225 #[test]
226 fn drop_removes_nested_key() {
227 let mut norm = Normalizer::new().with(Mask::Drop(JsonPath::parse("metadata.uid")));
228 let v = json!({"metadata": {"name": "x", "uid": "abc"}});
229 let out = norm.apply(&v);
230 assert_eq!(out, json!({"metadata": {"name": "x"}}));
231 assert!(v["metadata"]["uid"].is_string());
233 norm = norm.with(Mask::Drop(JsonPath::parse("status")));
234 let out2 = norm.apply(&json!({"metadata": {"uid": "z"}, "status": {"phase": "Active"}}));
235 assert_eq!(out2, json!({"metadata": {}}));
236 }
237
238 #[test]
239 fn wildcard_drops_time_in_each_element() {
240 let norm = Normalizer::new().with(Mask::Drop(JsonPath::parse(
241 "metadata.managedFields[*].time",
242 )));
243 let v = json!({"metadata": {"managedFields": [
244 {"manager": "a", "time": "t1"},
245 {"manager": "b", "time": "t2"},
246 ]}});
247 let out = norm.apply(&v);
248 assert_eq!(
249 out,
250 json!({"metadata": {"managedFields": [{"manager": "a"}, {"manager": "b"}]}})
251 );
252 }
253
254 #[test]
255 fn volatile_meta_leaves_finalizers_and_data() {
256 let norm = volatile_meta();
257 let v = json!({
258 "metadata": {"name": "ns", "uid": "u", "finalizers": ["kubernetes"]},
259 "spec": {"finalizers": ["kubernetes"]},
260 "data": {"k": "v"},
261 "status": {"phase": "Active"},
262 });
263 let out = norm.apply(&v);
264 assert!(out["metadata"]["finalizers"].is_array());
265 assert!(out["spec"]["finalizers"].is_array());
266 assert_eq!(out["data"]["k"], json!("v"));
267 assert!(out.get("status").is_none());
268 assert!(out["metadata"].get("uid").is_none());
269 }
270}