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

add If operation #8

Merged
merged 2 commits into from
Aug 9, 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
17 changes: 17 additions & 0 deletions rimu-value/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,23 @@ pub fn value_get_in<'a>(value: &'a Value, keys: &[&str]) -> Option<&'a Value> {
}
}

impl Value {
pub fn is_truthy(&self) -> bool {
match self {
Value::Null => false,
Value::Boolean(boolean) => *boolean,
Value::String(string) => string.len() > 0,
Value::Number(number) => match number {
Number::Unsigned(u) => u != &0_u64,
Number::Signed(s) => s != &0_i64,
Number::Float(f) => f != &0_f64,
},
Value::List(list) => list.len() > 0,
Value::Object(object) => object.len() > 0,
}
}
}

#[cfg(test)]
mod test {
use std::{borrow::Cow, ffi::OsString, path::PathBuf};
Expand Down
1 change: 1 addition & 0 deletions rimu/src/operations/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use super::Operation;
use crate::{Context, Engine, RenderError, Value};

#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EvalOperation {
#[serde(alias = "$eval")]
pub expr: String,
Expand Down
80 changes: 80 additions & 0 deletions rimu/src/operations/if_.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use serde::Deserialize;

use super::Operation;
use crate::{Context, Engine, RenderError, Template, Value};

#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IfOperation {
#[serde(rename = "$if")]
pub condition: Box<Template>,
#[serde(rename = "then")]
pub consequent: Option<Box<Template>>,
#[serde(rename = "else")]
pub alternative: Option<Box<Template>>,
}

impl Operation for IfOperation {
fn render(&self, engine: &Engine, context: &Context) -> Result<Value, RenderError> {
let condition = engine.render(&self.condition, context)?;

let value: Value = if let Value::String(condition) = condition {
engine.evaluate(&condition, context)?
} else {
condition
};

if value.is_truthy() {
if let Some(consequent) = &self.consequent {
engine.render(consequent, context)
} else {
Ok(Value::Null)
}
} else {
if let Some(alternative) = &self.alternative {
engine.render(alternative, context)
} else {
Ok(Value::Null)
}
}
}
}

#[cfg(test)]
mod tests {
use std::error::Error;

use super::*;
use crate::{Number, Value};

use map_macro::btree_map;
use pretty_assertions::assert_eq;

#[test]
fn if_() -> Result<(), Box<dyn Error>> {
let content = r#"
zero:
$if: ten > five
then:
$eval: five
else:
$eval: ten
"#;
let template: Template = serde_yaml::from_str(content)?;

let engine = Engine::default();
let mut context = Context::new();
context.insert("five", Value::Number(Number::Signed(5)));
context.insert("ten", Value::Number(Number::Signed(10)));

let actual: Value = engine.render(&template, &context)?;

let expected: Value = Value::Object(btree_map! {
"zero".into() => Value::Number(Number::Signed(5))
});

assert_eq!(expected, actual);

Ok(())
}
}
3 changes: 2 additions & 1 deletion rimu/src/operations/let_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use super::Operation;
use crate::{Context, Engine, RenderError, Template, Value};

