minijinja/value/merge_object.rs
1use std::collections::BTreeSet;
2use std::sync::Arc;
3
4use crate::value::{Enumerator, Object, Value, ValueKind};
5
6#[derive(Clone, Debug)]
7struct MergeObject(pub Box<[Value]>);
8
9impl Object for MergeObject {
10 fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
11 self.0
12 .iter()
13 .filter_map(|x| x.get_item_opt(key))
14 .find(|x| !x.is_undefined())
15 }
16
17 fn enumerate(self: &Arc<Self>) -> Enumerator {
18 // we collect here the whole internal object once on iteration so that
19 // we have an enumerator with a known length.
20 let items = self
21 .0
22 .iter()
23 .filter(|x| x.kind() == ValueKind::Map)
24 .flat_map(|v| v.try_iter().ok())
25 .flatten()
26 .collect::<BTreeSet<_>>();
27 Enumerator::Iter(Box::new(items.into_iter()))
28 }
29}
30
31/// Utility function to merge multiple maps into a single one.
32///
33/// If values are passed that are not maps, they are for the most part ignored.
34/// They cannot be enumerated, but attribute lookups can still work. That's
35/// because [`get_value`](crate::value::Object::get_value) is forwarded through
36/// to all objects.
37///
38/// This is the operation the [`context!`](crate::context) macro uses behind
39/// the scenes. The merge is done lazily which means that any dynamic object
40/// that behaves like a map can be used here. Note though that the order of
41/// this function is inverse to what the macro does.
42///
43/// ```
44/// use minijinja::{context, value::merge_maps};
45///
46/// let ctx1 = context!{
47/// name => "John",
48/// age => 30
49/// };
50///
51/// let ctx2 = context!{
52/// location => "New York",
53/// age => 25 // This will be overridden by ctx1's value
54/// };
55///
56/// let merged = merge_maps([ctx1, ctx2]);
57/// ```
58pub fn merge_maps<I, V>(iter: I) -> Value
59where
60 I: IntoIterator<Item = V>,
61 V: Into<Value>,
62{
63 let mut sources: Box<[Value]> = iter.into_iter().map(Into::into).collect();
64 // if we only have a single source, we can use it directly to avoid making
65 // an unnecessary indirection.
66 if sources.len() == 1 {
67 sources[0].clone()
68 } else {
69 sources.reverse();
70 Value::from_object(MergeObject(sources))
71 }
72}
73
74#[test]
75fn test_merge_object() {
76 use std::collections::BTreeMap;
77
78 let o = merge_maps([Value::from("abc"), Value::from(vec![1, 2, 3])]);
79 assert_eq!(o, Value::from(BTreeMap::<String, String>::new()));
80
81 let mut map1 = BTreeMap::new();
82 map1.insert("a", 1);
83 map1.insert("b", 2);
84
85 let mut map2 = BTreeMap::new();
86 map2.insert("b", 3);
87 map2.insert("c", 4);
88
89 let merged = merge_maps([Value::from(map1), Value::from(map2)]);
90
91 // Check that the merged object contains all keys with expected values
92 // The value from the latter map should be used when keys overlap
93 assert_eq!(merged.get_attr("a").unwrap(), Value::from(1));
94 assert_eq!(merged.get_attr("b").unwrap(), Value::from(3)); // Takes value from map2
95 assert_eq!(merged.get_attr("c").unwrap(), Value::from(4));
96}