Skip to content

Commit

Permalink
Feat/serializer (#33)
Browse files Browse the repository at this point in the history
* feat: Added a Serializer implementation for Values

* feat: Testing

* feat: Added exists and exists_one macros

* refactor: Map testing without CEL

* feat: Added tests for exist and exists_one macros

* refactor: Addressed Error handling and implemented TryIntoValue trait

* feat: Introducing add_variable_from_value
  • Loading branch information
lfbrehm authored Feb 17, 2024
1 parent 9246fb3 commit bff69f0
Show file tree
Hide file tree
Showing 11 changed files with 1,027 additions and 23 deletions.
7 changes: 6 additions & 1 deletion example/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ edition = "2021"
[dependencies]
cel-interpreter = { path = "../interpreter" }
chrono = "0.4.26"
serde = { version = "1.0.196", features = ["derive"] }

[[bin]]
name = "simple"
Expand All @@ -23,4 +24,8 @@ path = "src/functions.rs"

[[bin]]
name = "threads"
path = "src/threads.rs"
path = "src/threads.rs"

[[bin]]
name = "serde"
path = "src/serde.rs"
24 changes: 24 additions & 0 deletions example/src/serde.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use cel_interpreter::{to_value, Context, Program};
use serde::Serialize;

// An example struct that derives Serialize
#[derive(Serialize)]
struct MyStruct {
a: i32,
b: i32,
}

fn main() {
let program = Program::compile("foo.a == foo.b").unwrap();
let mut context = Context::default();

// MyStruct will be implicitly serialized into the CEL appropriate types
context
.add_variable("foo", MyStruct { a: 1, b: 1 })
.unwrap();
// To explicitly serialize structs use to_value()
let _cel_value = to_value(MyStruct { a: 2, b: 2 }).unwrap();

let value = program.execute(&context).unwrap();
assert_eq!(value, true.into());
}
8 changes: 4 additions & 4 deletions example/src/threads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ fn main() {
scope(|scope| {
scope.spawn(|| {
let mut context = Context::default();
context.add_variable("a", 1);
context.add_variable("b", 2);
context.add_variable("a", 1).unwrap();
context.add_variable("b", 2).unwrap();
let value = program.execute(&context).unwrap();
assert_eq!(value, 3.into());
});
scope.spawn(|| {
let mut context = Context::default();
context.add_variable("a", 2);
context.add_variable("b", 4);
context.add_variable("a", 2).unwrap();
context.add_variable("b", 4).unwrap();
let value = program.execute(&context).unwrap();
assert_eq!(value, 6.into());
});
Expand Down
2 changes: 1 addition & 1 deletion example/src/variables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use cel_interpreter::{Context, Program};
fn main() {
let program = Program::compile("foo * 2").unwrap();
let mut context = Context::default();
context.add_variable("foo", 10);
context.add_variable("foo", 10).unwrap();

let value = program.execute(&context).unwrap();
assert_eq!(value, 20.into());
Expand Down
2 changes: 2 additions & 0 deletions interpreter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ thiserror = "1.0.40"
chrono = "0.4.26"
nom = "7.1.3"
paste = "1.0.14"
serde = "1.0.196"

[dev-dependencies]
criterion = { version = "0.5.1", features = ["html_reports"] }
serde_bytes = "0.11.14"

[[bench]]
name = "runtime"
Expand Down
4 changes: 2 additions & 2 deletions interpreter/benches/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
c.bench_function(name, |b| {
let program = Program::compile(expr).expect("Parsing failed");
let mut ctx = Context::default();
ctx.add_variable("foo", HashMap::from([("bar", 1)]));
ctx.add_variable_from_value("foo", HashMap::from([("bar", 1)]));
b.iter(|| program.execute(&ctx))
});
}
Expand All @@ -59,7 +59,7 @@ pub fn map_macro_benchmark(c: &mut Criterion) {
let list = (0..size).collect::<Vec<_>>();
let program = Program::compile("list.map(x, x * 2)").unwrap();
let mut ctx = Context::default();
ctx.add_variable("list", list);
ctx.add_variable_from_value("list", list);
b.iter(|| program.execute(&ctx).unwrap())
});
}
Expand Down
26 changes: 24 additions & 2 deletions interpreter/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::magic::{Function, FunctionRegistry, Handler};
use crate::objects::Value;
use crate::objects::{TryIntoValue, Value};
use crate::{functions, ExecutionError};
use cel_parser::Expression;
use std::collections::HashMap;
Expand Down Expand Up @@ -40,7 +40,27 @@ pub enum Context<'a> {
}

impl<'a> Context<'a> {
pub fn add_variable<S, V>(&mut self, name: S, value: V)
pub fn add_variable<S, V>(
&mut self,
name: S,
value: V,
) -> Result<(), Box<dyn std::error::Error>>
where
S: Into<String>,
V: TryIntoValue,
{
match self {
Context::Root { variables, .. } => {
variables.insert(name.into(), value.try_into_value()?);
}
Context::Child { variables, .. } => {
variables.insert(name.into(), value.try_into_value()?);
}
}
Ok(())
}

pub fn add_variable_from_value<S, V>(&mut self, name: S, value: V)
where
S: Into<String>,
V: Into<Value>,
Expand Down Expand Up @@ -138,6 +158,8 @@ impl<'a> Default for Context<'a> {
ctx.add_function("timestamp", functions::timestamp);
ctx.add_function("string", functions::string);
ctx.add_function("double", functions::double);
ctx.add_function("exists", functions::exists);
ctx.add_function("exists_one", functions::exists_one);
ctx
}
}
128 changes: 123 additions & 5 deletions interpreter/src/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ pub fn map(
let mut values = Vec::with_capacity(items.len());
let mut ptx = ftx.ptx.clone();
for item in items.iter() {
ptx.add_variable(ident.clone(), item.clone());
ptx.add_variable_from_value(ident.clone(), item.clone());
let value = ptx.resolve(&expr)?;
values.push(value);
}
Expand Down Expand Up @@ -255,7 +255,7 @@ pub fn filter(
let mut values = Vec::with_capacity(items.len());
let mut ptx = ftx.ptx.clone();
for item in items.iter() {
ptx.add_variable(ident.clone(), item.clone());
ptx.add_variable_from_value(ident.clone(), item.clone());
if let Value::Bool(true) = ptx.resolve(&expr)? {
values.push(item.clone());
}
Expand Down Expand Up @@ -289,7 +289,7 @@ pub fn all(
Value::List(items) => {
let mut ptx = ftx.ptx.clone();
for item in items.iter() {
ptx.add_variable(&ident, item);
ptx.add_variable_from_value(&ident, item);
if let Value::Bool(false) = ptx.resolve(&expr)? {
return Ok(false);
}
Expand All @@ -299,7 +299,7 @@ pub fn all(
Value::Map(value) => {
let mut ptx = ftx.ptx.clone();
for key in value.map.keys() {
ptx.add_variable(&ident, key);
ptx.add_variable_from_value(&ident, key);
if let Value::Bool(false) = ptx.resolve(&expr)? {
return Ok(false);
}
Expand All @@ -310,6 +310,101 @@ pub fn all(
};
}

/// Returns a boolean value indicating whether a or more values in the provided
/// list or map meet the predicate defined by the provided expression. If
/// called on a map, the predicate is applied to the map keys.
///
/// This function is intended to be used like the CEL-go `exists` macro:
/// https://github.com/google/cel-spec/blob/master/doc/langdef.md#macros
///
/// # Example
/// ```cel
/// [1, 2, 3].exists(x, x > 0) == true
/// [{1:true, 2:true, 3:false}].exists(x, x > 0) == true
/// ```
pub fn exists(
ftx: &FunctionContext,
This(this): This<Value>,
ident: Identifier,
expr: Expression,
) -> Result<bool> {
match this {
Value::List(items) => {
let mut ptx = ftx.ptx.clone();
for item in items.iter() {
ptx.add_variable_from_value(&ident, item);
if let Value::Bool(true) = ptx.resolve(&expr)? {
return Ok(true);
}
}
Ok(false)
}
Value::Map(value) => {
let mut ptx = ftx.ptx.clone();
for key in value.map.keys() {
ptx.add_variable_from_value(&ident, key);
if let Value::Bool(true) = ptx.resolve(&expr)? {
return Ok(true);
}
}
Ok(false)
}
_ => Err(this.error_expected_type(ValueType::List)),
}
}

/// Returns a boolean value indicating whether only one value in the provided
/// list or map meets the predicate defined by the provided expression. If
/// called on a map, the predicate is applied to the map keys.
///
/// This function is intended to be used like the CEL-go `exists` macro:
/// https://github.com/google/cel-spec/blob/master/doc/langdef.md#macros
///
/// # Example
/// ```cel
/// [1, 2, 3].exists_one(x, x > 0) == false
/// [1, 2, 3].exists_one(x, x == 1) == true
/// [{1:true, 2:true, 3:false}].exists_one(x, x > 0) == false
/// ```
pub fn exists_one(
ftx: &FunctionContext,
This(this): This<Value>,
ident: Identifier,
expr: Expression,
) -> Result<bool> {
match this {
Value::List(items) => {
let mut ptx = ftx.ptx.clone();
let mut exists = false;
for item in items.iter() {
ptx.add_variable_from_value(&ident, item);
if let Value::Bool(true) = ptx.resolve(&expr)? {
if exists {
return Ok(false);
}
exists = true;
}
}
Ok(exists)
}
Value::Map(value) => {
let mut ptx = ftx.ptx.clone();
let mut exists = false;
for key in value.map.keys() {
ptx.add_variable_from_value(&ident, key);
if let Value::Bool(true) = ptx.resolve(&expr)? {
if exists {
return Ok(false);
}
exists = true;
}
}
Ok(exists)
}
_ => Err(this.error_expected_type(ValueType::List)),
}
}

/// Duration parses the provided argument into a [`Value::Duration`] value.
/// The argument must be string, and must be in the format of a duration. See
/// the [`parse_duration`] documentation for more information on the supported
Expand Down Expand Up @@ -394,7 +489,7 @@ mod tests {

for (name, script) in tests {
let mut ctx = Context::default();
ctx.add_variable("foo", HashMap::from([("bar", 1)]));
ctx.add_variable_from_value("foo", HashMap::from([("bar", 1)]));
assert_eq!(test_script(script, Some(ctx)), Ok(true.into()), "{}", name);
}
}
Expand Down Expand Up @@ -431,6 +526,29 @@ mod tests {
.for_each(assert_script);
}

#[test]
fn test_exists() {
[
("exist list #1", "[0, 1, 2].exists(x, x > 0)"),
("exist list #2", "[0, 1, 2].exists(x, x == 3) == false"),
("exist list #3", "[0, 1, 2, 2].exists(x, x == 2)"),
("exist map", "{0: 0, 1:1, 2:2}.exists(x, x > 0)"),
]
.iter()
.for_each(assert_script);
}

#[test]
fn test_exists_one() {
[
("exist list #1", "[0, 1, 2].exists_one(x, x > 0) == false"),
("exist list #2", "[0, 1, 2].exists_one(x, x == 0)"),
("exist map", "{0: 0, 1:1, 2:2}.exists_one(x, x == 2)"),
]
.iter()
.for_each(assert_script);
}

#[test]
fn test_max() {
[
Expand Down
10 changes: 6 additions & 4 deletions interpreter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ mod functions;
mod magic;
pub mod objects;
mod resolvers;
mod ser;
pub use ser::to_value;
mod testing;
use magic::FromContext;

Expand Down Expand Up @@ -154,9 +156,9 @@ mod tests {
fn variables() {
fn assert_output(script: &str, expected: ResolveResult) {
let mut ctx = Context::default();
ctx.add_variable("foo", HashMap::from([("bar", 1i64)]));
ctx.add_variable("arr", vec![1i64, 2, 3]);
ctx.add_variable("str", "foobar".to_string());
ctx.add_variable_from_value("foo", HashMap::from([("bar", 1i64)]));
ctx.add_variable_from_value("arr", vec![1i64, 2, 3]);
ctx.add_variable_from_value("str", "foobar".to_string());
assert_eq!(test_script(script, Some(ctx)), expected);
}

Expand Down Expand Up @@ -216,7 +218,7 @@ mod tests {

for (name, script, error) in tests {
let mut ctx = Context::default();
ctx.add_variable("foo", HashMap::from([("bar", 1)]));
ctx.add_variable_from_value("foo", HashMap::from([("bar", 1)]));
let res = test_script(script, Some(ctx));
assert_eq!(res, error.into(), "{}", name);
}
Expand Down
Loading

0 comments on commit bff69f0

Please sign in to comment.