Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support string concat with + #87

Merged
merged 4 commits into from Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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