diff --git a/Cargo.toml b/Cargo.toml index f4d9c9b..66b5daa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cel-eval" -version = "0.1.4" +version = "0.1.6" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.htmlž diff --git a/README.md b/README.md index 66bb158..f93af73 100644 --- a/README.md +++ b/README.md @@ -81,9 +81,20 @@ The JSON is required to be in shape of `ExecutionContext`, which is defined as: ] }, // Functions for our platform object - signature will be changed soon to allow for args - "platform" : { - "functionName" : "fallbackValue" + "computed" : { + "functionName": [{ // List of args + "type": "string", + "value": "event_name" + }] + }, + // Functions for our device object - signature will be changed soon to allow for args + "device" : { + "functionName": [{ // List of args + "type": "string", + "value": "event_name" + }] } + }}, // The expression to evaluate "expression": "foo == 100" diff --git a/src/ast.rs b/src/ast.rs index 9cd84c7..da0a665 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -9,7 +9,8 @@ use std::sync::Arc; pub(crate) struct ASTExecutionContext { pub(crate) variables: PassableMap, pub(crate) expression: JSONExpression, - pub(crate) platform: Option>>, + pub(crate) computed: Option>>, + pub(crate) device: Option>> } #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] diff --git a/src/cel.udl b/src/cel.udl index 1e88eea..b5b3373 100644 --- a/src/cel.udl +++ b/src/cel.udl @@ -2,10 +2,14 @@ interface HostContext { [Async] string computed_property(string name, string args); + [Async] + string device_property(string name, string args); + }; namespace cel { string evaluate_with_context(string definition, HostContext context); string evaluate_ast_with_context(string definition, HostContext context); string evaluate_ast(string ast); + string parse_to_ast(string expression); }; diff --git a/src/lib.rs b/src/lib.rs index 5fc6ee2..0a68071 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,31 +18,34 @@ use std::fmt::Debug; use std::ops::Deref; use std::sync::{Arc, mpsc, Mutex}; use std::thread::spawn; +use cel_parser::parse; #[cfg(target_arch = "wasm32")] use wasm_bindgen_futures::spawn_local; #[cfg(not(target_arch = "wasm32"))] use futures_lite::future::block_on; + + /** * Host context trait that defines the methods that the host context should implement, * i.e. iOS or Android calling code. This trait is used to resolve dynamic properties in the - * CEL expression during evaluation, such as `platform.daysSinceEvent("event_name")` or similar. + * CEL expression during evaluation, such as `computed.daysSinceEvent("event_name")` or similar. + * Note: Since WASM async support in the browser is still not fully mature, we're using the + * target_arch cfg to define the trait methods differently for WASM and non-WASM targets. */ - -#[async_trait] -pub trait AsyncHostContext: Send + Sync { - async fn computed_property(&self, name: String, args: String) -> String; -} - #[cfg(target_arch = "wasm32")] pub trait HostContext: Send + Sync { fn computed_property(&self, name: String, args: String) -> String; + + fn device_property(&self, name: String, args: String) -> String; } #[cfg(not(target_arch = "wasm32"))] #[async_trait] pub trait HostContext: Send + Sync { async fn computed_property(&self, name: String, args: String) -> String; + + async fn device_property(&self, name: String, args: String) -> String; } /** @@ -57,7 +60,8 @@ pub fn evaluate_ast_with_context(definition: String, host: Arc) execute_with( AST(data.expression.into()), data.variables, - data.platform, + data.computed, + data.device, host, ) } @@ -94,14 +98,27 @@ pub fn evaluate_with_context(definition: String, host: Arc) -> execute_with( CompiledProgram(compiled), data.variables, - data.platform, + data.computed, + data.device, host, ) } /** - Type of expression to be executed, either a compiled program or an AST. -*/ + * Transforms a given CEL expression into a CEL AST, serialized as JSON. + * @param expression The CEL expression to parse + * @return The AST of the expression, serialized as JSON + */ +pub fn parse_to_ast(expression: String) -> String { + let ast : JSONExpression = parse(expression.as_str()).expect( + format!("Failed to parse expression: {}", expression).as_str() + ).into(); + serde_json::to_string(&ast).expect("Failed to serialize AST into JSON") +} + +/** +Type of expression to be executed, either a compiled program or an AST. + */ enum ExecutableType { AST(Expression), CompiledProgram(Program), @@ -117,7 +134,8 @@ enum ExecutableType { fn execute_with( executable: ExecutableType, variables: PassableMap, - platform: Option>>, + computed: Option>>, + device: Option>>, host: Arc, ) -> String { let host = host.clone(); @@ -136,8 +154,14 @@ fn execute_with( // This function is used to extract the value of a property from the host context // As UniFFi doesn't support recursive enums yet, we have to pass it in as a // JSON serialized string of a PassableValue from Host and deserialize it here + + enum PropType { + Computed, + Device, + } #[cfg(not(target_arch = "wasm32"))] fn prop_for( + prop_type: PropType, name: Arc, args: Option>, ctx: &Arc, @@ -146,11 +170,16 @@ fn execute_with( let val = futures_lite::future::block_on(async move { let ctx = ctx.clone(); - ctx.computed_property( - name.clone().to_string(), - serde_json::to_string(&args).expect("Failed to serialize args for computed property"), - ) - .await + match prop_type { + PropType::Computed => ctx.computed_property( + name.clone().to_string(), + serde_json::to_string(&args).expect("Failed to serialize args for computed property"), + ).await, + PropType::Device => ctx.device_property( + name.clone().to_string(), + serde_json::to_string(&args).expect("Failed to serialize args for computed property"), + ).await, + } }); // Deserialize the value let passable: Option = serde_json::from_str(val.as_str()).unwrap_or(Some(PassableValue::Null)); @@ -160,27 +189,53 @@ fn execute_with( #[cfg(target_arch = "wasm32")] fn prop_for( + prop_type: PropType, name: Arc, args: Option>, ctx: &Arc, ) -> Option { let ctx = ctx.clone(); - let val = ctx.computed_property( + let val = match prop_type { + PropType::Computed => ctx.computed_property( name.clone().to_string(), serde_json::to_string(&args).expect("Failed to serialize args for computed property"), - ); + ), + PropType::Device => ctx.device_property( + name.clone().to_string(), + serde_json::to_string(&args).expect("Failed to serialize args for computed property"), + ), + }; // Deserialize the value let passable: Option = serde_json::from_str(val.as_str()).unwrap_or(Some(PassableValue::Null)); passable } + let computed = computed.unwrap_or(HashMap::new()).clone(); - let platform = platform.unwrap_or(HashMap::new()).clone(); + // Create computed properties as a map of keys and function names + let computed_host_properties: HashMap = computed + .iter() + .map(|it| { + let args = it.1.clone(); + let args = if args.is_empty() { + None + } else { + Some(Box::new(PassableValue::List(args))) + }; + let name = it.0.clone(); + ( + Key::String(Arc::new(name.clone())), + Function(name, args).to_cel(), + ) + }) + .collect(); - // Create platform properties as a map of keys and function names - let platform_properties: HashMap = platform + let device = device.unwrap_or(HashMap::new()).clone(); + + // Create device properties as a map of keys and function names + let device_host_properties: HashMap = device .iter() .map(|it| { let args = it.1.clone(); @@ -197,28 +252,42 @@ fn execute_with( }) .collect(); - // Add the map to the platform object + + // Add the map to the `computed` object ctx.add_variable( - "platform", + "computed", Value::Map(Map { - map: Arc::new(platform_properties), + map: Arc::new(computed_host_properties), }), ) - .unwrap(); + .unwrap(); + + let binding = device.clone(); + // Combine the device and computed properties + let host_properties = binding + .iter() + .chain(computed.iter()) + .map(|(k, v)| (k.clone(), v.clone())) + .into_iter(); + let mut device_properties_clone = device.clone().clone(); // Add those functions to the context - for it in platform.iter() { + for it in host_properties { + let mut value = device_properties_clone.clone(); let key = it.0.clone(); let host_clone = Arc::clone(&host); // Clone the Arc to pass into the closure let key_str = key.clone(); // Clone key for usage in the closure ctx.add_function( key_str.as_str(), move |ftx: &FunctionContext| -> Result { + let device = value.clone(); let fx = ftx.clone(); let name = fx.name.clone(); // Move the name into the closure let args = fx.args.clone(); // Clone the arguments let host = host_clone.lock().unwrap(); // Lock the host for safe access prop_for( + if device.contains_key(&it.0) + { PropType::Device } else { PropType::Computed }, name.clone(), Some( args.iter() @@ -229,9 +298,9 @@ fn execute_with( ), &*host, ) - .map_or(Err(ExecutionError::UndeclaredReference(name)), |v| { - Ok(v.to_cel()) - }) + .map_or(Err(ExecutionError::UndeclaredReference(name)), |v| { + Ok(v.to_cel()) + }) }, ); } @@ -310,9 +379,10 @@ impl fmt::Display for DisplayableValue { } } } + impl fmt::Display for DisplayableError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f,"{}",self.0.to_string().as_str()) + write!(f, "{}", self.0.to_string().as_str()) } } @@ -329,6 +399,10 @@ mod tests { async fn computed_property(&self, name: String, args: String) -> String { self.map.get(&name).unwrap().to_string() } + + async fn device_property(&self, name: String, args: String) -> String { + self.map.get(&name).unwrap().to_string() + } } #[tokio::test] @@ -347,7 +421,7 @@ mod tests { } "# - .to_string(), + .to_string(), ctx, ); assert_eq!(res, "true"); @@ -370,7 +444,7 @@ mod tests { } "# - .to_string(), + .to_string(), ctx, ); assert_eq!(res, "true"); @@ -393,10 +467,10 @@ mod tests { } "# - .to_string(), + .to_string(), ctx, ); - assert_eq!(res, "UndeclaredReference"); + assert_eq!(res, "Undeclared reference to 'test_custom_func'"); } #[test] @@ -423,7 +497,7 @@ mod tests { } "# - .to_string(), + .to_string(), ctx, ); assert_eq!(res, "true"); @@ -458,7 +532,7 @@ mod tests { } "# - .to_string(), + .to_string(), ctx, ); println!("{}", res); @@ -495,19 +569,46 @@ mod tests { } } }, - "platform" : { + "computed" : { "daysSinceEvent": [{ "type": "string", "value": "event_name" }] }, - "expression": "platform.daysSinceEvent(\"test\") == user.some_value" + "device" : { + "timeSinceEvent": [{ + "type": "string", + "value": "event_name" + }] + }, + "expression": "computed.daysSinceEvent(\"test\") == user.some_value" } "# - .to_string(), + .to_string(), ctx, ); println!("{}", res); assert_eq!(res, "true"); } + + + #[test] + fn test_parse_to_ast() { + let expression = "device.daysSince(app_install) == 3"; + let ast_json = parse_to_ast(expression.to_string()); + println!("\nSerialized AST:"); + println!("{}", ast_json); + // Deserialize back to JSONExpression + let deserialized_json_expr: JSONExpression = serde_json::from_str(&ast_json).unwrap(); + + // Convert back to original Expression + let deserialized_expr: Expression = deserialized_json_expr.into(); + + println!("\nDeserialized Expression:"); + println!("{:?}", deserialized_expr); + + let parsed_expression = parse(expression).unwrap(); + assert_eq!(parsed_expression, deserialized_expr); + println!("\nOriginal and deserialized expressions are equal!"); + } } diff --git a/src/models.md b/src/models.md index c1708eb..1ab6377 100644 --- a/src/models.md +++ b/src/models.md @@ -21,7 +21,7 @@ An example of `ExecutionContext` JSON for convenience: } } }, - "platform" : { + "computed" : { "daysSinceEvent": [{ "type": "string", "value": "event_name" diff --git a/src/models.rs b/src/models.rs index bebb923..78919f8 100644 --- a/src/models.rs +++ b/src/models.rs @@ -9,7 +9,8 @@ use std::sync::Arc; pub(crate) struct ExecutionContext { pub(crate) variables: PassableMap, pub(crate) expression: String, - pub(crate) platform: Option>>, + pub(crate) computed: Option>>, + pub(crate) device: Option>> } #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index cae1269..c4990d6 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -25,6 +25,10 @@ extern "C" { #[wasm_bindgen(method, catch)] fn computed_property(this: &JsHostContext, name: String, args: String) -> Result; + + #[wasm_bindgen(method, catch)] + fn device_property(this: &JsHostContext, name: String, args: String) -> Result; + } /** @@ -67,6 +71,17 @@ impl HostContext for HostContextAdapter { .expect( format!("Could not deserialize the result from computed property - Is some: {}", result.is_some()).as_str()) } + + fn device_property(&self, name: String, args: String) -> String { + let context = Arc::clone(&self.context); + let promise = context.device_property(name.clone(), args.clone()); + let result = promise.expect("Did not receive the proper result from computed").as_string(); + result + .clone() + .expect( + format!("Could not deserialize the result from computed property - Is some: {}", result.is_some()).as_str()) + } + } unsafe impl Send for HostContextAdapter {} @@ -91,6 +106,11 @@ pub async fn evaluate_ast(ast: String) -> Result { Ok(cel_eval::evaluate_ast(ast)) } +#[wasm_bindgen] +pub async fn parse_into_ast(expression: String) -> Result { + Ok(cel_eval::parse_into_ast(expression)) +} + #[cfg(test)] mod tests { #[test]