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}