1#![doc = include_str!("../README.md")]
2
3use cfg_if::cfg_if;
4use std::collections::HashMap;
5use tanzim_load::Payload;
6use tanzim_value::{LocatedValue, Map, Value};
7
8pub type Merged = HashMap<Option<String>, (Vec<Payload>, LocatedValue)>;
12
13pub trait Merge {
20 fn merge(&self, parsed_list: &[(Payload, LocatedValue)]) -> Result<Merged, Error>;
25}
26
27#[derive(Debug, thiserror::Error)]
29pub enum Error {
30 #[error(transparent)]
31 Other(#[from] Box<dyn std::error::Error + Send + Sync>),
32}
33
34pub struct LastWins;
38
39impl Merge for LastWins {
40 fn merge(&self, parsed_list: &[(Payload, LocatedValue)]) -> Result<Merged, Error> {
41 cfg_if! {
42 if #[cfg(feature = "tracing")] {
43 tracing::debug!(msg = "Merging configuration with last-wins strategy", entry_count = parsed_list.len());
44 } else if #[cfg(feature = "logging")] {
45 log::debug!("msg=\"Merging configuration with last-wins strategy\" entry_count={}", parsed_list.len());
46 }
47 }
48 let mut result: Merged = HashMap::new();
49 for (payload, value) in parsed_list {
50 let key = payload.maybe_name.clone();
51 cfg_if! {
52 if #[cfg(feature = "tracing")] {
53 tracing::trace!(msg = "Applied last-wins merge entry", name = ?key);
54 } else if #[cfg(feature = "logging")] {
55 log::trace!("msg=\"Applied last-wins merge entry\" name={key:?}");
56 }
57 }
58 result.insert(key, (vec![payload.clone()], value.clone()));
59 }
60 cfg_if! {
61 if #[cfg(feature = "tracing")] {
62 tracing::info!(msg = "Merged configuration with last-wins strategy", group_count = result.len());
63 } else if #[cfg(feature = "logging")] {
64 log::info!("msg=\"Merged configuration with last-wins strategy\" group_count={}", result.len());
65 }
66 }
67 Ok(result)
68 }
69}
70
71pub struct DeepMerge;
77
78fn deep_merge_value(base: LocatedValue, overlay: LocatedValue) -> LocatedValue {
79 if let (Value::Map(base_map), Value::Map(overlay_map)) = (base.value(), overlay.value()) {
80 let mut result_map = Map::new();
81 let base_entries = base_map.entries();
82 let overlay_entries = overlay_map.entries();
83
84 for (key, base_val) in base_entries {
85 if let Some(overlay_val) = overlay_map.get(key) {
86 result_map.insert(
87 key.clone(),
88 deep_merge_value(base_val.clone(), overlay_val.clone()),
89 );
90 } else {
91 result_map.insert(key.clone(), base_val.clone());
92 }
93 }
94
95 for (key, overlay_val) in overlay_entries {
96 if !result_map.contains_key(key) {
97 result_map.insert(key.clone(), overlay_val.clone());
98 }
99 }
100
101 return LocatedValue::new(Value::Map(result_map), overlay.location().clone());
102 }
103 overlay
104}
105
106impl Merge for DeepMerge {
107 fn merge(&self, parsed_list: &[(Payload, LocatedValue)]) -> Result<Merged, Error> {
108 cfg_if! {
109 if #[cfg(feature = "tracing")] {
110 tracing::debug!(msg = "Merging configuration with deep-merge strategy", entry_count = parsed_list.len());
111 } else if #[cfg(feature = "logging")] {
112 log::debug!("msg=\"Merging configuration with deep-merge strategy\" entry_count={}", parsed_list.len());
113 }
114 }
115 let mut result: Merged = HashMap::new();
116
117 for (payload, value) in parsed_list {
118 let key = payload.maybe_name.clone();
119
120 if let Some(existing) = result.get_mut(&key) {
121 cfg_if! {
122 if #[cfg(feature = "tracing")] {
123 tracing::debug!(msg = "Deep-merging into existing entry", name = ?key);
124 } else if #[cfg(feature = "logging")] {
125 log::debug!("msg=\"Deep-merging into existing entry\" name={key:?}");
126 }
127 }
128 existing.0.push(payload.clone());
129 let merged = deep_merge_value(existing.1.clone(), value.clone());
130 existing.1 = merged;
131 } else {
132 cfg_if! {
133 if #[cfg(feature = "tracing")] {
134 tracing::trace!(msg = "Added new deep-merge entry", name = ?key);
135 } else if #[cfg(feature = "logging")] {
136 log::trace!("msg=\"Added new deep-merge entry\" name={key:?}");
137 }
138 }
139 result.insert(key, (vec![payload.clone()], value.clone()));
140 }
141 }
142
143 cfg_if! {
144 if #[cfg(feature = "tracing")] {
145 tracing::info!(msg = "Merged configuration with deep-merge strategy", group_count = result.len());
146 } else if #[cfg(feature = "logging")] {
147 log::info!("msg=\"Merged configuration with deep-merge strategy\" group_count={}", result.len());
148 }
149 }
150 Ok(result)
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use tanzim_load::Payload;
158 use tanzim_source::SourceBuilder;
159 use tanzim_value::{LocatedValue, Location, Map, Value};
160
161 fn source() -> tanzim_source::Source {
162 SourceBuilder::new()
163 .with_source("mock")
164 .with_resource("test")
165 .build()
166 .unwrap()
167 }
168
169 fn payload(name: Option<&str>) -> Payload {
170 Payload {
171 source: source(),
172 maybe_name: name.map(str::to_string),
173 maybe_format: Some("txt".into()),
174 content: Vec::new(),
175 }
176 }
177
178 fn string_value(text: &str) -> LocatedValue {
179 LocatedValue::new(
180 Value::String(text.to_string()),
181 Location::at("mock", "test", None, None, None),
182 )
183 }
184
185 fn map_value(entries: &[(&str, &str)]) -> LocatedValue {
186 let mut map = Map::new();
187 for (key, value) in entries {
188 map.insert(key.to_string(), string_value(value));
189 }
190 LocatedValue::new(
191 Value::Map(map),
192 Location::at("mock", "test", None, None, None),
193 )
194 }
195
196 #[test]
197 fn last_wins_empty_input() {
198 let merged = LastWins.merge(&[]).unwrap();
199 assert!(merged.is_empty());
200 }
201
202 #[test]
203 fn last_wins_keeps_last_value_for_same_name() {
204 let parsed = vec![
205 (payload(Some("app")), string_value("first")),
206 (payload(Some("app")), string_value("second")),
207 ];
208 let merged = LastWins.merge(&parsed).unwrap();
209 let (_, value) = merged.get(&Some("app".into())).unwrap();
210 assert_eq!(value.value().as_string().unwrap(), "second");
211 }
212
213 #[test]
214 fn last_wins_groups_unnamed_entries() {
215 let parsed = vec![
216 (payload(None), string_value("first")),
217 (payload(None), string_value("second")),
218 ];
219 let merged = LastWins.merge(&parsed).unwrap();
220 let (_, value) = merged.get(&None).unwrap();
221 assert_eq!(value.value().as_string().unwrap(), "second");
222 }
223
224 #[test]
225 fn last_wins_distinct_names() {
226 let parsed = vec![
227 (payload(Some("alpha")), string_value("a")),
228 (payload(Some("beta")), string_value("b")),
229 ];
230 let merged = LastWins.merge(&parsed).unwrap();
231 assert_eq!(merged.len(), 2);
232 assert_eq!(
233 merged
234 .get(&Some("alpha".into()))
235 .unwrap()
236 .1
237 .value()
238 .as_string()
239 .unwrap(),
240 "a"
241 );
242 assert_eq!(
243 merged
244 .get(&Some("beta".into()))
245 .unwrap()
246 .1
247 .value()
248 .as_string()
249 .unwrap(),
250 "b"
251 );
252 }
253
254 #[test]
255 fn deep_merge_empty_input() {
256 let merged = DeepMerge.merge(&[]).unwrap();
257 assert!(merged.is_empty());
258 }
259
260 #[test]
261 fn deep_merge_recurses_into_shared_map_keys() {
262 let parsed = vec![
263 (
264 payload(Some("app")),
265 map_value(&[("host", "localhost"), ("port", "8080")]),
266 ),
267 (
268 payload(Some("app")),
269 map_value(&[("port", "9090"), ("debug", "true")]),
270 ),
271 ];
272 let merged = DeepMerge.merge(&parsed).unwrap();
273 let (payloads, value) = merged.get(&Some("app".into())).unwrap();
274 assert_eq!(payloads.len(), 2);
275 let map = value.value().as_map().unwrap();
276 assert_eq!(
277 map.get("host").unwrap().value().as_string().unwrap(),
278 "localhost"
279 );
280 assert_eq!(
281 map.get("port").unwrap().value().as_string().unwrap(),
282 "9090"
283 );
284 assert_eq!(
285 map.get("debug").unwrap().value().as_string().unwrap(),
286 "true"
287 );
288 }
289
290 #[test]
291 fn deep_merge_scalar_overlay_replaces_map() {
292 let parsed = vec![
293 (payload(Some("app")), map_value(&[("mode", "auto")])),
294 (payload(Some("app")), string_value("override")),
295 ];
296 let merged = DeepMerge.merge(&parsed).unwrap();
297 let (_, value) = merged.get(&Some("app".into())).unwrap();
298 assert_eq!(value.value().as_string().unwrap(), "override");
299 }
300
301 #[test]
302 fn deep_merge_unnamed_bucket() {
303 let parsed = vec![
304 (payload(None), map_value(&[("a", "1")])),
305 (payload(None), map_value(&[("b", "2")])),
306 ];
307 let merged = DeepMerge.merge(&parsed).unwrap();
308 let (payloads, value) = merged.get(&None).unwrap();
309 assert_eq!(payloads.len(), 2);
310 let map = value.value().as_map().unwrap();
311 assert_eq!(map.get("a").unwrap().value().as_string().unwrap(), "1");
312 assert_eq!(map.get("b").unwrap().value().as_string().unwrap(), "2");
313 }
314}