Skip to content

Commit

Permalink
Support string concat with + (#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuataylor committed Sep 2, 2022
1 parent 179842f commit 0201e1a
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 39 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/clippy.yml
@@ -1,6 +1,6 @@
name: Clippy

on: [push]
on: [push, pull_request]

jobs:
build:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/rustfmt.yml
@@ -1,6 +1,6 @@
name: Rustfmt

on: [push]
on: [push, pull_request]

jobs:
build:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
@@ -1,6 +1,6 @@
name: Tests

on: [push]
on: [push, pull_request]

jobs:
test-latest:
Expand Down
107 changes: 71 additions & 36 deletions minijinja/src/value.rs
Expand Up @@ -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,
},
}
Expand All @@ -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,
},
}
Expand Down Expand Up @@ -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> {
Expand All @@ -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)),
Expand Down Expand Up @@ -541,31 +547,46 @@ 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> {
fn do_it(lhs: &Value, rhs: &Value) -> Option<Value> {
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, %);
Expand All @@ -576,35 +597,18 @@ 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> {
fn do_it(lhs: &Value, rhs: &Value) -> Option<Value> {
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.
Expand All @@ -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.
Expand Down Expand Up @@ -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]
Expand Down
16 changes: 16 additions & 0 deletions 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 }}
4 changes: 4 additions & 0 deletions minijinja/tests/inputs/set.txt
Expand Up @@ -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 }}
18 changes: 18 additions & 0 deletions 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

4 changes: 4 additions & 0 deletions minijinja/tests/snapshots/test_templates__vm@set.txt.snap
Expand Up @@ -51,3 +51,7 @@ Filter block

[THIS IS A WAS TRUE]

Set with +

helloworld

0 comments on commit 0201e1a

Please sign in to comment.