Skip to content

Commit

Permalink
add If operation (#8)
Browse files Browse the repository at this point in the history
also

- add `value.is_truthy()`
- add `#[serde(deny_unknown_fields)]` to Operation's
  • Loading branch information
ahdinosaur committed Aug 18, 2023
1 parent 5926af4 commit 964177c
Show file tree
Hide file tree
Showing 9 changed files with 300 additions and 10 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,30 @@ foo:
```
#### If
Template:
```yaml
k1:
$if: 'cond'
then: 1
else: 2
k2: 3
```
Context:
```yaml
cond: true
```
Output:
```yaml
k1: 1
k2: 3
```
#### Match
#### Switch
#### Map
Expand Down
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

0 comments on commit 964177c

Please sign in to comment.