Skip to main content

k8s_openapi/
deep_merge.rs

1/// A trait applies to types that support deep merging.
2///
3/// `current.merge_from(new)` behaves in the following ways:
4///
5/// ## `struct`s
6///
7/// Structs are merged by individually merging each of their fields. For example, given:
8///
9/// ```rust,ignore
10/// struct S {
11///     a: i32,
12///     b: String,
13/// }
14/// ```
15///
16/// ... the expected impl of `DeepMerge` for `S` would be:
17///
18/// ```rust,ignore
19/// impl DeepMerge for S {
20///     fn merge_from(&mut self, other: Self) {
21///         self.a.merge_from(other.a);
22///         self.b.merge_from(other.b);
23///     }
24/// }
25/// ```
26///
27/// The structs in the `k8s-openapi` crate behave this way. If you are implementing this trait for your own types, it is recommended to impl it in the same way.
28///
29/// ## `Option`
30///
31/// - If `new` is a `None`, `current` is unchanged.
32///
33/// - If `new` is a `Some(new_inner)`:
34///
35///   - If `current` is a `None`, `current` becomes `Some(new_inner)`.
36///
37///   - If `current` is a `Some(current_inner)`, `current_inner` is merged with `new_inner`.
38///
39/// ## `Vec`
40///
41/// Use an [explicit merge strategy.](`strategies::list`)
42///
43/// ## `BTreeMap`
44///
45/// Use an [explicit merge strategy.](`strategies::map`)
46///
47/// ## `serde_json::Value`
48///
49/// `serde_json::Value` is merged using the JSON merge algorithm (RFC 7396).
50///
51/// ## Other types
52///
53/// `self` is just replaced by `other`.
54pub trait DeepMerge {
55    /// Merge `other` into `self`.
56    fn merge_from(&mut self, other: Self);
57}
58
59macro_rules! default_overwriting_impl {
60    () => {
61        fn merge_from(&mut self, other: Self) {
62            *self = other;
63        }
64    };
65}
66
67impl DeepMerge for bool { default_overwriting_impl! {} }
68impl DeepMerge for i32 { default_overwriting_impl! {} }
69impl DeepMerge for i64 { default_overwriting_impl! {} }
70impl DeepMerge for f64 { default_overwriting_impl! {} }
71impl DeepMerge for std::string::String { default_overwriting_impl! {} }
72impl DeepMerge for crate::ByteString { default_overwriting_impl! {} }
73impl DeepMerge for jiff::Timestamp { default_overwriting_impl! {} }
74
75impl DeepMerge for serde_json::Value {
76    fn merge_from(&mut self, other: Self) {
77        if
78            let serde_json::Value::Object(this) = self &&
79            let serde_json::Value::Object(other) = other
80        {
81            for (k, v) in other {
82                if v.is_null() {
83                    this.remove(&k);
84                }
85                else {
86                    this.entry(k).or_insert(serde_json::Value::Null).merge_from(v);
87                }
88            }
89
90            return;
91        }
92
93        *self = other;
94    }
95}
96
97impl<T> DeepMerge for std::boxed::Box<T> where T: DeepMerge {
98    fn merge_from(&mut self, other: Self) {
99        (**self).merge_from(*other);
100    }
101}
102
103impl<T> DeepMerge for Option<T> where T: DeepMerge {
104    fn merge_from(&mut self, other: Self) {
105        if let Some(other) = other {
106            if let Some(s) = self {
107                s.merge_from(other);
108            }
109            else {
110                *self = Some(other);
111            }
112        }
113    }
114}
115
116/// Strategies for merging collections.
117pub mod strategies {
118    /// Strategies for merging [`std::vec::Vec`]s.
119    ///
120    /// These correspond to [`JSONSchemaProps.x-kubernetes-list-type`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#jsonschemaprops-v1-apiextensions-k8s-io).
121    pub mod list {
122        mod private {
123            pub trait AsOptVec {
124                type Item;
125
126                fn set_if_some(&mut self, new: Self);
127                fn as_mut_opt(&mut self) -> Option<&mut std::vec::Vec<Self::Item>>;
128                fn into_opt(self) -> Option<std::vec::Vec<Self::Item>>;
129            }
130
131            impl<T> AsOptVec for std::vec::Vec<T> {
132                type Item = T;
133
134                fn set_if_some(&mut self, new: Self) {
135                    *self = new;
136                }
137
138                fn as_mut_opt(&mut self) -> Option<&mut Self> {
139                    Some(self)
140                }
141
142                fn into_opt(self) -> Option<Self> {
143                    Some(self)
144                }
145            }
146
147            impl<T> AsOptVec for Option<std::vec::Vec<T>> {
148                type Item = T;
149
150                fn set_if_some(&mut self, new: Self) {
151                    if new.is_some() {
152                        *self = new;
153                    }
154                }
155
156                fn as_mut_opt(&mut self) -> Option<&mut std::vec::Vec<T>> {
157                    self.as_mut()
158                }
159
160                fn into_opt(self) -> Self {
161                    self
162                }
163            }
164        }
165        use private::AsOptVec;
166
167        /// The whole list is treated as one scalar value, and will be replaced with the new (non-[`None`]) value.
168        pub fn atomic<V>(current: &mut V, new: V) where V: AsOptVec {
169            current.set_if_some(new);
170        }
171
172        /// The list is treated as a map.
173        ///
174        /// Any items with matching keys will be deep-merged. Any items in `new` that are not in `current` will be appended to `current`.
175        pub fn map<V>(
176            current: &mut V,
177            new: V,
178            key_comparators: &[fn(&V::Item, &V::Item) -> bool],
179            merge_item: fn(&mut V::Item, V::Item),
180        )
181        where
182            V: AsOptVec,
183        {
184            if let Some(current) = current.as_mut_opt() {
185                for new_item in new.into_opt().into_iter().flatten() {
186                    if let Some(current_item) = current.iter_mut().find(|current_item| key_comparators.iter().all(|&f| f(&**current_item, &new_item))) {
187                        merge_item(current_item, new_item);
188                    }
189                    else {
190                        current.push(new_item);
191                    }
192                }
193            }
194            else {
195                current.set_if_some(new);
196            }
197        }
198
199        /// The list is treated as a set.
200        ///
201        /// Items from `new` will be appended to `current`, _unless_ `current` already contains an equal item.
202        pub fn set<V>(current: &mut V, new: V) where V: AsOptVec, V::Item: PartialEq {
203            if let Some(current) = current.as_mut_opt() {
204                for item in new.into_opt().into_iter().flatten() {
205                    if !current.contains(&item) {
206                        current.push(item);
207                    }
208                }
209            }
210            else {
211                current.set_if_some(new);
212            }
213        }
214    }
215
216    /// Strategies for merging [`BTreeMap`s.](std::collections::BTreeMap)
217    ///
218    /// These correspond to [`JSONSchemaProps.x-kubernetes-map-type`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#jsonschemaprops-v1-apiextensions-k8s-io).
219    pub mod map {
220        mod private {
221            use std::collections::BTreeMap;
222
223            pub trait AsOptMap {
224                type Key;
225                type Value;
226
227                fn set_if_some(&mut self, new: Self);
228                fn as_mut_opt(&mut self) -> Option<&mut BTreeMap<Self::Key, Self::Value>>;
229                fn into_opt(self) -> Option<BTreeMap<Self::Key, Self::Value>>;
230            }
231
232            impl<K, V> AsOptMap for BTreeMap<K, V> {
233                type Key = K;
234                type Value = V;
235
236                fn set_if_some(&mut self, new: Self) {
237                    *self = new;
238                }
239
240                fn as_mut_opt(&mut self) -> Option<&mut Self> {
241                    Some(self)
242                }
243
244                fn into_opt(self) -> Option<Self> {
245                    Some(self)
246                }
247            }
248
249            impl<K, V> AsOptMap for Option<BTreeMap<K, V>> {
250                type Key = K;
251                type Value = V;
252
253                fn set_if_some(&mut self, new: Self) {
254                    if new.is_some() {
255                        *self = new;
256                    }
257                }
258
259                fn as_mut_opt(&mut self) -> Option<&mut BTreeMap<K, V>> {
260                    self.as_mut()
261                }
262
263                fn into_opt(self) -> Self {
264                    self
265                }
266            }
267        }
268        use private::AsOptMap;
269
270        /// The whole map is treated as one scalar value, and will be replaced with the new (non-[`None`]) value.
271        pub fn atomic<M>(current: &mut M, new: M) where M: AsOptMap {
272            current.set_if_some(new);
273        }
274
275        /// Each value will be merged separately.
276        pub fn granular<M>(current: &mut M, new: M, merge_value: fn(&mut M::Value, M::Value))
277        where
278            M: AsOptMap,
279            M::Key: Ord,
280        {
281            if let Some(current) = current.as_mut_opt() {
282                for (k, new_v) in new.into_opt().into_iter().flatten() {
283                    match current.entry(k) {
284                        std::collections::btree_map::Entry::Vacant(entry) => { entry.insert(new_v); }
285                        std::collections::btree_map::Entry::Occupied(entry) => merge_value(entry.into_mut(), new_v),
286                    }
287                }
288            }
289            else {
290                current.set_if_some(new);
291            }
292        }
293    }
294}