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

Feat/serializer #33

Merged
merged 7 commits into from
Feb 17, 2024
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
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
Loading