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 1c2b322
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 52 deletions.
19 changes: 14 additions & 5 deletions interpreter/benches/runtime.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use cel_interpreter::context::Context;
use cel_interpreter::{Program, Value};
use cel_interpreter::{ExecutionError, 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,30 @@ 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()
.ok_or(ExecutionError::UndeclaredReference(var.to_string().into()))
});
} 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
172 changes: 143 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::{DynamicCollection, DynamicCollectionResolverFn, 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,158 @@ 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| match external_values.get(ident) {
Some(v) => Some(Value::String(v.clone().into())),
None => None,
});
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) -> DynamicCollectionResolverFn {
let receiver = self.clone();
Box::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) -> DynamicCollectionResolverFn {
let receiver = self.clone();
Box::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(DynamicCollection::new(
receiver.species.resolver(),
))),
_ => None,
},
_ => None,
})
}
}

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

impl Film {
fn resolver(&self) -> DynamicCollectionResolverFn {
let receiver = Arc::new(self.clone());
Box::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(DynamicCollection::new(
Box::new(move |member| {
let receiver = receiver.clone();
match member {
ResolvedMember::Index(Value::Int(idx)) => {
match receiver.characters.get(idx as usize) {
Some(v) => Some(Value::DynamicCollection(
DynamicCollection::new(v.resolver()),
)),
_ => None,
}
}
_ => 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(DynamicCollection::new(
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
50 changes: 43 additions & 7 deletions interpreter/src/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ use cel_parser::{ArithmeticOp, Atom, Expression, Member, RelationOp, UnaryOp};
use chrono::{DateTime, Duration, FixedOffset};
use core::ops;
use serde::{Serialize, Serializer};
use std::any::Any;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::fmt::{Display, Formatter};
use std::sync::Arc;

Expand Down Expand Up @@ -152,9 +152,39 @@ impl<T: Serialize> TryIntoValue for T {
}
}

#[derive(Clone, Debug)]
pub enum ResolvedMember {
Index(Value),
Attribute(Arc<String>),
}

pub type DynamicCollectionResolverFn = Box<dyn Fn(ResolvedMember) -> Option<Value> + Sync + Send>;

// Value's have to implement Debug but Fn's don't, so we
// create a DynamicCollection wrapper that we can implement Debug for.
#[derive(Clone)]
pub struct DynamicCollection {
pub(crate) resolver: Arc<DynamicCollectionResolverFn>,
}

impl DynamicCollection {
pub fn new(handler: DynamicCollectionResolverFn) -> Self {
DynamicCollection {
resolver: Arc::new(Box::new(handler)),
}
}
}

impl fmt::Debug for DynamicCollection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DynamicCollection").finish()
}
}

#[derive(Debug, Clone)]
pub enum Value {
Any(Arc<Box<dyn Any + Sync + Send>>),
// DynamicCollection is a special type that allows for dynamic resolution of members.
DynamicCollection(DynamicCollection),

List(Arc<Vec<Value>>),
Map(Map),
Expand Down Expand Up @@ -224,7 +254,7 @@ impl Value {
Value::Duration(_) => ValueType::Duration,
Value::Timestamp(_) => ValueType::Timestamp,
Value::Null => ValueType::Null,
Value::Any(_) => ValueType::Any,
Value::DynamicCollection(_) => ValueType::Any,
}
}

Expand Down Expand Up @@ -549,6 +579,11 @@ impl<'a> Value {
Member::Index(idx) => {
let idx = Value::resolve(idx, ctx)?;
match (self, idx) {
(Value::DynamicCollection(dc), idx) => {
(dc.resolver)(ResolvedMember::Index(idx))
.unwrap_or(Value::Null)
.into()
}
(Value::List(items), Value::Int(idx)) => {
items.get(idx as usize).unwrap().clone().into()
}
Expand Down Expand Up @@ -587,6 +622,9 @@ impl<'a> Value {
// This will always either be because we're trying to access
// a property on self, or a method on self.
let child = match self {
Value::DynamicCollection(ref dc) => {
(dc.resolver)(ResolvedMember::Attribute(name.clone()))
}
Value::Map(ref m) => m.map.get(&name.clone().into()).cloned(),
_ => None,
};
Expand All @@ -595,9 +633,7 @@ impl<'a> Value {
// give priority to the property. Maybe we can implement lookahead
// to see if the next token is a function call?
match (child.is_some(), ctx.has_function(&***name)) {
(false, false) => ctx
.get_dynamic_variable(Some(self), name.to_string())
.map_err(|_| NoSuchKey(name.clone())),
(false, false) => Err(NoSuchKey(name.clone())),
(true, true) | (true, false) => child.unwrap().into(),
(false, true) => Value::Function(name.clone(), Some(self.into())).into(),
}
Expand Down Expand Up @@ -643,7 +679,7 @@ impl<'a> Value {
Value::Duration(v) => v.num_nanoseconds().map(|n| n != 0).unwrap_or(false),
Value::Timestamp(v) => v.timestamp_nanos_opt().unwrap_or_default() > 0,
Value::Function(_, _) => false,
Value::Any(_) => false,
Value::DynamicCollection(_) => false,
}
}
}
Expand Down

0 comments on commit 1c2b322

Please sign in to comment.