diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index e812a0e5..aa834207 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 90029629..a7a0ce60 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/.github/workflows/tests.yml b/.github/workflows/tests.yml index af14db36..52282688 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/src/value.rs b/minijinja/src/value.rs index 70f27cd4..0782f45f 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 { @@ -465,6 +468,9 @@ fn coerce(a: &Value, b: &Value) -> Option { (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 { @@ -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 { + fn do_it(lhs: &Value, rhs: &Value) -> Option { + 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 { 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 { @@ -593,18 +605,10 @@ pub(crate) fn int_div(lhs: &Value, rhs: &Value) -> Result { 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 { 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] diff --git a/minijinja/tests/inputs/adding.txt b/minijinja/tests/inputs/adding.txt new file mode 100644 index 00000000..7d3dc7be --- /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 fd27d65c..ab2d6b0b 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 00000000..b1470578 --- /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 dc12e4ae..7bd4e8b0 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 +