Skip to main content

normalize_core/
merge.rs

1//! Merge trait for configuration layering.
2//!
3//! Used to merge global config with project config, where "other" (project) wins.
4
5use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
6use std::path::PathBuf;
7
8/// Trait for merging configuration values.
9///
10/// Convention: `other` takes precedence over `self`.
11/// For Option types, `other` wins if Some, otherwise falls back to `self`.
12pub trait Merge {
13    fn merge(self, other: Self) -> Self;
14}
15
16// === Primitives (other always wins) ===
17
18impl Merge for bool {
19    fn merge(self, other: Self) -> Self {
20        other
21    }
22}
23
24impl Merge for i8 {
25    fn merge(self, other: Self) -> Self {
26        other
27    }
28}
29
30impl Merge for i16 {
31    fn merge(self, other: Self) -> Self {
32        other
33    }
34}
35
36impl Merge for i32 {
37    fn merge(self, other: Self) -> Self {
38        other
39    }
40}
41
42impl Merge for i64 {
43    fn merge(self, other: Self) -> Self {
44        other
45    }
46}
47
48impl Merge for i128 {
49    fn merge(self, other: Self) -> Self {
50        other
51    }
52}
53
54impl Merge for isize {
55    fn merge(self, other: Self) -> Self {
56        other
57    }
58}
59
60impl Merge for u8 {
61    fn merge(self, other: Self) -> Self {
62        other
63    }
64}
65
66impl Merge for u16 {
67    fn merge(self, other: Self) -> Self {
68        other
69    }
70}
71
72impl Merge for u32 {
73    fn merge(self, other: Self) -> Self {
74        other
75    }
76}
77
78impl Merge for u64 {
79    fn merge(self, other: Self) -> Self {
80        other
81    }
82}
83
84impl Merge for u128 {
85    fn merge(self, other: Self) -> Self {
86        other
87    }
88}
89
90impl Merge for usize {
91    fn merge(self, other: Self) -> Self {
92        other
93    }
94}
95
96impl Merge for f32 {
97    fn merge(self, other: Self) -> Self {
98        other
99    }
100}
101
102impl Merge for f64 {
103    fn merge(self, other: Self) -> Self {
104        other
105    }
106}
107
108impl Merge for char {
109    fn merge(self, other: Self) -> Self {
110        other
111    }
112}
113
114// === Strings ===
115
116impl Merge for String {
117    fn merge(self, other: Self) -> Self {
118        other
119    }
120}
121
122impl Merge for PathBuf {
123    fn merge(self, other: Self) -> Self {
124        other
125    }
126}
127
128// === Option: merge inner values if both Some ===
129
130impl<T: Merge> Merge for Option<T> {
131    fn merge(self, other: Self) -> Self {
132        match (self, other) {
133            (Some(a), Some(b)) => Some(a.merge(b)),
134            (None, b) => b,
135            (a, None) => a,
136        }
137    }
138}
139
140// === Collections: extend/merge ===
141
142impl<T> Merge for Vec<T> {
143    /// Vectors: other replaces self entirely (not appended)
144    fn merge(self, other: Self) -> Self {
145        other
146    }
147}
148
149impl<K: Eq + std::hash::Hash, V> Merge for HashMap<K, V> {
150    /// HashMaps: other's keys override self's
151    fn merge(mut self, other: Self) -> Self {
152        self.extend(other);
153        self
154    }
155}
156
157impl<K: Ord, V> Merge for BTreeMap<K, V> {
158    /// BTreeMaps: other's keys override self's
159    fn merge(mut self, other: Self) -> Self {
160        self.extend(other);
161        self
162    }
163}
164
165impl<T: Eq + std::hash::Hash> Merge for HashSet<T> {
166    /// HashSets: union
167    fn merge(mut self, other: Self) -> Self {
168        self.extend(other);
169        self
170    }
171}
172
173impl<T: Ord> Merge for BTreeSet<T> {
174    /// BTreeSets: union
175    fn merge(mut self, other: Self) -> Self {
176        self.extend(other);
177        self
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn test_bool_merge() {
187        assert!(false.merge(true));
188        assert!(!true.merge(false));
189    }
190
191    #[test]
192    fn test_option_merge() {
193        assert_eq!(None::<i32>.merge(Some(1)), Some(1));
194        assert_eq!(Some(1).merge(None), Some(1));
195        assert_eq!(Some(1).merge(Some(2)), Some(2)); // inner merge: other wins for primitives
196    }
197
198    #[test]
199    fn test_option_hashmap_merge() {
200        // Option<HashMap> should merge inner hashmaps when both Some
201        let a: Option<HashMap<&str, i32>> = Some([("x", 1), ("y", 2)].into_iter().collect());
202        let b: Option<HashMap<&str, i32>> = Some([("y", 3), ("z", 4)].into_iter().collect());
203
204        // normalize-syntax-allow: rust/unwrap-in-impl - test code, panic is appropriate
205        let merged = a.merge(b).unwrap();
206        assert_eq!(merged.get("x"), Some(&1)); // from a
207        assert_eq!(merged.get("y"), Some(&3)); // b wins
208        assert_eq!(merged.get("z"), Some(&4)); // from b
209    }
210
211    #[test]
212    fn test_hashmap_merge() {
213        let mut a = HashMap::new();
214        a.insert("a", 1);
215        a.insert("b", 2);
216
217        let mut b = HashMap::new();
218        b.insert("b", 3);
219        b.insert("c", 4);
220
221        let merged = a.merge(b);
222        assert_eq!(merged.get("a"), Some(&1));
223        assert_eq!(merged.get("b"), Some(&3)); // b wins
224        assert_eq!(merged.get("c"), Some(&4));
225    }
226}