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

Replace ValueCow::as_bool with truthiness function #17

Merged
merged 4 commits into from
Jul 1, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions SYNTAX.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ fn main() -> upon::Result<()> {

let output = upon::Engine::new()
.compile("Hello {{ user.name }}!")?
.render(&ctx)?;
.render(ctx)?;

println!("{output}");

Expand Down
10 changes: 3 additions & 7 deletions src/compile/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
18 changes: 7 additions & 11 deletions src/render/value.rs
Original file line number Diff line number Diff line change
@@ -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<bool> {
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,
rossmacarthur marked this conversation as resolved.
Show resolved Hide resolved
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,
}
}
}
Expand Down
13 changes: 11 additions & 2 deletions src/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/types/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
152 changes: 74 additions & 78 deletions tests/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -427,64 +428,101 @@ fn render_inline_expr_err_not_found_in_map() {
);
}

fn falsy() -> Vec<Value> {
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<Value> {
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]
Expand Down Expand Up @@ -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",
"
--> <anonymous>: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",
"
--> <anonymous>:1:17
|
1 | lorem {% if not ipsum.dolor %}{{ sit }}{% endif %}
| ^^^^^^^^^^^
|
= reason: REASON
",
);
}

#[test]
fn render_for_statement_list() {
let result = Engine::new()
Expand Down