diff --git a/SYNTAX.md b/SYNTAX.md index 699c5e6..7cb1b29 100644 --- a/SYNTAX.md +++ b/SYNTAX.md @@ -103,8 +103,18 @@ Conditionals are marked using an opening `if` block and a closing `endif` block. It can also have zero or more optional `else if` clauses and an optional `else` clause. A conditional renders the contents of the block based on the specified condition which can be any -[**expression**](#expressions) but it must resolve to a boolean value. A -boolean expression can be negated by applying the prefix `not`. +[**expression**](#expressions). An expression can be negated by applying the +prefix `not`. The conditional evaluates the expression based on it’s +truthiness. The following values are considered falsy, every other value is +truthy and will pass the condition: + +- `None` +- Boolean `false` +- An integer with value `0` +- A float with value `0.0` +- An empty string +- An empty list +- An empty map Consider the following template. If the nested field `user.is_enabled` is returns `false` then the first paragraph would be rendered. Otherwise if diff --git a/examples/serde.rs b/examples/serde.rs index 64b1200..226293b 100644 --- a/examples/serde.rs +++ b/examples/serde.rs @@ -19,7 +19,7 @@ fn main() -> upon::Result<()> { let output = upon::Engine::new() .compile("Hello {{ user.name }}!")? - .render(&ctx)?; + .render(ctx)?; println!("{output}"); diff --git a/src/compile/mod.rs b/src/compile/mod.rs index 54cc80d..4401947 100644 --- a/src/compile/mod.rs +++ b/src/compile/mod.rs @@ -81,14 +81,13 @@ impl Compiler { then_branch, else_branch, }) => { - let span = cond.span(); self.compile_expr(cond); // then branch let instr = if not { - Instr::JumpIfTrue(FIXME, span) + Instr::JumpIfTrue(FIXME) } else { - Instr::JumpIfFalse(FIXME, span) + Instr::JumpIfFalse(FIXME) }; let j = self.push(instr); self.compile_scope(then_branch); @@ -176,10 +175,7 @@ impl Compiler { fn update_jump(&mut self, i: usize) { let n = self.instrs.len(); let j = match &mut self.instrs[i] { - Instr::Jump(j) - | Instr::JumpIfTrue(j, _) - | Instr::JumpIfFalse(j, _) - | Instr::LoopNext(j) => j, + Instr::Jump(j) | Instr::JumpIfTrue(j) | Instr::JumpIfFalse(j) | Instr::LoopNext(j) => j, _ => panic!("not a jump instr"), }; *j = n; diff --git a/src/render/mod.rs b/src/render/mod.rs index e2cbc23..1e39718 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -128,15 +128,15 @@ impl<'a> Renderer<'a> { continue; } - Instr::JumpIfTrue(j, span) => { - if expr.take().unwrap().as_bool(&t.source, *span)? { + Instr::JumpIfTrue(j) => { + if expr.take().unwrap().as_bool() { *pc = *j; continue; } } - Instr::JumpIfFalse(j, span) => { - if !expr.take().unwrap().as_bool(&t.source, *span)? { + Instr::JumpIfFalse(j) => { + if !expr.take().unwrap().as_bool() { *pc = *j; continue; } diff --git a/src/render/value.rs b/src/render/value.rs index acd9579..0ee0489 100644 --- a/src/render/value.rs +++ b/src/render/value.rs @@ -1,20 +1,16 @@ use crate::types::ast; -use crate::types::span::Span; use crate::value::ValueCow; use crate::{Error, Result, Value}; impl ValueCow<'_> { - pub fn as_bool(&self, source: &str, span: Span) -> Result { + pub fn as_bool(&self) -> bool { match &**self { - Value::Bool(cond) => Ok(*cond), - value => { - let v = value.human(); - Err(Error::render( - format!("expected bool, but expression evaluated to {v}"), - source, - span, - )) - } + Value::None | Value::Bool(false) | Value::Integer(0) => false, + Value::Float(n) if *n == 0.0 => false, + Value::String(s) if s.is_empty() => false, + Value::List(l) if l.is_empty() => false, + Value::Map(m) if m.is_empty() => false, + _ => true, } } } diff --git a/src/syntax.rs b/src/syntax.rs index e1924b5..7a38e71 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -85,8 +85,17 @@ //! block. It can also have zero or more optional `else if` clauses and an //! optional `else` clause. A conditional renders the contents of the block //! based on the specified condition which can be any -//! [**expression**](#expressions) but it must resolve to a boolean value. A -//! boolean expression can be negated by applying the prefix `not`. +//! [**expression**](#expressions). An expression can be negated by applying the +//! prefix `not`. The conditional evaluates the expression based on it's +//! truthiness. The following values are considered falsy, every other value is +//! truthy and will pass the condition: +//! - `None` +//! - Boolean `false` +//! - An integer with value `0` +//! - A float with value `0.0` +//! - An empty string +//! - An empty list +//! - An empty map //! //! Consider the following template. If the nested field `user.is_enabled` is //! returns `false` then the first paragraph would be rendered. Otherwise if diff --git a/src/types/program.rs b/src/types/program.rs index 3447178..652c08e 100644 --- a/src/types/program.rs +++ b/src/types/program.rs @@ -21,10 +21,10 @@ pub enum Instr { Jump(usize), /// Jump to the instruction if the current expression is true - JumpIfTrue(usize, Span), + JumpIfTrue(usize), /// Jump to the instruction if the current expression is false - JumpIfFalse(usize, Span), + JumpIfFalse(usize), /// Emit the current expression Emit(Span), diff --git a/tests/render.rs b/tests/render.rs index 94d42f1..cbc10a5 100644 --- a/tests/render.rs +++ b/tests/render.rs @@ -5,6 +5,7 @@ mod helpers; use std::collections::BTreeMap; use std::error::Error as _; use std::fmt::Write; +use std::iter::zip; use upon::fmt; use upon::{value, Engine, Error, Value}; @@ -427,64 +428,101 @@ fn render_inline_expr_err_not_found_in_map() { ); } +fn falsy() -> Vec { + vec![ + Value::None, + Value::Bool(false), + Value::Integer(0), + Value::Float(0.0), + Value::String(String::new()), + Value::List(vec![]), + Value::Map(BTreeMap::new()), + ] +} + +fn truthy() -> Vec { + vec![ + Value::Bool(true), + Value::Integer(1337), + Value::Float(13.37), + Value::String("testing".into()), + Value::from([1, 2, 3]), + Value::from([("a", 1i64), ("b", 2i64)]), + ] +} + #[test] fn render_if_statement_cond_true() { - let result = Engine::new() - .compile("lorem {% if ipsum.dolor %}{{ sit }}{% else %}{{ amet }}{% endif %}") - .unwrap() - .render(value! { ipsum: { dolor: true }, sit: "consectetur" }) - .unwrap(); - assert_eq!(result, "lorem consectetur") + for value in truthy() { + let result = Engine::new() + .compile("lorem {% if ipsum %}{{ sit }}{% else %}{{ amet }}{% endif %}") + .unwrap() + .render(value! { ipsum: value.clone(), sit: "consectetur" }) + .unwrap(); + assert_eq!(result, "lorem consectetur"); + } } #[test] fn render_if_statement_cond_false() { - let result = Engine::new() - .compile("lorem {% if ipsum.dolor %}{{ sit }}{% else %}{{ amet }}{% endif %}") - .unwrap() - .render(value! { ipsum: { dolor: false }, amet: "consectetur" }) - .unwrap(); - assert_eq!(result, "lorem consectetur") + for value in falsy() { + let result = Engine::new() + .compile("lorem {% if ipsum.dolor %}{{ sit }}{% else %}{{ amet }}{% endif %}") + .unwrap() + .render(value! { ipsum: { dolor: value.clone() }, amet: "consectetur" }) + .unwrap(); + assert_eq!(result, "lorem consectetur"); + } } #[test] fn render_if_statement_cond_not() { - let result = Engine::new() - .compile("lorem {% if not ipsum.dolor %}{{ sit }}{% else %}{{ amet }}{% endif %}") - .unwrap() - .render(value! { ipsum: { dolor: false }, sit: "consectetur" }) - .unwrap(); - assert_eq!(result, "lorem consectetur") + for value in falsy() { + let result = Engine::new() + .compile("lorem {% if not ipsum.dolor %}{{ sit }}{% else %}{{ amet }}{% endif %}") + .unwrap() + .render(value! { ipsum: {dolor: value.clone()}, sit: "consectetur" }) + .unwrap(); + assert_eq!(result, "lorem consectetur"); + } } #[test] fn render_if_statement_else_if_cond_false() { - let result = Engine::new() - .compile("lorem {% if ipsum %} dolor {% else if sit %} amet {% endif %}, consectetur") - .unwrap() - .render(value! { ipsum: false, sit: false }) - .unwrap(); - assert_eq!(result, "lorem , consectetur"); + for value in falsy() { + let result = Engine::new() + .compile("lorem {% if ipsum %} dolor {% else if sit %} amet {% endif %}, consectetur") + .unwrap() + .render(value! { ipsum: value.clone(), sit: value.clone() }) + .unwrap(); + assert_eq!(result, "lorem , consectetur"); + } } #[test] fn render_if_statement_else_if_cond_true() { - let result = Engine::new() - .compile("lorem {% if ipsum %} dolor {% else if sit %} amet {% endif %}, consectetur") - .unwrap() - .render(value! { ipsum: false, sit: true }) - .unwrap(); - assert_eq!(result, "lorem amet , consectetur"); + for (t, f) in zip(truthy(), falsy()) { + let result = Engine::new() + .compile("lorem {% if ipsum %} dolor {% else if sit %} amet {% endif %}, consectetur") + .unwrap() + .render(value! { ipsum: f, sit: t }) + .unwrap(); + assert_eq!(result, "lorem amet , consectetur"); + } } #[test] fn render_if_statement_else_if_cond_not() { - let result = Engine::new() - .compile("lorem {% if ipsum %} dolor {% else if not sit %} amet {% endif %}, consectetur") - .unwrap() - .render(value! { ipsum: false, sit: false }) - .unwrap(); - assert_eq!(result, "lorem amet , consectetur"); + for falsy in falsy() { + let result = Engine::new() + .compile( + "lorem {% if ipsum %} dolor {% else if not sit %} amet {% endif %}, consectetur", + ) + .unwrap() + .render(value! { ipsum: falsy.clone(), sit: falsy }) + .unwrap(); + assert_eq!(result, "lorem amet , consectetur"); + } } #[test] @@ -521,48 +559,6 @@ fn render_if_statement_multi() { } } -#[test] -fn render_if_statement_err_cond_not_bool() { - let err = Engine::new() - .compile("lorem {% if ipsum.dolor %}{{ sit }}{% endif %}") - .unwrap() - .render(value! { ipsum: { dolor: { } } }) - .unwrap_err(); - assert_err( - &err, - "expected bool, but expression evaluated to map", - " - --> :1:13 - | - 1 | lorem {% if ipsum.dolor %}{{ sit }}{% endif %} - | ^^^^^^^^^^^ - | - = reason: REASON -", - ); -} - -#[test] -fn render_if_statement_err_cond_not_not_bool() { - let err = Engine::new() - .compile("lorem {% if not ipsum.dolor %}{{ sit }}{% endif %}") - .unwrap() - .render(value! { ipsum: { dolor: { } } }) - .unwrap_err(); - assert_err( - &err, - "expected bool, but expression evaluated to map", - " - --> :1:17 - | - 1 | lorem {% if not ipsum.dolor %}{{ sit }}{% endif %} - | ^^^^^^^^^^^ - | - = reason: REASON -", - ); -} - #[test] fn render_for_statement_list() { let result = Engine::new()