Skip to content

Commit 9dd096d

Browse files
authored
Added chain filter (#807)
1 parent 02a5ba4 commit 9dd096d

File tree

9 files changed

+259
-39
lines changed

9 files changed

+259
-39
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ All notable changes to MiniJinja are documented here.
1313
* Allow `isize` as argument type. #799
1414
* MiniJinja now correctly handles `\x` escape sequences in strings
1515
as well as octals. #805
16+
* Added a new `|chain` filter. #807
1617

1718
## 2.10.2
1819

minijinja/src/defaults.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ pub(crate) fn get_builtin_filters() -> BTreeMap<Cow<'static, str>, Value> {
110110
rv.insert("map".into(), Value::from_function(filters::map));
111111
rv.insert("groupby".into(), Value::from_function(filters::groupby));
112112
rv.insert("unique".into(), Value::from_function(filters::unique));
113+
rv.insert("chain".into(), Value::from_function(filters::chain));
113114
rv.insert("pprint".into(), Value::from_function(filters::pprint));
114115

115116
#[cfg(feature = "json")]

minijinja/src/filters.rs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,6 @@
113113
//!
114114
//! Some additional filters are available in the
115115
//! [`minijinja-contrib`](https://crates.io/crates/minijinja-contrib) crate.
116-
use std::sync::Arc;
117-
118116
use crate::error::Error;
119117
use crate::utils::write_escaped;
120118
use crate::value::Value;
@@ -169,12 +167,14 @@ mod builtins {
169167

170168
use crate::error::ErrorKind;
171169
use crate::utils::{safe_sort, splitn_whitespace};
170+
use crate::value::merge_object::{MergeDict, MergeSeq};
172171
use crate::value::ops::{self, as_f64};
173172
use crate::value::{Enumerator, Kwargs, Object, ObjectRepr, ValueKind, ValueRepr};
174173
use std::borrow::Cow;
175174
use std::cmp::Ordering;
176175
use std::fmt::Write;
177176
use std::mem;
177+
use std::sync::Arc;
178178

179179
/// Converts a value to uppercase.
180180
///
@@ -1467,6 +1467,57 @@ mod builtins {
14671467
Ok(Value::from(rv))
14681468
}
14691469

1470+
/// Chain two or more iterable objects as a single iterable object.
1471+
///
1472+
/// If all the individual objects are dictionaries, then the final chained object
1473+
/// also acts like a dictionary -- you can lookup a key, or iterate over the keys
1474+
/// etc. Note that the dictionaries are not merged, so if there are duplicate keys,
1475+
/// then the lookup will return the value from the last matching dictionary in the
1476+
/// chain.
1477+
///
1478+
/// If all the individual objects are sequences, then the final chained
1479+
/// object also acts like a list as if the lists are appended.
1480+
///
1481+
/// Otherwise, the chained object acts like an iterator chaining individual
1482+
/// iterators, but it cannot be indexed.
1483+
///
1484+
/// ```jinja
1485+
/// {{ users | chain(moreusers) | length }}
1486+
/// {% for user, info in shard0 | chain(shard1, shard2) | dictsort %}
1487+
/// {{user}}: {{info}}
1488+
/// {% endfor %}
1489+
/// {{ list1 | chain(list2) | attr(1) }}
1490+
/// ```
1491+
#[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
1492+
pub fn chain(
1493+
_state: &State,
1494+
value: Value,
1495+
others: crate::value::Rest<Value>,
1496+
) -> Result<Value, Error> {
1497+
let all_values = Some(value.clone())
1498+
.into_iter()
1499+
.chain(others.0.iter().cloned())
1500+
.collect::<Vec<_>>();
1501+
1502+
if all_values.iter().all(|v| v.kind() == ValueKind::Map) {
1503+
Ok(Value::from_object(MergeDict::new(all_values)))
1504+
} else if all_values
1505+
.iter()
1506+
.all(|v| matches!(v.kind(), ValueKind::Seq))
1507+
{
1508+
Ok(Value::from_object(MergeSeq::new(all_values)))
1509+
} else {
1510+
// General iterator chaining behavior
1511+
Ok(Value::make_object_iterable(all_values, |values| {
1512+
Box::new(values.iter().flat_map(|v| match v.try_iter() {
1513+
Ok(iter) => Box::new(iter) as Box<dyn Iterator<Item = Value> + Send + Sync>,
1514+
Err(err) => Box::new(Some(Value::from(err)).into_iter())
1515+
as Box<dyn Iterator<Item = Value> + Send + Sync>,
1516+
})) as Box<dyn Iterator<Item = Value> + Send + Sync>
1517+
}))
1518+
}
1519+
}
1520+
14701521
/// Pretty print a variable.
14711522
///
14721523
/// This is useful for debugging as it better shows what's inside an object.

minijinja/src/value/merge_object.rs

Lines changed: 84 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,97 @@
11
use std::collections::BTreeSet;
22
use std::sync::Arc;
33

4-
use crate::value::{Enumerator, Object, Value, ValueKind};
4+
use crate::value::ops::LenIterWrap;
5+
use crate::value::{Enumerator, Object, ObjectExt, ObjectRepr, Value, ValueKind};
56

6-
#[derive(Clone, Debug)]
7-
struct MergeObject(pub Box<[Value]>);
7+
/// Dictionary merging behavior - create custom object with lookup capability
8+
#[derive(Debug)]
9+
pub struct MergeDict {
10+
values: Box<[Value]>,
11+
}
812

9-
impl Object for MergeObject {
13+
impl MergeDict {
14+
pub fn new(values: Vec<Value>) -> Self {
15+
Self {
16+
values: values.into_boxed_slice(),
17+
}
18+
}
19+
}
20+
21+
impl Object for MergeDict {
1022
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())
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
1532
}
1633

1734
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
35+
// Collect all keys from all dictionaries (only include maps)
36+
let keys: BTreeSet<Value> = self
37+
.values
2238
.iter()
2339
.filter(|x| x.kind() == ValueKind::Map)
24-
.flat_map(|v| v.try_iter().ok())
40+
.filter_map(|v| v.try_iter().ok())
2541
.flatten()
26-
.collect::<BTreeSet<_>>();
27-
Enumerator::Iter(Box::new(items.into_iter()))
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)]
49+
pub struct MergeSeq {
50+
values: Box<[Value]>,
51+
total_len: Option<usize>,
52+
}
53+
54+
impl 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+
63+
impl 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+
})
2895
}
2996
}
3097

