Repository: mitsuhiko/minijinja (rust)

Issue:
Add support for the `*` operator
Jinja2 supports e.g. `{{ ' ' * 10 }}` but that doesn't work with MiniJinja. We were able to circumvent the issue with a recursive macro, but that operator would simplify things!

```
Error: invalid operation: tried to use * operator on unsupported types string and number (in template.html:15)
```

Below is the workaround in case anyone else is having the same issue.

```
{%- macro repeat(s, times) -%}
  {%- if times >= 1 -%}
    {{ s }}
  
    {%- if times > 1 -%}
      {{ repeat(s, times - 1) }}
    {%- endif -%}
    
  {%- endif -%}
{%- endmacro -%}

{{ repeat("abc", 4) }}
```


Pull request description:
Implement mul behavior from Jinja2
This implements the mul behavior from Jinja2.  Specifically `{{ iterable * num }}` repeats `iterable` `num` times.

Fixes #518 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d192a3f..4db3a8a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
 
 All notable changes to MiniJinja are documented here.
 
+## 2.0.2
+
+- Implemented sequence (+ some iterator) and string repeating with the `*`
+  operator to match Jinja2 behavior.  #519
+
 ## 2.0.1
 
 - Fixed an issue that caused custom delimiters to not work in the Python
diff --git a/Makefile b/Makefile
index 3090dec..20c41b8 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
 DOC_FEATURES=loader,json,urlencode,custom_syntax,fuel
-TEST_FEATURES=unstable_machinery,builtins,loader,json,urlencode,debug,internal_debug,macros,multi_template,adjacent_loop_items,custom_syntax
+TEST_FEATURES=unstable_machinery,builtins,loader,json,urlencode,debug,internal_debug,macros,multi_template,adjacent_loop_items,custom_syntax,deserialization
 
 .PHONY: all
 all: test
diff --git a/minijinja/src/value/ops.rs b/minijinja/src/value/ops.rs
index 62eff79..eab8298 100644
--- a/minijinja/src/value/ops.rs
+++ b/minijinja/src/value/ops.rs
@@ -1,5 +1,5 @@
 use crate::error::{Error, ErrorKind};
-use crate::value::{ObjectRepr, Value, ValueKind, ValueRepr};
+use crate::value::{DynObject, ObjectRepr, Value, ValueKind, ValueRepr};
 
 const MIN_I128_AS_POS_U128: u128 = 170141183460469231731687303715884105728;
 
@@ -204,9 +204,93 @@ pub fn add(lhs: &Value, rhs: &Value) -> Result<Value, Error> {
 }
 
 math_binop!(sub, checked_sub, -);
-math_binop!(mul, checked_mul, *);
 math_binop!(rem, checked_rem_euclid, %);
 
