Repository: mitsuhiko/minijinja
Pull request #87 (2022-09-02 02:36:20)

Support string concat with +
This allows doing something like:
```jinja
{{ 'hello' + 'world' }}
```

Not supported is - and /


Patch:
diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml
index e812a0e..aa83420 100644
--- a/.github/workflows/clippy.yml
+++ b/.github/workflows/clippy.yml
@@ -1,6 +1,6 @@
 name: Clippy
 
-on: [push]
+on: [push, pull_request]
 
 jobs:
   build:
diff --git a/.github/workflows/rustfmt.yml b/.github/workflows/rustfmt.yml
index 9002962..a7a0ce6 100644
--- a/.github/workflows/rustfmt.yml
+++ b/.github/workflows/rustfmt.yml
@@ -1,6 +1,6 @@
 name: Rustfmt
 
-on: [push]
+on: [push, pull_request]
 
 jobs:
   build:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c8e7ade..e8c82db 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ All notable changes to MiniJinja are documented here.
 - Add missing escape support for single quotes (`'`).  (#81) 
 - Added support for newlines in string literals.  (#85)
 - Added support for block assignment syntax.  (#86)
+- Enable debug mode by default in debug builds.  (#88)
 
 # 0.18.1
 
diff --git a/minijinja/src/environment.rs b/minijinja/src/environment.rs
index 59fdcd0..ec9ed9f 100644
--- a/minijinja/src/environment.rs
+++ b/minijinja/src/environment.rs
@@ -299,9 +299,10 @@ impl<'env, 'source> Expression<'env, 'source> {
 impl<'source> Environment<'source> {
     /// Creates a new environment with sensible defaults.
     ///
-    /// This environment does not yet contain any templates but it will have all the
-    /// default filters loaded.  If you do not want any default configuration you
-    /// can use the alternative [`empty`](Environment::empty) method.
+    /// This environment does not yet contain any templates but it will have all
+    /// the default filters, tests and globals loaded.  If you do not want any
+    /// default configuration you can use the alternative
+    /// [`empty`](Environment::empty) method.
     pub fn new() -> Environment<'source> {
         Environment {
             templates: Source::Borrowed(Default::default()),
@@ -310,14 +311,14 @@ impl<'source> Environment<'source> {
             globals: RcType::new(functions::get_globals()),
             default_auto_escape: RcType::new(default_auto_escape),
             #[cfg(feature = "debug")]
-            debug: false,
+            debug: cfg!(debug_assertions),
         }
     }
 
     /// Creates a completely empty environment.
     ///
-    /// This environment has no filters, no templates and no default logic for
-    /// auto escaping configured.
+    /// This environment has no filters, no templates, no globals and no default
+    /// logic for auto escaping configured.
     pub fn empty() -> Environment<'source> {
         Environment {
             templates: Source::Borrowed(Default::default()),
@@ -326,7 +327,7 @@ impl<'source> Environment<'source> {
             globals: RcType::default(),
             default_auto_escape: RcType::new(no_auto_escape),
             #[cfg(feature = "debug")]
-            debug: false,
+            debug: cfg!(debug_assertions),
         }
     }
 
@@ -363,7 +364,8 @@ impl<'source> Environment<'source> {
     /// return a [`DebugInfo`](crate::error::DebugInfo) object from
     /// [`Error::debug_info`](crate::error::Error::debug_info).
     ///
-    /// This requires the `debug` feature.
+    /// This requires the `debug` feature.  This is enabled by default if
+    /// debug assertions are enabled and false otherwise.
     #[cfg(feature = "debug")]
     #[cfg_attr(docsrs, doc(cfg(feature = "debug")))]
     pub fn set_debug(&mut self, enabled: bool) {
diff --git a/minijinja/src/value.rs b/minijinja/src/value.rs
index 70f27cd..0782f45 100644
--- a/minijinja/src/value.rs
+++ b/minijinja/src/value.rs
@@ -301,6 +301,7 @@ impl PartialEq for Value {
             _ => match coerce(self, other) {
                 Some(CoerceResult::F64(a, b)) => a == b,
                 Some(CoerceResult::I128(a, b)) => a == b,
+                Some(CoerceResult::String(a, b)) => a == b,
                 None => false,
             },
         }
@@ -319,6 +320,7 @@ impl PartialOrd for Value {
             _ => match coerce(self, other) {
                 Some(CoerceResult::F64(a, b)) => a.partial_cmp(&b),
                 Some(CoerceResult::I128(a, b)) => a.partial_cmp(&b),
+                Some(CoerceResult::String(a, b)) => a.partial_cmp(&b),
                 None => None,
             },
         }
@@ -444,6 +446,7 @@ value_from!(char, Char);
 enum CoerceResult {
     I128(i128, i128),
     F64(f64, f64),
+    String(String, String),
 }
 
 fn as_f64(value: &Value) -> Option<f64> {
@@ -465,6 +468,9 @@ fn coerce(a: &Value, b: &Value) -> Option<CoerceResult> {
         (ValueRepr::U128(a), ValueRepr::U128(b)) => {
             Some(CoerceResult::I128(**a as i128, **b as i128))
         }
+        (ValueRepr::String(a), ValueRepr::String(b)) => {
+            Some(CoerceResult::String(a.to_string(), b.to_string()))
+        }
         (ValueRepr::I64(a), ValueRepr::I64(b)) => Some(CoerceResult::I128(*a as i128, *b as i128)),
         (ValueRepr::I128(ref a), ValueRepr::I128(ref b)) => Some(CoerceResult::I128(**a, **b)),
         (ValueRepr::F64(a), ValueRepr::F64(b)) => Some(CoerceResult::F64(*a, *b)),
@@ -541,6 +547,18 @@ fn int_as_value(val: i128) -> Value {
     }
 }
 
+fn impossible_op(op: &str, lhs: &Value, rhs: &Value) -> Error {
+    Error::new(
+        ErrorKind::ImpossibleOperation,
+        format!(
+            "tried to use {} operator on unsupported types {} and {}",
+            op,
+            lhs.kind(),
+            rhs.kind()
+        ),
+    )
+}
+
 macro_rules! math_binop {
     ($name:ident, $int:ident, $float:tt) => {
         pub(crate) fn $name(lhs: &Value, rhs: &Value) -> Result<Value, Error> {
@@ -548,24 +566,27 @@ macro_rules! math_binop {
                 match coerce(lhs, rhs)? {
                     CoerceResult::I128(a, b) => Some(int_as_value(a.$int(b))),
                     CoerceResult::F64(a, b) => Some((a $float b).into()),
+                    _ => None
                 }
             }
             do_it(lhs, rhs).ok_or_else(|| {
-                Error::new(
-                    ErrorKind::ImpossibleOperation,
-                    format!(
-                        "tried to use {} operator on unsupported types {} and {}",
-                        stringify!($float),
-                        lhs.kind(),
-                        rhs.kind()
-                    )
-                )
+                impossible_op(stringify!($float), lhs, rhs)
             })
         }
     }
 }
 
-math_binop!(add, wrapping_add, +);
+pub(crate) fn add(lhs: &Value, rhs: &Value) -> Result<Value, Error> {
+    fn do_it(lhs: &Value, rhs: &Value) -> Option<Value> {
+        match coerce(lhs, rhs)? {
+            CoerceResult::I128(a, b) => Some(int_as_value(a.wrapping_add(b))),
+            CoerceResult::F64(a, b) => Some((a + b).into()),
+            CoerceResult::String(a, b) => Some(Value::from([a, b].concat())),
+        }
+    }
+    do_it(lhs, rhs).ok_or_else(|| impossible_op("+", lhs, rhs))
+}
+
 math_binop!(sub, wrapping_sub, -);
 math_binop!(mul, wrapping_mul, *);
 math_binop!(rem, wrapping_rem_euclid, %);
@@ -576,16 +597,7 @@ pub(crate) fn div(lhs: &Value, rhs: &Value) -> Result<Value, Error> {
         let b = as_f64(rhs)?;
         Some((a / b).into())
     }
-    do_it(lhs, rhs).ok_or_else(|| {
-        Error::new(
-            ErrorKind::ImpossibleOperation,
-            format!(
-                "tried to use / operator on unsupported types {} and {}",
-                lhs.kind(),
-                rhs.kind()
-            ),
-        )
-    })
+    do_it(lhs, rhs).ok_or_else(|| impossible_op("/", lhs, rhs))
 }
 
 pub(crate) fn int_div(lhs: &Value, rhs: &Value) -> Result<Value, Error> {
@@ -593,18 +605,10 @@ pub(crate) fn int_div(lhs: &Value, rhs: &Value) -> Result<Value, Error> {
         match coerce(lhs, rhs)? {
             CoerceResult::I128(a, b) => Some(int_as_value(a.div_euclid(b))),
             CoerceResult::F64(a, b) => Some(a.div_euclid(b).into()),
+            CoerceResult::String(_, _) => None,
         }
     }
-    do_it(lhs, rhs).ok_or_else(|| {
-        Error::new(
-            ErrorKind::ImpossibleOperation,
-            format!(
-                "tried to use // operator on unsupported types {} and {}",
-                lhs.kind(),
-                rhs.kind()
-            ),
-        )
-    })
+    do_it(lhs, rhs).ok_or_else(|| impossible_op("//", lhs, rhs))
 }
 
 /// Implements a binary `pow` operation on values.
@@ -613,14 +617,10 @@ pub(crate) fn pow(lhs: &Value, rhs: &Value) -> Result<Value, Error> {
         match coerce(lhs, rhs)? {
             CoerceResult::I128(a, b) => Some(int_as_value(a.pow(TryFrom::try_from(b).ok()?))),
             CoerceResult::F64(a, b) => Some((a.powf(b)).into()),
+            CoerceResult::String(_, _) => None,
         }
     }
-    do_it(lhs, rhs).ok_or_else(|| {
-        Error::new(
-            ErrorKind::ImpossibleOperation,
-            concat!("could not calculate the power"),
-        )
-    })
+    do_it(lhs, rhs).ok_or_else(|| impossible_op("**", lhs, rhs))
 }
 
 /// Implements an unary `neg` operation on value.
@@ -1732,6 +1732,41 @@ fn test_adding() {
     );
 
     assert_eq!(add(&value!(1), &value!(2)), Ok(value!(3)));
+    assert_eq!(add(&value!("foo"), &value!("bar")), Ok(value!("foobar")));
+}
+
+#[test]
+fn test_subtracting() {
+    let err = sub(&value!("a"), &value!(42)).unwrap_err();
+    assert_eq!(
+        err.to_string(),
+        "impossible operation: tried to use - operator on unsupported types string and number"
+    );
+
+    let err = sub(&value!("foo"), &value!("bar")).unwrap_err();
+    assert_eq!(
+        err.to_string(),
+        "impossible operation: tried to use - operator on unsupported types string and string"
+    );
+
+    assert_eq!(sub(&value!(2), &value!(1)), Ok(value!(1)));
+}
+
+#[test]
+fn test_dividing() {
+    let err = div(&value!("a"), &value!(42)).unwrap_err();
+    assert_eq!(
+        err.to_string(),
+        "impossible operation: tried to use / operator on unsupported types string and number"
+    );
+
+    let err = div(&value!("foo"), &value!("bar")).unwrap_err();
+    assert_eq!(
+        err.to_string(),
+        "impossible operation: tried to use / operator on unsupported types string and string"
+    );
+
+    assert_eq!(div(&value!(100), &value!(2)), Ok(value!(50.0)));
 }
 
 #[test]


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index af14db3..5228268 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,6 +1,6 @@
 name: Tests
 
-on: [push]
+on: [push, pull_request]
 
 jobs:
   test-latest:
diff --git a/minijinja/tests/inputs/adding.txt b/minijinja/tests/inputs/adding.txt
new file mode 100644
index 0000000..7d3dc7b
--- /dev/null
+++ b/minijinja/tests/inputs/adding.txt
@@ -0,0 +1,16 @@
+{
+  "name": "World"
+}
+---
+Strings:
+{{ 'to' + name + 'you' }}!
+{{ 'to' + 'you' }}!
+
+Adding:
+{{ 1 + 2 }}
+
+Minus:
+{{ 2 + 1 }}
+
+Divide:
+{{ 100 / 2 }}
\ No newline at end of file
diff --git a/minijinja/tests/inputs/set.txt b/minijinja/tests/inputs/set.txt
index fd27d65..ab2d6b0 100644
--- a/minijinja/tests/inputs/set.txt
+++ b/minijinja/tests/inputs/set.txt
@@ -41,3 +41,7 @@ Block:
 Filter block
 {% set upper_var | upper %}This is a {{ foo }}{% endset %}
 [{{ upper_var }}]
+
+Set with +
+{% set set_plus = "hello" + "world" %}
+{{ set_plus }}
\ No newline at end of file
diff --git a/minijinja/tests/snapshots/test_templates__vm@adding.txt.snap b/minijinja/tests/snapshots/test_templates__vm@adding.txt.snap
new file mode 100644
index 0000000..b147057
--- /dev/null
+++ b/minijinja/tests/snapshots/test_templates__vm@adding.txt.snap
@@ -0,0 +1,18 @@
+---
+source: minijinja/tests/test_templates.rs
+expression: "&rendered"
+input_file: minijinja/tests/inputs/adding.txt
+---
+Strings:
+toWorldyou!
+toyou!
+
+Adding:
+3
+
+Minus:
+3
+
+Divide:
+50.0
+
diff --git a/minijinja/tests/snapshots/test_templates__vm@set.txt.snap b/minijinja/tests/snapshots/test_templates__vm@set.txt.snap
index dc12e4a..7bd4e8b 100644
--- a/minijinja/tests/snapshots/test_templates__vm@set.txt.snap
+++ b/minijinja/tests/snapshots/test_templates__vm@set.txt.snap
@@ -51,3 +51,7 @@ Filter block
 
 [THIS IS A WAS TRUE]
 
+Set with +
+
+helloworld
+
diff --git a/minijinja/tests/test_templates.rs b/minijinja/tests/test_templates.rs
index f6582e0..5b83565 100644
--- a/minijinja/tests/test_templates.rs
+++ b/minijinja/tests/test_templates.rs
@@ -27,6 +27,10 @@ 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();
+        #[cfg(feature = "debug")]
+        {
+            env.set_debug(false);
+        }
         let ctx: serde_json::Value = serde_json::from_str(iter.next().unwrap()).unwrap();
 
         for (path, source) in &refs {