use cel::{Context, Program, Value}; use serde_wasm_bindgen; use std::collections::HashMap; use wasm_bindgen::prelude::*; // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global // allocator. #[cfg(feature = "wee_alloc")] #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = console)] fn log(s: &str); } macro_rules! console_log { ($($t:tt)*) => (log(&format_args!($($t)*).to_string())) } #[wasm_bindgen] pub struct CelResult { success: bool, result: String, error: Option, } #[wasm_bindgen] impl CelResult { #[wasm_bindgen(getter)] pub fn success(&self) -> bool { self.success } #[wasm_bindgen(getter)] pub fn result(&self) -> String { self.result.clone() } #[wasm_bindgen(getter)] pub fn error(&self) -> Option { self.error.clone() } } /// Convert a JavaScript value to a CEL Value recursively fn js_value_to_cel_value(js_val: &JsValue) -> Result { if js_val.is_null() || js_val.is_undefined() { Ok(Value::Null) } else if js_val.is_bigint() { // Handle BigInt match js_val.as_f64() { Some(n) => Ok(Value::Int(n as i64)), None => Err("Failed to convert BigInt to number".to_string()), } } else if let Some(b) = js_val.as_bool() { Ok(Value::Bool(b)) } else if let Some(n) = js_val.as_f64() { // Check if it's an integer if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 { Ok(Value::Int(n as i64)) } else { Ok(Value::Float(n)) } } else if let Some(s) = js_val.as_string() { Ok(Value::String(s.into())) } else if js_val.is_array() { // Handle arrays let array = js_sys::Array::from(js_val); let mut cel_vec = Vec::new(); for i in 0..array.length() { let item = array.get(i); let cel_item = js_value_to_cel_value(&item)?; cel_vec.push(cel_item); } Ok(Value::List(cel_vec.into())) } else if js_val.is_object() { // Handle objects let obj = js_sys::Object::from(js_val.clone()); let entries = js_sys::Object::entries(&obj); let mut cel_map = HashMap::new(); for i in 0..entries.length() { let entry = js_sys::Array::from(&entries.get(i)); let key = entry .get(0) .as_string() .ok_or_else(|| "Object key must be a string".to_string())?; let value = entry.get(1); let cel_value = js_value_to_cel_value(&value)?; cel_map.insert(key, cel_value); } Ok(Value::Map(cel_map.into())) } else { Err(format!("Unsupported JavaScript type: {:?}", js_val)) } } /// Convert a CEL Value back to a JavaScript value fn cel_value_to_js_value(cel_val: &Value) -> Result { match cel_val { Value::Null => Ok(JsValue::NULL), Value::Bool(b) => Ok(JsValue::from_bool(*b)), Value::Int(i) => Ok(JsValue::from_f64(*i as f64)), Value::UInt(u) => Ok(JsValue::from_f64(*u as f64)), Value::Float(f) => Ok(JsValue::from_f64(*f)), Value::String(s) => Ok(JsValue::from_str(s)), Value::Bytes(b) => { // Convert bytes to Uint8Array let array = js_sys::Uint8Array::new_with_length(b.len() as u32); for (i, byte) in b.iter().enumerate() { array.set_index(i as u32, *byte); } Ok(array.into()) } Value::List(list) => { let array = js_sys::Array::new(); for item in list.iter() { let js_item = cel_value_to_js_value(item)?; array.push(&js_item); } Ok(array.into()) } Value::Map(map) => { let obj = js_sys::Object::new(); for (key, value) in map.iter() { let js_value = cel_value_to_js_value(value)?; js_sys::Reflect::set(&obj, &JsValue::from_str(key), &js_value) .map_err(|_| "Failed to set object property".to_string())?; } Ok(obj.into()) } Value::Duration(d) => { // Convert duration to string representation Ok(JsValue::from_str(&format!("{}s", d.as_secs_f64()))) } Value::Timestamp(ts) => { // Convert timestamp to ISO string let datetime = chrono::DateTime::from_timestamp(ts.as_secs() as i64, ts.subsec_nanos()) .ok_or_else(|| "Invalid timestamp".to_string())?; Ok(JsValue::from_str(&datetime.to_rfc3339())) } _ => Err(format!( "Unsupported CEL type for conversion: {:?}", cel_val )), } } /// Evaluate a CEL expression with the given context #[wasm_bindgen] pub fn evaluate_cel(expression: &str, context: &JsValue) -> CelResult { let result = evaluate_cel_internal(expression, context); match result { Ok(value) => CelResult { success: true, result: format!("{:?}", value), error: None, }, Err(error) => CelResult { success: false, result: String::new(), error: Some(error), }, } } /// Evaluate a CEL expression and return the result as a JavaScript value #[wasm_bindgen] pub fn evaluate_cel_js(expression: &str, context: &JsValue) -> Result { match evaluate_cel_internal(expression, context) { Ok(cel_value) => match cel_value_to_js_value(&cel_value) { Ok(js_val) => Ok(js_val), Err(e) => Err(JsValue::from_str(&format!("Conversion error: {}", e))), }, Err(e) => Err(JsValue::from_str(&e)), } } fn evaluate_cel_internal(expression: &str, context: &JsValue) -> Result { // Compile the CEL expression let program = Program::compile(expression).map_err(|e| format!("Compilation error: {}", e))?; // Create context let mut cel_context = Context::default(); // Add context variables if provided if !context.is_undefined() && !context.is_null() { if context.is_object() { let obj = js_sys::Object::from(context.clone()); let entries = js_sys::Object::entries(&obj); for i in 0..entries.length() { let entry = js_sys::Array::from(&entries.get(i)); let key = entry .get(0) .as_string() .ok_or_else(|| "Context key must be a string".to_string())?; let value = entry.get(1); let cel_value = js_value_to_cel_value(&value) .map_err(|e| format!("Failed to convert context value '{}': {}", key, e))?; cel_context .add_variable(&key, cel_value) .map_err(|e| format!("Failed to add context variable '{}': {}", key, e))?; } } else { return Err("Context must be an object".to_string()); } } // Execute the program program .execute(&cel_context) .map_err(|e| format!("Execution error: {}", e)) } /// Initialize the WASM module (optional, for debugging) #[wasm_bindgen(start)] pub fn main() { #[cfg(feature = "console_error_panic_hook")] console_error_panic_hook::set_once(); console_log!("CEL-Rust WASM module initialized!"); }