+pub fn mul(lhs: &Value, rhs: &Value) -> Result<Value, Error> {
+    if let Some((s, n)) = lhs
+        .as_str()
+        .map(|s| (s, rhs))
+        .or_else(|| rhs.as_str().map(|s| (s, lhs)))
+    {
+        return Ok(Value::from(s.repeat(ok!(n.as_usize().ok_or_else(|| {
+            Error::new(
+                ErrorKind::InvalidOperation,
+                "strings can only be multiplied with integers",
+            )
+        })))));
+    } else if let Some((seq, n)) = lhs
+        .as_object()
+        .map(|s| (s, rhs))
+        .or_else(|| rhs.as_object().map(|s| (s, lhs)))
+        .filter(|x| matches!(x.0.repr(), ObjectRepr::Iterable | ObjectRepr::Seq))
+    {
+        return repeat_iterable(n, seq);
+    }
+
+    match coerce(lhs, rhs) {
+        Some(CoerceResult::I128(a, b)) => match a.checked_mul(b) {
+            Some(val) => Ok(int_as_value(val)),
+            None => Err(failed_op(stringify!(*), lhs, rhs)),
+        },
+        Some(CoerceResult::F64(a, b)) => Ok((a * b).into()),
+        _ => Err(impossible_op(stringify!(*), lhs, rhs)),
+    }
+}
+
+fn repeat_iterable(n: &Value, seq: &DynObject) -> Result<Value, Error> {
+    struct LenIterWrap<I: Send + Sync>(usize, I);
+
+    impl<I: Iterator<Item = Value> + Send + Sync> Iterator for LenIterWrap<I> {
+        type Item = Value;
+
+        #[inline(always)]
+        fn next(&mut self) -> Option<Self::Item> {
+            self.1.next()
+        }
+
+        #[inline(always)]
+        fn size_hint(&self) -> (usize, Option<usize>) {
+            (self.0, Some(self.0))
+        }
+    }
+
+    let n = ok!(n.as_usize().ok_or_else(|| {
+        Error::new(
+            ErrorKind::InvalidOperation,
+            "sequences and iterables can only be multiplied with integers",
+        )
+    }));
+
+    let len = ok!(seq.enumerator_len().ok_or_else(|| {
+        Error::new(
+            ErrorKind::InvalidOperation,
+            "cannot repeat unsized iterables",
+        )
+    }));
+
+    // This is not optimal.  We only query the enumerator for the length once
+    // but we support repeated iteration.  We could both lie about our length
+    // here and we could actually deal with an object that changes how much
+    // data it returns.  This is not really permissible so we won't try to
+    // improve on this here.
+    Ok(Value::make_object_iterable(seq.clone(), move |seq| {
+        Box::new(LenIterWrap(
+            len * n,
+            (0..n).flat_map(move |_| {
+                seq.try_iter().unwrap_or_else(|| {
+                    Box::new(
+                        std::iter::repeat(Value::from(Error::new(
+                            ErrorKind::InvalidOperation,
+                            "iterable did not iterate against expectations",
+                        )))
+                        .take(len),
+                    )
+                })
+            }),
+        ))
+    }))
+}
+
 pub fn div(lhs: &Value, rhs: &Value) -> Result<Value, Error> {
     fn do_it(lhs: &Value, rhs: &Value) -> Option<Value> {
         let a = some!(as_f64(lhs));


Test patch:
diff --git a/minijinja/tests/inputs/err_repeat_iterator_with_object.txt b/minijinja/tests/inputs/err_repeat_iterator_with_object.txt
new file mode 100644
index 0000000..31211c1
--- /dev/null
+++ b/minijinja/tests/inputs/err_repeat_iterator_with_object.txt
@@ -0,0 +1,4 @@
+{
+}
+---
+{{ [1, 2, 3] * {} }}
diff --git a/minijinja/tests/inputs/err_repeat_iterator_with_string.txt b/minijinja/tests/inputs/err_repeat_iterator_with_string.txt
new file mode 100644
index 0000000..1ee5840
--- /dev/null
+++ b/minijinja/tests/inputs/err_repeat_iterator_with_string.txt
@@ -0,0 +1,4 @@
+{
+}
+---
+{{ [1, 2, 3] * "3" }}
diff --git a/minijinja/tests/inputs/err_repeat_true_iterator.txt b/minijinja/tests/inputs/err_repeat_true_iterator.txt
new file mode 100644
index 0000000..01cec6e
--- /dev/null
+++ b/minijinja/tests/inputs/err_repeat_true_iterator.txt
@@ -0,0 +1,4 @@
+{
+}
+---
+{{ one_shot_iterator * 3 }}
diff --git a/minijinja/tests/inputs/mul.txt b/minijinja/tests/inputs/mul.txt
new file mode 100644
index 0000000..1763f27
--- /dev/null
+++ b/minijinja/tests/inputs/mul.txt
@@ -0,0 +1,9 @@
+{
+}
+---
+{{ range(3) * 3 }}
+{{ "foo" * 3 }}
+{{ [1, 2] * 2 }}
+{{ [1, 2, 3, 4][:2] * 2 }}
+{{ 2 * [1, 2] }}
+{{ 2 * "foo" }}
diff --git a/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap b/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap
index 85b7dae..be74d65 100644
--- a/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap
+++ b/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap
@@ -19,6 +19,7 @@ State {
             depth: 0,
         },
         "f": minijinja::functions::builtins::range,
+        "one_shot_iterator": <iterator>,
         "upper": 1,
     },
     env: Environment {
diff --git a/minijinja/tests/snapshots/test_templates__vm@err_repeat_iterator_with_object.txt.snap b/minijinja/tests/snapshots/test_templates__vm@err_repeat_iterator_with_object.txt.snap
new file mode 100644
index 0000000..ab17664
--- /dev/null
+++ b/minijinja/tests/snapshots/test_templates__vm@err_repeat_iterator_with_object.txt.snap
@@ -0,0 +1,22 @@
+---
+source: minijinja/tests/test_templates.rs
+description: "{{ [1, 2, 3] * {} }}"
+info: {}
+input_file: minijinja/tests/inputs/err_repeat_iterator_with_object.txt
+---
+!!!ERROR!!!
+
+Error {
+    kind: InvalidOperation,
+    detail: "sequences and iterables can only be multiplied with integers",
+    name: "err_repeat_iterator_with_object.txt",
+    line: 1,
+}
+
+invalid operation: sequences and iterables can only be multiplied with integers (in err_repeat_iterator_with_object.txt:1)
+--------------------- err_repeat_iterator_with_object.txt ---------------------
+   1 > {{ [1, 2, 3] * {} }}
+     i    ^^^^^^^^^^^^^^ invalid operation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+No referenced variables
+-------------------------------------------------------------------------------
diff --git a/minijinja/tests/snapshots/test_templates__vm@err_repeat_iterator_with_string.txt.snap b/minijinja/tests/snapshots/test_templates__vm@err_repeat_iterator_with_string.txt.snap
new file mode 100644
index 0000000..edddc19
--- /dev/null
+++ b/minijinja/tests/snapshots/test_templates__vm@err_repeat_iterator_with_string.txt.snap
@@ -0,0 +1,22 @@
+---
+source: minijinja/tests/test_templates.rs
+description: "{{ [1, 2, 3] * \"3\" }}"
+info: {}
+input_file: minijinja/tests/inputs/err_repeat_iterator_with_string.txt
+---
+!!!ERROR!!!
+
+Error {
+    kind: InvalidOperation,
+    detail: "strings can only be multiplied with integers",
+    name: "err_repeat_iterator_with_string.txt",
+    line: 1,
+}
+
+invalid operation: strings can only be multiplied with integers (in err_repeat_iterator_with_string.txt:1)
+--------------------- err_repeat_iterator_with_string.txt ---------------------
+   1 > {{ [1, 2, 3] * "3" }}
+     i    ^^^^^^^^^^^^^^^ invalid operation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+No referenced variables
+-------------------------------------------------------------------------------
diff --git a/minijinja/tests/snapshots/test_templates__vm@err_repeat_true_iterator.txt.snap b/minijinja/tests/snapshots/test_templates__vm@err_repeat_true_iterator.txt.snap
new file mode 100644
index 0000000..52dc281
--- /dev/null
+++ b/minijinja/tests/snapshots/test_templates__vm@err_repeat_true_iterator.txt.snap
@@ -0,0 +1,24 @@
+---
+source: minijinja/tests/test_templates.rs
+description: "{{ one_shot_iterator * 3 }}"
+info: {}
+input_file: minijinja/tests/inputs/err_repeat_true_iterator.txt
+---
+!!!ERROR!!!
+
+Error {
+    kind: InvalidOperation,
+    detail: "cannot repeat unsized iterables",
+    name: "err_repeat_true_iterator.txt",
+    line: 1,
+}
+
+invalid operation: cannot repeat unsized iterables (in err_repeat_true_iterator.txt:1)
+------------------------ err_repeat_true_iterator.txt -------------------------
+   1 > {{ one_shot_iterator * 3 }}
+     i    ^^^^^^^^^^^^^^^^^^^^^ invalid operation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Referenced variables: {
+    one_shot_iterator: <iterator>,
+}
+-------------------------------------------------------------------------------
diff --git a/minijinja/tests/snapshots/test_templates__vm@mul.txt.snap b/minijinja/tests/snapshots/test_templates__vm@mul.txt.snap
new file mode 100644
index 0000000..ede04b1
--- /dev/null
+++ b/minijinja/tests/snapshots/test_templates__vm@mul.txt.snap
@@ -0,0 +1,12 @@
+---
+source: minijinja/tests/test_templates.rs
+description: "{{ range(3) * 3 }}\n{{ \"foo\" * 3 }}\n{{ [1, 2] * 2 }}\n{{ [1, 2, 3, 4][:2] * 2 }}\n{{ 2 * [1, 2] }}\n{{ 2 * \"foo\" }}"
+info: {}
+input_file: minijinja/tests/inputs/mul.txt
+---
+[0, 1, 2, 0, 1, 2, 0, 1, 2]
+foofoofoo
+[1, 2, 1, 2]
+[1, 2, 1, 2]
+[1, 2, 1, 2]
+foofoo
diff --git a/minijinja/tests/test_templates.rs b/minijinja/tests/test_templates.rs
index 8e6fefe..9b38711 100644
--- a/minijinja/tests/test_templates.rs
+++ b/minijinja/tests/test_templates.rs
@@ -2,7 +2,8 @@
     feature = "multi_template",
     feature = "macros",
     feature = "builtins",
-    feature = "adjacent_loop_items"
+    feature = "adjacent_loop_items",
+    feature = "deserialization"
 ))]
 use std::collections::BTreeMap;
 use std::fmt::Write;
@@ -35,7 +36,7 @@ fn test_vm() {
         let contents = std::fs::read_to_string(path).unwrap();
         let mut iter = contents.splitn(2, "\n---\n");
         let mut env = Environment::new();
-        let ctx: serde_json::Value = serde_json::from_str(iter.next().unwrap()).unwrap();
+        let ctx: Value = serde_json::from_str(iter.next().unwrap()).unwrap();
 
         for (path, source) in &refs {
             let ref_filename = path.file_name().unwrap().to_str().unwrap();
@@ -50,7 +51,11 @@ fn test_vm() {
         } else {
             let template = env.get_template(filename).unwrap();
 
-            match template.render(&ctx) {
+            let actual_context = context! {
+                one_shot_iterator => Value::make_one_shot_iterator(0..3),
+                ..ctx.clone()
+            };
+            match template.render(&actual_context) {
                 Ok(mut rendered) => {
                     rendered.push('\n');
                     rendered