vrl/value/kind/
merge.rs

1//! All types related to merging one [`Kind`] into another.
2
3use super::Field;
4use std::ops::BitOr;
5
6use super::{Collection, Kind};
7
8impl Kind {
9    /// Merge `other` into `self`, using the provided `Strategy`.
10    pub fn merge(&mut self, other: Self, strategy: Strategy) {
11        self.merge_keep(other, strategy.collisions.is_shallow());
12    }
13
14    fn merge_primitives(&mut self, other: &Self) {
15        self.bytes = self.bytes.or(other.bytes);
16        self.integer = self.integer.or(other.integer);
17        self.float = self.float.or(other.float);
18        self.boolean = self.boolean.or(other.boolean);
19        self.timestamp = self.timestamp.or(other.timestamp);
20        self.regex = self.regex.or(other.regex);
21        self.null = self.null.or(other.null);
22        self.undefined = self.undefined.or(other.undefined);
23    }
24
25    fn merge_objects(&mut self, other: Option<Collection<Field>>, overwrite: bool) {
26        match (self.object.as_mut(), other) {
27            (None, rhs @ Some(_)) => self.object = rhs,
28            (Some(lhs), Some(rhs)) => lhs.merge(rhs, overwrite),
29            _ => {}
30        }
31    }
32
33    /// Returns the union of self and other.
34    #[must_use]
35    pub fn union(&self, other: Self) -> Self {
36        let mut kind = self.clone();
37        kind.merge_keep(other, false);
38        kind
39    }
40
41    /// Merge `other` into `self`, optionally overwriting on conflicts.
42    // deprecated
43    pub fn merge_keep(&mut self, other: Self, overwrite: bool) {
44        self.merge_primitives(&other);
45        self.merge_objects(other.object, overwrite);
46
47        match (self.array.as_mut(), other.array) {
48            (None, Some(rhs)) => self.array = Some(rhs),
49            (Some(lhs), Some(rhs)) => lhs.merge(rhs, overwrite),
50            _ => {}
51        }
52    }
53}
54
55/// The strategy to apply to the merge between two `Kind`s.
56#[derive(Debug, Clone, Copy, Eq, PartialEq)]
57pub struct Strategy {
58    /// How to deal with types in a collection if both specify a type.
59    /// This only applies to types in a collection (not the root type)
60    pub collisions: CollisionStrategy,
61}
62
63/// The choice of "depth" to apply when merging two [`Kind`]s.
64#[derive(Debug, Clone, Copy, Eq, PartialEq)]
65pub enum CollisionStrategy {
66    /// Use the 2nd value. This should no longer be used, and will be removed in the future.
67    /// Try using `Kind::insert` or a custom function instead.
68    Overwrite,
69
70    /// Merge both together
71    Union,
72}
73
74impl CollisionStrategy {
75    /// Check if `shallow` strategy is enabled.
76    #[must_use]
77    pub const fn is_shallow(self) -> bool {
78        matches!(self, Self::Overwrite)
79    }
80
81    /// Check if `deep` strategy is enabled.
82    #[must_use]
83    pub const fn is_deep(self) -> bool {
84        matches!(self, Self::Union)
85    }
86}
87
88impl BitOr for Kind {
89    type Output = Self;
90
91    fn bitor(self, rhs: Self) -> Self::Output {
92        self.union(rhs)
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use std::collections::{BTreeMap, HashMap};
100
101    #[test]
102    #[allow(clippy::too_many_lines)]
103    fn test_merge() {
104        struct TestCase {
105            this: Kind,
106            other: Kind,
107            strategy: Strategy,
108            merged: Kind,
109        }
110
111        for (
112            title,
113            TestCase {
114                mut this,
115                other,
116                strategy,
117                merged,
118            },
119        ) in HashMap::from([
120            (
121                "object field with unknown",
122                TestCase {
123                    this: Kind::object(Collection::any()),
124                    other: Kind::object(BTreeMap::from([("x".into(), Kind::integer())])),
125                    strategy: Strategy {
126                        collisions: CollisionStrategy::Union,
127                    },
128                    merged: {
129                        let mut collection =
130                            Collection::from(BTreeMap::from([("x".into(), Kind::any())]));
131                        collection.set_unknown(Kind::any());
132                        Kind::object(collection)
133                    },
134                },
135            ),
136            (
137                "primitives shallow",
138                TestCase {
139                    this: Kind::bytes(),
140                    other: Kind::integer(),
141                    strategy: Strategy {
142                        collisions: CollisionStrategy::Overwrite,
143                    },
144                    merged: Kind::bytes().or_integer(),
145                },
146            ),
147            (
148                "primitives deep",
149                TestCase {
150                    this: Kind::bytes(),
151                    other: Kind::integer(),
152                    strategy: Strategy {
153                        collisions: CollisionStrategy::Union,
154                    },
155                    merged: Kind::bytes().or_integer(),
156                },
157            ),
158            (
159                "mixed unknown shallow",
160                TestCase {
161                    this: Kind::bytes().or_object(Collection::from_unknown(Kind::integer())),
162                    other: Kind::bytes().or_object(Collection::from_unknown(Kind::bytes())),
163                    strategy: Strategy {
164                        collisions: CollisionStrategy::Overwrite,
165                    },
166                    merged: Kind::bytes()
167                        .or_object(Collection::from_unknown(Kind::integer().or_bytes())),
168                },
169            ),
170            (
171                "mixed unknown deep",
172                TestCase {
173                    this: Kind::bytes().or_object(Collection::from_unknown(Kind::integer())),
174                    other: Kind::bytes().or_object(Collection::from_unknown(Kind::bytes())),
175                    strategy: Strategy {
176                        collisions: CollisionStrategy::Union,
177                    },
178                    merged: Kind::bytes()
179                        .or_object(Collection::from_unknown(Kind::integer().or_bytes())),
180                },
181            ),
182            (
183                "mixed known shallow",
184                TestCase {
185                    this: Kind::bytes().or_object(BTreeMap::from([
186                        (
187                            "foo".into(),
188                            Kind::object(BTreeMap::from([
189                                ("qux".into(), Kind::bytes()),
190                                ("quux".into(), Kind::boolean()),
191                                ("this".into(), Kind::timestamp()),
192                            ])),
193                        ),
194                        ("bar".into(), Kind::integer()),
195                    ])),
196                    other: Kind::integer().or_object(BTreeMap::from([
197                        (
198                            "foo".into(),
199                            Kind::object(BTreeMap::from([
200                                ("qux".into(), Kind::integer()),
201                                ("quux".into(), Kind::regex()),
202                                ("that".into(), Kind::null()),
203                            ])),
204                        ),
205                        ("baz".into(), Kind::boolean()),
206                    ])),
207                    strategy: Strategy {
208                        collisions: CollisionStrategy::Overwrite,
209                    },
210                    merged: Kind::bytes().or_integer().or_object(BTreeMap::from([
211                        (
212                            "foo".into(),
213                            Kind::object(BTreeMap::from([
214                                ("qux".into(), Kind::integer()),
215                                ("quux".into(), Kind::regex()),
216                                ("that".into(), Kind::null()),
217                            ])),
218                        ),
219                        ("bar".into(), Kind::integer()),
220                        ("baz".into(), Kind::boolean()),
221                    ])),
222                },
223            ),
224            (
225                "mixed known deep",
226                TestCase {
227                    this: Kind::bytes().or_object(BTreeMap::from([
228                        (
229                            "foo".into(),
230                            Kind::object(BTreeMap::from([
231                                ("qux".into(), Kind::bytes()),
232                                ("quux".into(), Kind::boolean()),
233                                ("this".into(), Kind::timestamp()),
234                            ])),
235                        ),
236                        ("bar".into(), Kind::integer()),
237                    ])),
238                    other: Kind::integer().or_object(BTreeMap::from([
239                        (
240                            "foo".into(),
241                            Kind::object(BTreeMap::from([
242                                ("qux".into(), Kind::integer()),
243                                ("quux".into(), Kind::regex()),
244                                ("that".into(), Kind::null()),
245                            ])),
246                        ),
247                        ("baz".into(), Kind::boolean()),
248                    ])),
249                    strategy: Strategy {
250                        collisions: CollisionStrategy::Union,
251                    },
252                    merged: Kind::bytes().or_integer().or_object(BTreeMap::from([
253                        (
254                            "foo".into(),
255                            Kind::object(BTreeMap::from([
256                                ("qux".into(), Kind::bytes().or_integer()),
257                                ("quux".into(), Kind::boolean().or_regex()),
258                                ("this".into(), Kind::timestamp().or_undefined()),
259                                ("that".into(), Kind::null().or_undefined()),
260                            ])),
261                        ),
262                        ("bar".into(), Kind::integer().or_undefined()),
263                        ("baz".into(), Kind::boolean().or_undefined()),
264                    ])),
265                },
266            ),
267        ]) {
268            this.merge(other, strategy);
269            assert_eq!(this, merged, "{title}");
270        }
271    }
272}