#
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 let serde_json::Value::Object(this) = self {
    78            if let serde_json::Value::Object(other) = other {
    79                for (k, v) in other {
    80                    if v.is_null() {
    81                        this.remove(&k);
    82                    }
    83                    else {
    84                        this.entry(k).or_insert(serde_json::Value::Null).merge_from(v);
    85                    }
    86                }
    87
    88                return;
    89            }
    90        }
    91
    92        *self = other;
    93    }
    94}
    95
    96impl<T> DeepMerge for std::boxed::Box<T> where T: DeepMerge {
    97    fn merge_from(&mut self, other: Self) {
    98        (**self).merge_from(*other);
    99    }
    100}
    101
    102impl<T> DeepMerge for Option<T> where T: DeepMerge {
    103    fn merge_from(&mut self, other: Self) {
    104        if let Some(other) = other {
    105            if let Some(s) = self {
    106                s.merge_from(other);
    107            }
    108            else {
    109                *self = Some(other);
    110            }
    111        }
    112    }
    113}
    114
    115/// Strategies for merging collections.
    116pub mod strategies {
    117    /// Strategies for merging [`std::vec::Vec`]s.
    118    ///
    119    /// These correspond to [`JSONSchemaProps.x-kubernetes-list-type`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#jsonschemaprops-v1-apiextensions-k8s-io).
    120    pub mod list {
    121        mod private {
    122            pub trait AsOptVec {
    123                type Item;
    124
    125                fn set_if_some(&mut self, new: Self);
    126                fn as_mut_opt(&mut self) -> Option<&mut std::vec::Vec<Self::Item>>;
    127                fn into_opt(self) -> Option<std::vec::Vec<Self::Item>>;
    128            }
    129
    130            impl<T> AsOptVec for std::vec::Vec<T> {
    131                type Item = T;
    132
    133                fn set_if_some(&mut self, new: Self) {
    134                    *self = new;
    135                }
    136
    137                fn as_mut_opt(&mut self) -> Option<&mut Self> {
    138                    Some(self)
    139                }
    140
    141                fn into_opt(self) -> Option<Self> {
    142                    Some(self)
    143                }
    144            }
    145
    146            impl<T> AsOptVec for Option<std::vec::Vec<T>> {
    147                type Item = T;
    148
    149                fn set_if_some(&mut self, new: Self) {
    150                    if new.is_some() {
    151                        *self = new;
    152                    }
    153                }
    154
    155                fn as_mut_opt(&mut self) -> Option<&mut std::vec::Vec<T>> {
    156                    self.as_mut()
    157                }
    158
    159                fn into_opt(self) -> Self {
    160                    self
    161                }
    162            }
    163        }
    164        use private::AsOptVec;
    165
    166        /// The whole list is treated as one scalar value, and will be replaced with the new (non-[`None`]) value.
    167        pub fn atomic<V>(current: &mut V, new: V) where V: AsOptVec {
    168            current.set_if_some(new);
    169        }
    170
    171        /// The list is treated as a map.
    172        ///
    173        /// Any items with matching keys will be deep-merged. Any items in `new` that are not in `current` will be appended to `current`.
    174        pub fn map<V>(
    175            current: &mut V,
    176            new: V,
    177            key_comparators: &[fn(&V::Item, &V::Item) -> bool],
    178            merge_item: fn(&mut V::Item, V::Item),
    179        )
    180        where
    181            V: AsOptVec,
    182        {
    183            if let Some(current) = current.as_mut_opt() {
    184                for new_item in new.into_opt().into_iter().flatten() {
    185                    if let Some(current_item) = current.iter_mut().find(|current_item| key_comparators.iter().all(|&f| f(&**current_item, &new_item))) {
    186                        merge_item(current_item, new_item);
    187                    }
    188                    else {
    189                        current.push(new_item);
    190                    }
    191                }
    192            }
    193            else {
    194                current.set_if_some(new);
    195            }
    196        }
    197
    198        /// The list is treated as a set.
    199        ///
    200        /// Items from `new` will be appended to `current`, _unless_ `current` already contains an equal item.
    201        pub fn set<V>(current: &mut V, new: V) where V: AsOptVec, V::Item: PartialEq {
    202            if let Some(current) = current.as_mut_opt() {
    203                for item in new.into_opt().into_iter().flatten() {
    204                    if !current.contains(&item) {
    205                        current.push(item);
    206                    }
    207                }
    208            }
    209            else {
    210                current.set_if_some(new);
    211            }
    212        }
    213    }
    214
    215    /// Strategies for merging [`BTreeMap`s.](std::collections::BTreeMap)
    216    ///
    217    /// These correspond to [`JSONSchemaProps.x-kubernetes-map-type`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#jsonschemaprops-v1-apiextensions-k8s-io).
    218    pub mod map {
    219        mod private {
    220            use std::collections::BTreeMap;
    221
    222            pub trait AsOptMap {
    223                type Key;
    224                type Value;
    225
    226                fn set_if_some(&mut self, new: Self);
    227                fn as_mut_opt(&mut self) -> Option<&mut BTreeMap<Self::Key, Self::Value>>;
    228                fn into_opt(self) -> Option<BTreeMap<Self::Key, Self::Value>>;
    229            }
    230
    231            impl<K, V> AsOptMap for BTreeMap<K, V> {
    232                type Key = K;
    233                type Value = V;
    234
    235                fn set_if_some(&mut self, new: Self) {
    236                    *self = new;
    237                }
    238
    239                fn as_mut_opt(&mut self) -> Option<&mut Self> {
    240                    Some(self)
    241                }
    242
    243                fn into_opt(self) -> Option<Self> {
    244                    Some(self)
    245                }
    246            }
    247
    248            impl<K, V> AsOptMap for Option<BTreeMap<K, V>> {
    249                type Key = K;
    250                type Value = V;
    251
    252                fn set_if_some(&mut self, new: Self) {
    253                    if new.is_some() {
    254                        *self = new;
    255                    }
    256                }
    257
    258                fn as_mut_opt(&mut self) -> Option<&mut BTreeMap<K, V>> {
    259                    self.as_mut()
    260                }
    261
    262                fn into_opt(self) -> Self {
    263                    self
    264                }
    265            }
    266        }
    267        use private::AsOptMap;
    268
    269        /// The whole map is treated as one scalar value, and will be replaced with the new (non-[`None`]) value.
    270        pub fn atomic<M>(current: &mut M, new: M) where M: AsOptMap {
    271            current.set_if_some(new);
    272        }
    273
    274        /// Each value will be merged separately.
    275        pub fn granular<M>(current: &mut M, new: M, merge_value: fn(&mut M::Value, M::Value))
    276        where
    277            M: AsOptMap,
    278            M::Key: Ord,
    279        {
    280            if let Some(current) = current.as_mut_opt() {
    281                for (k, new_v) in new.into_opt().into_iter().flatten() {
    282                    match current.entry(k) {
    283                        std::collections::btree_map::Entry::Vacant(entry) => { entry.insert(new_v); }
    284                        std::collections::btree_map::Entry::Occupied(entry) => merge_value(entry.into_mut(), new_v),
    285                    }
    286                }
    287            }
    288            else {
    289                current.set_if_some(new);
    290            }
    291        }
    292    }
    293}