@@ -60,14 +127,13 @@ where
60127
I: IntoIterator<Item = V>,
61128
V: Into<Value>,
62129
{
63-
let mut sources: Box<[Value]> = iter.into_iter().map(Into::into).collect();
130+
let sources: Vec<Value> = iter.into_iter().map(Into::into).collect();
64131
// if we only have a single source, we can use it directly to avoid making
65132
// an unnecessary indirection.
66133
if sources.len() == 1 {
67134
sources[0].clone()
68135
} else {
69-
sources.reverse();
70-
Value::from_object(MergeObject(sources))
136+
Value::from_object(MergeDict::new(sources))
71137
}
72138
}
73139

minijinja/src/value/ops.rs

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,23 @@ use crate::value::{DynObject, ObjectRepr, Value, ValueKind, ValueRepr};
33

44
const MIN_I128_AS_POS_U128: u128 = 170141183460469231731687303715884105728;
55

6+
/// Iterator wrapper that provides exact size hints for iterators with known length.
7+
pub(crate) struct LenIterWrap<I: Send + Sync>(pub(crate) usize, pub(crate) I);
8+
9+
impl<I: Iterator<Item = Value> + Send + Sync> Iterator for LenIterWrap<I> {
10+
type Item = Value;
11+
12+
#[inline(always)]
13+
fn next(&mut self) -> Option<Self::Item> {
14+
self.1.next()
15+
}
16+
17+
#[inline(always)]
18+
fn size_hint(&self) -> (usize, Option<usize>) {
19+
(self.0, Some(self.0))
20+
}
21+
}
22+
623
pub enum CoerceResult<'a> {
724
I128(i128, i128),
825
F64(f64, f64),
@@ -316,22 +333,6 @@ pub fn mul(lhs: &Value, rhs: &Value) -> Result<Value, Error> {
316333
}
317334

318335
fn repeat_iterable(n: &Value, seq: &DynObject) -> Result<Value, Error> {
319-
struct LenIterWrap<I: Send + Sync>(usize, I);
320-
321-
impl<I: Iterator<Item = Value> + Send + Sync> Iterator for LenIterWrap<I> {
322-
type Item = Value;
323-
324-
#[inline(always)]
325-
fn next(&mut self) -> Option<Self::Item> {
326-
self.1.next()
327-
}
328-
329-
#[inline(always)]
330-
fn size_hint(&self) -> (usize, Option<usize>) {
331-
(self.0, Some(self.0))
332-
}
333-
}
334-
335336
let n = ok!(n.as_usize().ok_or_else(|| {
336337
Error::new(
337338
ErrorKind::InvalidOperation,

minijinja/tests/inputs/filters.txt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@
1616
"a": "b",
1717
"c": "d"
1818
},
19-
"scary_html": "<>&'"
19+
"scary_html": "<>&'",
20+
"list2": [4, 5, 6],
21+
"list3": [7, 8],
22+
"map2": {
23+
"e": "f",
24+
"g": "h"
25+
}
2026
}
2127
---
2228
lower: {{ word|lower }}
@@ -121,3 +127,9 @@ sum-empty: {{ []|sum }}
121127
sum-float: {{ [0.5, 1.0]|sum }}
122128
lines: {{ "foo\nbar\r\nbaz"|lines }}
123129
string: {{ [1|string, 2|string] }}
130+
chain-lists: {{ list|chain(list2)|list }}
131+
chain-multiple: {{ list|chain(list2, list3)|list }}
132+
chain-maps: {{ map|chain(map2) }}
133+
chain-and-length: {{ list|chain(list2)|length }}
134+
chain-iteration: {% for item in list|chain(list2) %}{{ item }}{% if not loop.last %},{% endif %}{% endfor %}
135+
chain-indexing: {{ (list|chain(list2))[4] }}

minijinja/tests/snapshots/[email protected]

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ State {
7474
"batch",
7575
"bool",
7676
"capitalize",
77+
"chain",
7778
"count",
7879
"d",
7980
"default",

0 commit comments

Comments
 (0)