Skip to content

Commit

Permalink
Converted the Value::Any to Value::DynamicCollection
Browse files Browse the repository at this point in the history
This simplifies dynamically resolving deep data structures using a chain 
of closure resolver functions.

Signed-off-by: Hiram Chirino <[email protected]>
  • Loading branch information
chirino committed Jul 5, 2024
1 parent 1771ec4 commit 60cbf7d
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 53 deletions.
13 changes: 9 additions & 4 deletions interpreter/benches/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use cel_interpreter::context::Context;
use cel_interpreter::{Program, Value};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use std::collections::HashMap;
use std::sync::Arc;

pub fn criterion_benchmark(c: &mut Criterion) {
let expressions = vec![
Expand Down Expand Up @@ -75,22 +76,26 @@ pub fn variable_resolution_benchmark(c: &mut Criterion) {

for size in sizes {
let mut expr = String::new();

let mut doc = HashMap::new();
for i in 0..size {
doc.insert(format!("var_{i}", i = i), Value::Null);
expr.push_str(&format!("var_{i}", i = i));
if i < size - 1 {
expr.push_str("||");
}
}

let doc = Arc::new(doc);
let program = Program::compile(&expr).unwrap();
group.bench_function(format!("variable_resolution_{}", size).as_str(), |b| {
let mut ctx = Context::default();
if use_dynamic_resolver {
ctx.set_dynamic_resolver(move |_, _| Ok(Value::Null));
let doc = doc.clone();
ctx.set_dynamic_resolver(move |var| doc.get(var).cloned());
} else {
for i in 0..size {
ctx.add_variable_from_value(&format!("var_{i}", i = i), Value::Null);
}
doc.iter()
.for_each(|(k, v)| ctx.add_variable_from_value(k.to_string(), v.clone()));
}
b.iter(|| program.execute(&ctx).unwrap())
});
Expand Down
18 changes: 7 additions & 11 deletions interpreter/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ pub enum Context<'a> {
},
}

type DynamicResolverFn =
Box<dyn Fn(Option<Value>, &str) -> Result<Value, ExecutionError> + Sync + Send>;
type DynamicResolverFn = Box<dyn Fn(&str) -> Option<Value> + Sync + Send>;

impl<'a> Context<'a> {
pub fn add_variable<S, V>(
Expand Down Expand Up @@ -89,25 +88,22 @@ impl<'a> Context<'a> {
.get(&name)
.cloned()
.or_else(|| parent.get_variable(&name).ok())
.map_or_else(|| self.get_dynamic_variable(None, name), Ok),
.map_or_else(|| self.get_dynamic_variable(name), Ok),
Context::Root { variables, .. } => variables
.get(&name)
.cloned()
.map_or_else(|| self.get_dynamic_variable(None, name), Ok),
.map_or_else(|| self.get_dynamic_variable(name), Ok),
}
}

pub fn get_dynamic_variable<S>(
&self,
this: Option<Value>,
name: S,
) -> Result<Value, ExecutionError>
pub fn get_dynamic_variable<S>(&self, name: S) -> Result<Value, ExecutionError>
where
S: Into<String>,
{
let name = name.into();
return if let Some(dynamic_resolver) = self.get_dynamic_resolver() {
(dynamic_resolver)(this.clone(), name.as_str())
(dynamic_resolver)(name.as_str())
.ok_or_else(|| ExecutionError::UndeclaredReference(name.into()))
} else {
Err(ExecutionError::UndeclaredReference(name.into()))
};
Expand Down Expand Up @@ -146,7 +142,7 @@ impl<'a> Context<'a> {

pub fn set_dynamic_resolver<F>(&mut self, handler: F)
where
F: Fn(Option<Value>, &str) -> Result<Value, ExecutionError> + Sync + Send + 'static,
F: Fn(&str) -> Option<Value> + Sync + Send + 'static,
{
if let Context::Root {
dynamic_resolver, ..
Expand Down
165 changes: 136 additions & 29 deletions interpreter/src/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -547,8 +547,9 @@ mod tests {
use std::sync::Arc;

use crate::context::Context;
use crate::objects::{MemberResolver, ResolvedMember};
use crate::testing::test_script;
use crate::{ExecutionError, Value};
use crate::Value;
use std::collections::HashMap;

fn assert_script(input: &(&str, &str)) {
Expand Down Expand Up @@ -780,45 +781,151 @@ mod tests {
let external_values = Arc::new(HashMap::from([("hello".to_string(), "world".to_string())]));

let mut ctx = Context::default();
ctx.set_dynamic_resolver(move |_, ident| match external_values.get(ident) {
Some(v) => Ok(Value::String(v.clone().into())),
None => Err(ExecutionError::UndeclaredReference(
ident.to_string().into(),
)),
ctx.set_dynamic_resolver(move |ident| {
external_values
.get(ident)
.map(|v| Value::String(v.clone().into()))
});
assert_eq!(test_script("hello == 'world'", Some(ctx)), Ok(true.into()));
}

#[derive(Debug, Clone)]
struct Path(String);

#[test]
fn test_deep_dynamic_resolver() {
// You can resolve dynamic values by providing a custom resolver function.
let external_values = Arc::new(HashMap::from([(
"foo.bar.happy".to_string(),
"hour".to_string(),
)]));
#[derive(Clone)]
struct Species {
name: String,
language: Option<String>,
homeworld: Option<String>,
}

let mut ctx = Context::default();
ctx.set_dynamic_resolver(move |this, identifier| {
let name: String = identifier.to_string();
match this {
Some(Value::Any(value_any)) => match value_any.downcast_ref::<Path>() {
Some(Path(path)) => {
let path = format!("{}.{}", path, name);
match external_values.get(path.as_str()) {
Some(v) => Ok(Value::String(v.clone().into())),
None => Ok(Value::Any(Arc::new(Box::new(Path(path))))),
}
impl Species {
fn resolver(&self) -> MemberResolver {
let receiver = self.clone();
MemberResolver::new(move |name| match name {
ResolvedMember::Attribute(name) => match name.as_str() {
"name" => Some(Value::String(receiver.name.clone().into())),
"language" => match receiver.language.clone() {
Some(v) => Some(Value::String(v.clone().into())),
None => Some(Value::Null),
},
"homeworld" => match receiver.homeworld.clone() {
Some(v) => Some(Value::String(v.clone().into())),
None => Some(Value::Null),
},
_ => None,
},
_ => None,
})
}
}

#[derive(Clone)]
struct Character {
name: String,
gender: Option<String>,
species: Species,
}

impl Character {
fn resolver(&self) -> MemberResolver {
let receiver = self.clone();
MemberResolver::new(move |member| match member {
ResolvedMember::Attribute(name) => match name.as_str() {
"name" => Some(Value::String(receiver.name.clone().into())),
"gender" => match receiver.gender.clone() {
Some(v) => Some(Value::String(v.clone().into())),
None => Some(Value::Null),
},
"species" => Some(Value::DynamicCollection(receiver.species.resolver())),
_ => None,
},
_ => None,
})
}
}

#[derive(Clone)]
struct Film {
director: String,
title: String,
characters: Vec<Character>,
}

impl Film {
fn resolver(&self) -> MemberResolver {
let receiver = Arc::new(self.clone());
MemberResolver::new(move |member| {
let receiver = receiver.clone();
match member {
ResolvedMember::Attribute(name) => match name.as_str() {
"director" => Some(Value::String(receiver.director.clone().into())),
"title" => Some(Value::String(receiver.title.clone().into())),
"characters" => Some(Value::DynamicCollection(MemberResolver::new(
move |member| {
let receiver = receiver.clone();
match member {
ResolvedMember::Index(Value::Int(idx)) => receiver
.characters
.get(idx as usize)
.map(|v| Value::DynamicCollection(v.resolver())),
_ => None,
}
},
))),
_ => None,
},
_ => None,
}
None => Err(ExecutionError::UndeclaredReference(name.into())),
},
_ => Ok(Value::Any(Arc::new(Box::new(Path(name))))),
})
}
}

let doc = Film {
director: "George Lucas".to_string(),
title: "A New Hope".to_string(),
characters: vec![
Character {
name: "Luke Skywalker".to_string(),
gender: Some("male".to_string()),
species: Species {
name: "Human".to_string(),
language: Some("english".to_string()),
homeworld: Some("Earth".to_string()),
},
},
Character {
name: "C-3PO".to_string(),
gender: None,
species: Species {
name: "Droid".to_string(),
language: None,
homeworld: None,
},
},
Character {
name: "Chewbacca".to_string(),
gender: Some("male".to_string()),
species: Species {
name: "Wookie".to_string(),
language: None,
homeworld: Some("Kashyyyk".to_string()),
},
},
],
};

let mut ctx = Context::default();
ctx.set_dynamic_resolver(move |name| match name {
"film" => Some(Value::DynamicCollection(doc.clone().resolver())),
_ => None,
});
assert_eq!(
test_script("foo.bar.happy == 'hour'", Some(ctx)),
test_script(
"film.title == 'A New Hope' && \
film.characters[0].name =='Luke Skywalker' && \
film.characters[0].species.name == 'Human'",
Some(ctx)
),
Ok(true.into())
);
}
Expand Down
Loading

0 comments on commit 60cbf7d

Please sign in to comment.