From 9bf8fcbb6d6e9df4677c7413b0369c8bd961c01c Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Thu, 19 Sep 2024 20:21:57 +0200 Subject: [PATCH 1/4] Add support for device and computed in CEL instead of platform, add new callback function to host --- Cargo.toml | 2 +- README.md | 15 ++++++- src/ast.rs | 3 +- src/cel.udl | 3 ++ src/lib.rs | 114 +++++++++++++++++++++++++++++++++++++++--------- src/models.md | 2 +- src/models.rs | 3 +- wasm/src/lib.rs | 15 +++++++ 8 files changed, 130 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f4d9c9b..7b6b5c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cel-eval" -version = "0.1.4" +version = "0.1.5" 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..a7eb5f0 100644 --- a/src/cel.udl +++ b/src/cel.udl @@ -2,6 +2,9 @@ interface HostContext { [Async] string computed_property(string name, string args); + [Async] + string device_property(string name, string args); + }; namespace cel { diff --git a/src/lib.rs b/src/lib.rs index 5fc6ee2..8e6bfaa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,12 +26,13 @@ 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. */ #[async_trait] pub trait AsyncHostContext: Send + Sync { async fn computed_property(&self, name: String, args: String) -> String; + } #[cfg(target_arch = "wasm32")] @@ -43,6 +44,9 @@ pub trait HostContext: Send + Sync { #[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 +61,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,7 +99,8 @@ pub fn evaluate_with_context(definition: String, host: Arc) -> execute_with( CompiledProgram(compiled), data.variables, - data.platform, + data.computed, + data.device, host, ) } @@ -117,7 +123,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 +143,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 +159,17 @@ 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,16 +179,23 @@ 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"), - ); + ).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)); @@ -177,10 +203,30 @@ fn execute_with( } - let platform = platform.unwrap_or(HashMap::new()).clone(); + let computed = computed.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(); + + let device = device.unwrap_or(HashMap::new()).clone(); - // Create platform properties as a map of keys and function names - let platform_properties: HashMap = platform + // 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 +243,44 @@ 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(); + 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() @@ -329,6 +391,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] @@ -396,7 +462,7 @@ mod tests { .to_string(), ctx, ); - assert_eq!(res, "UndeclaredReference"); + assert_eq!(res, "Undeclared reference to 'test_custom_func'"); } #[test] @@ -495,13 +561,19 @@ 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(), 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..c59e0c1 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 {} From 4a45a25a1382265c5d2beb791a7997fe3b703d47 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Thu, 19 Sep 2024 21:06:43 +0200 Subject: [PATCH 2/4] Fix wasm compilation issues --- src/lib.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8e6bfaa..2244c32 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,6 +38,8 @@ pub trait AsyncHostContext: Send + Sync { #[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"))] @@ -190,11 +192,11 @@ fn execute_with( 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)); @@ -277,7 +279,7 @@ fn execute_with( 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)) + if device.contains_key(&it.0) {PropType::Device} else {PropType::Computed}, From b0bf77efce500cd8ec8b2fe098ad717338c7995b Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Tue, 24 Sep 2024 18:03:15 +0200 Subject: [PATCH 3/4] Expose parse_to_ast method --- src/cel.udl | 1 + src/lib.rs | 83 ++++++++++++++++++++++++++++++++----------------- wasm/src/lib.rs | 5 +++ 3 files changed, 61 insertions(+), 28 deletions(-) diff --git a/src/cel.udl b/src/cel.udl index a7eb5f0..b5b3373 100644 --- a/src/cel.udl +++ b/src/cel.udl @@ -11,4 +11,5 @@ 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 2244c32..0a68071 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,28 +18,26 @@ 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 `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; + fn device_property(&self, name: String, args: String) -> String; } #[cfg(not(target_arch = "wasm32"))] @@ -48,7 +46,6 @@ 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; - } /** @@ -108,8 +105,20 @@ pub fn evaluate_with_context(definition: String, host: Arc) -> } /** - 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), @@ -171,7 +180,6 @@ fn execute_with( 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)); @@ -204,7 +212,6 @@ fn execute_with( passable } - let computed = computed.unwrap_or(HashMap::new()).clone(); // Create computed properties as a map of keys and function names @@ -253,11 +260,11 @@ fn execute_with( map: Arc::new(computed_host_properties), }), ) - .unwrap(); + .unwrap(); let binding = device.clone(); // Combine the device and computed properties - let host_properties = binding + let host_properties = binding .iter() .chain(computed.iter()) .map(|(k, v)| (k.clone(), v.clone())) @@ -280,9 +287,7 @@ fn execute_with( 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}, + { PropType::Device } else { PropType::Computed }, name.clone(), Some( args.iter() @@ -293,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()) + }) }, ); } @@ -374,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()) } } @@ -415,7 +421,7 @@ mod tests { } "# - .to_string(), + .to_string(), ctx, ); assert_eq!(res, "true"); @@ -438,7 +444,7 @@ mod tests { } "# - .to_string(), + .to_string(), ctx, ); assert_eq!(res, "true"); @@ -461,7 +467,7 @@ mod tests { } "# - .to_string(), + .to_string(), ctx, ); assert_eq!(res, "Undeclared reference to 'test_custom_func'"); @@ -491,7 +497,7 @@ mod tests { } "# - .to_string(), + .to_string(), ctx, ); assert_eq!(res, "true"); @@ -526,7 +532,7 @@ mod tests { } "# - .to_string(), + .to_string(), ctx, ); println!("{}", res); @@ -578,10 +584,31 @@ mod tests { "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/wasm/src/lib.rs b/wasm/src/lib.rs index c59e0c1..c4990d6 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -106,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] From 9e5f061191939e7b2e0794d7457a840fee7809a9 Mon Sep 17 00:00:00 2001 From: Ian Rumac Date: Tue, 24 Sep 2024 18:04:46 +0200 Subject: [PATCH 4/4] Bump version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7b6b5c8..66b5daa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cel-eval" -version = "0.1.5" +version = "0.1.6" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.htmlž