minijinja/value/
merge_object.rs

1use std::collections::BTreeSet;
2use std::sync::Arc;
3
4use crate::value::ops::LenIterWrap;
5use crate::value::{Enumerator, Object, ObjectExt, ObjectRepr, Value, ValueKind};
6
7/// Dictionary merging behavior - create custom object with lookup capability
8#[derive(Debug)]
9pub struct MergeDict {
10    values: Box<[Value]>,
11}
12
13impl MergeDict {
14    pub fn new(values: Vec<Value>) -> Self {
15        Self {
16            values: values.into_boxed_slice(),
17        }
18    }
19}
20
21impl Object for MergeDict {
22    fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
23        // Look up key in reverse order (last matching dict wins)
24        for value in self.values.iter().rev() {
25            if let Ok(v) = value.get_item(key) {
26                if !v.is_undefined() {
27                    return Some(v);
28                }
29            }
30        }
31        None
32    }
33
34    fn enumerate(self: &Arc<Self>) -> Enumerator {
35        // Collect all keys from all dictionaries (only include maps)
36        let keys: BTreeSet<Value> = self
37            .values
38            .iter()
39            .filter(|x| x.kind() == ValueKind::Map)
40            .filter_map(|v| v.try_iter().ok())
41            .flatten()
42            .collect();
43        Enumerator::Iter(Box::new(keys.into_iter()))
44    }
45}
46
47/// List merging behavior - calculate total length for size hint
48#[derive(Debug)]
49pub struct MergeSeq {
50    values: Box<[Value]>,
51    total_len: Option<usize>,
52}
53
54impl MergeSeq {
55    pub fn new(values: Vec<Value>) -> Self {
56        Self {
57            total_len: values.iter().map(|v| v.len()).sum(),
58            values: values.into_boxed_slice(),
59        }
60    }
61}
62
63impl Object for MergeSeq {
64    fn repr(self: &Arc<Self>) -> ObjectRepr {
65        ObjectRepr::Seq
66    }
67
68    fn get_value(self: &Arc<Self>, key: &Value) -> Option<Value> {
69        if let Some(idx) = key.as_usize() {
70            let mut current_idx = 0;
71            for value in self.values.iter() {
72                let len = value.len().unwrap_or(0);
73                if idx < current_idx + len {
74                    return value.get_item(&Value::from(idx - current_idx)).ok();
75                }
76                current_idx += len;
77            }
78        }
79        None
80    }
81
82    fn enumerate(self: &Arc<Self>) -> Enumerator {
83        self.mapped_enumerator(|this| {
84            let iter = this.values.iter().flat_map(|v| match v.try_iter() {
85                Ok(iter) => Box::new(iter) as Box<dyn Iterator<Item = Value> + Send + Sync>,
86                Err(err) => Box::new(Some(Value::from(err)).into_iter())
87                    as Box<dyn Iterator<Item = Value> + Send + Sync>,
88            });
89            if let Some(total_len) = this.total_len {
90                Box::new(LenIterWrap(total_len, iter))
91            } else {
92                Box::new(iter)
93            }
94        })
95    }
96}
97
98/// Utility function to merge multiple maps into a single one.
99///
100/// If values are passed that are not maps, they are for the most part ignored.
101/// They cannot be enumerated, but attribute lookups can still work.   That's
102/// because [`get_value`](crate::value::Object::get_value) is forwarded through
103/// to all objects.
104///
105/// This is the operation the [`context!`](crate::context) macro uses behind
106/// the scenes.  The merge is done lazily which means that any dynamic object
107/// that behaves like a map can be used here.  Note though that the order of
108/// this function is inverse to what the macro does.
109///
110/// ```
111/// use minijinja::{context, value::merge_maps};
112///
113/// let ctx1 = context!{
114///     name => "John",
115///     age => 30
116/// };
117///
118/// let ctx2 = context!{
119///     location => "New York",
120///     age => 25  // This will be overridden by ctx1's value
121/// };
122///
123/// let merged = merge_maps([ctx1, ctx2]);
124/// ```
125pub fn merge_maps<I, V>(iter: I) -> Value
126where
127    I: IntoIterator<Item = V>,
128    V: Into<Value>,
129{
130    let sources: Vec<Value> = iter.into_iter().map(Into::into).collect();
131    // if we only have a single source, we can use it directly to avoid making
132    // an unnecessary indirection.
133    if sources.len() == 1 {
134        sources[0].clone()
135    } else {
136        Value::from_object(MergeDict::new(sources))
137    }
138}
139
140#[test]
141fn test_merge_object() {
142    use std::collections::BTreeMap;
143
144    let o = merge_maps([Value::from("abc"), Value::from(vec![1, 2, 3])]);
145    assert_eq!(o, Value::from(BTreeMap::<String, String>::new()));
146
147    let mut map1 = BTreeMap::new();
148    map1.insert("a", 1);
149    map1.insert("b", 2);
150
151    let mut map2 = BTreeMap::new();
152    map2.insert("b", 3);
153    map2.insert("c", 4);
154
155    let merged = merge_maps([Value::from(map1), Value::from(map2)]);
156
157    // Check that the merged object contains all keys with expected values
158    // The value from the latter map should be used when keys overlap
159    assert_eq!(merged.get_attr("a").unwrap(), Value::from(1));
160    assert_eq!(merged.get_attr("b").unwrap(), Value::from(3)); // Takes value from map2
161    assert_eq!(merged.get_attr("c").unwrap(), Value::from(4));
162}