#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LetOperation {
#[serde(rename = "$let")]
pub variables: Box<Template>,
Expand Down Expand Up @@ -32,7 +33,7 @@ mod tests {
use pretty_assertions::assert_eq;

#[test]
fn eval() -> Result<(), Box<dyn Error>> {
fn let_() -> Result<(), Box<dyn Error>> {
let content = r#"
zero:
$let:
Expand Down
5 changes: 5 additions & 0 deletions rimu/src/operations/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
mod eval;
mod if_;
mod let_;

use serde::{de::value::MapDeserializer, Deserialize};

pub use self::eval::EvalOperation;
pub use self::if_::IfOperation;
pub use self::let_::LetOperation;
use crate::{Context, Engine, Object, ParseError, RenderError, Value};

Expand All @@ -15,13 +17,15 @@ pub trait Operation {
pub enum Operations {
Eval(EvalOperation),
Let(LetOperation),
If(IfOperation),
}

impl Operations {
pub(crate) fn render(&self, engine: &Engine, context: &Context) -> Result<Value, RenderError> {
match self {
Operations::Eval(op) => op.render(engine, context),
Operations::Let(op) => op.render(engine, context),
Operations::If(op) => op.render(engine, context),
}
}
}
Expand All @@ -48,6 +52,7 @@ pub(crate) fn parse_operation(operator: &str, object: &Object) -> Result<Operati
match operator {
"$eval" => Ok(Operations::Eval(EvalOperation::deserialize(map_de)?)),
"$let" => Ok(Operations::Let(LetOperation::deserialize(map_de)?)),
"$if" => Ok(Operations::If(IfOperation::deserialize(map_de)?)),
_ => Err(ParseError::UnknownOperator {
operator: operator.to_owned(),
}),
Expand Down
5 changes: 5 additions & 0 deletions rimu/tests/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,8 @@ fn interpolation() -> Result<(), Box<dyn Error>> {
fn let_() -> Result<(), Box<dyn Error>> {
test_specs(include_str!("./spec/let.yml"))
}

#[test]
fn if_() -> Result<(), Box<dyn Error>> {
test_specs(include_str!("./spec/if.yml"))
}
156 changes: 156 additions & 0 deletions rimu/tests/spec/if.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
- title: $if-then-else, true
context: {cond: true}
template: {$if: 'cond', then: 1, else: 2}
output: 1

- title: $if-then-else, false
context: {cond: false}
template: {$if: 'cond', then: 1, else: 2}
output: 2

- title: $if-then in array, true
context: {cond: true}
template: [0, {$if: 'cond', then: 1}]
output: [0, 1]

- title: $if-then in array, false
context: {cond: false}
template: [0, {$if: 'cond', then: 1}]
output: [0] # missing else branch should return a delete-marker

- title: $if-then in object, true
context: {cond: true}
template: {key: {$if: 'cond', then: 1}, k2: 3}
output: {key: 1, k2: 3}

- title: $if-then in object, false
context: {cond: false}
template: {key: {$if: 'cond', then: 1}, k2: 3}
output: {k2: 3} # missing else branch should return a delete-marker

- title: $if -> delete-marker, true
context: {cond: true}
template: {key: {$if: 'cond'}, k2: 3}
output: {k2: 3} # missing then/else branches should return a delete-marker

- title: $if -> delete-marker, false
context: {cond: false}
template: {key: {$if: 'cond'}, k2: 3}
output: {k2: 3} # missing then/else branches should return a delete-marker

- title: $if->then, then => $eval, true
context: {key: {b: 1}}
template: {$if: 'true', then: {$eval: 'key'}}
output: {b: 1}

- title: $if->else, else => $eval, false
context: {key: {b: 1}}
template: {$if: 'false', else: {$eval: 'key'}}
output: {b: 1}

- title: $if->then, then => ${}, true
context: {key: 'one'}
template: {$if: 'true', then: '${key}'}
output: 'one'

- title: $if->else, else => ${}, false
context: {key: 'one'}
template: {$if: 'false', else: '${key}'}
output: 'one'

- title: $if->then, then => object, true
context: {cond: true}
template: {$if: 'cond', then: {key: 'hello world'}}
output: {key: 'hello world'}

- title: $if->else, else => object, false
context: {cond: false}
template: {$if: 'cond', else: {key: 'hello world'}}
output: {key: 'hello world'}

- title: $if->then, then => object, $eval, true
context: {cond: true, key: 'hello world'}
template: {$if: 'cond', then: {key: {$eval: 'key'}}}
output: {key: 'hello world'}

- title: $if->else, else => object, $eval, false
context: {cond: false, key: 'hello world'}
template: {$if: 'cond', else: {key: {$eval: 'key'}}}
output: {key: 'hello world'}

- title: $if->then, then => object, interpolation, true
context: {cond: true, key: 'world'}
template: {$if: 'cond', then: {key: 'hello ${key}'}}
output: {key: 'hello world'}

- title: $if->else, else => object, interpolation, false
context: {cond: false, key: 'world'}
template: {$if: 'cond', else: {key: 'hello ${key}'}}
output: {key: 'hello world'}

- title: $if->then->else, empty string
context: {cond: "", key: 'world'}
template: {$if: 'cond', then: "t", else: "f"}
output: f

- title: $if->then->else, nonempty string
context: {cond: "stuff", key: 'world'}
template: {$if: 'cond', then: "t", else: "f"}
output: t

- title: $if->then->else, string "0" # once upon a time, this was false in PHP.. maybe still is
context: {cond: "0", key: 'world'}
template: {$if: 'cond', then: "t", else: "f"}
output: t

- title: $if->then->else, zero
context: {cond: 0, key: 'world'}
template: {$if: 'cond', then: "t", else: "f"}
output: f

- title: $if->then->else, one
context: {cond: 1, key: 'world'}
template: {$if: 'cond', then: "t", else: "f"}
output: t

- title: $if->then->else, null
context: {cond: null, key: 'world'}
template: {$if: 'cond', then: "t", else: "f"}
output: f

- title: $if->then->else, empty array
context: {cond: [], key: 'world'}
template: {$if: 'cond', then: "t", else: "f"}
output: f

- title: $if->then->else, nonempty array
context: {cond: [1, 2], key: 'world'}
template: {$if: 'cond', then: "t", else: "f"}
output: t

- title: $if->then->else, empty object
context: {cond: {}, key: 'world'}
template: {$if: 'cond', then: "t", else: "f"}
output: f

- title: $if->then->else, nonempty object
context: {cond: {a: 2}, key: 'world'}
template: {$if: 'cond', then: "t", else: "f"}
output: t

#- title: $if->then->else, function
# context: {}
# template: {$if: 'min', then: "t", else: "f"}
# output: t

- title: $if->then evaluating to nothing at the top level is null
context: {cond: false}
template: {$if: 'cond', then: "t"}
output: null

- title: $if-then-else with undefined properties
context: {cond: true}
template: {$if: 'cond', then: 1, foo: 'bar', else: 2, bing: 'baz'}
error:
type: ParseError
message: 'value error: unknown field `bing`, expected one of `$if`, `then`, `else`'
19 changes: 10 additions & 9 deletions rimu/tests/spec/let.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
context: {}
error:
type: ParseError
message: 'value error: missing field `in`'
message: 'value error: unknown field `a`, expected `$let` or `in`'

- title: let array
template: {$let: [1, 2], in: {$eval: "1 + 2"}}
Expand Down Expand Up @@ -70,15 +70,16 @@
# template: {$let: {x: [1, 2, 3]}, in: [$reverse: {$eval: "x"}]}
# output: [[ 3, 2, 1 ]]

#- title: checking $let with $if without else
# context: {c: 'itm'}
# template: {$let: {a: {$if: 'c == "item"', then: 'value'}}, in: {a: '${a}'}}
# error: 'InterpreterError at template.a: unknown context value a'
- title: checking $let with $if without else
context: {c: 'itm'}
template: {$let: {a: {$if: 'c == "item"', then: 'value'}}, in: {a: '${a}'}}
error:
message: 'missing context: a'

#- title: let with a value of $if-then-else
# template: {$let: {$if: something == 3, then: {a: 10, b: 20}, else: {a: 20, b: 10}}, in: {$eval: 'a + b'}}
# context: {'something': 3}
# output: 30
- title: let with a value of $if-then-else
template: {$let: {$if: something == 3, then: {a: 10, b: 20}, else: {a: 20, b: 10}}, in: {$eval: 'a + b'}}
context: {'something': 3}
output: 30

- title: let with a rendered key
template: {$let: {"first_${name}": 1, "second_${name}": 2}, in: {$eval: "first_prize + second_prize"}}
Expand Down