diff --git a/Cargo.lock b/Cargo.lock index 2e38df4..43bf4a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,9 +91,10 @@ dependencies = [ [[package]] name = "cel_js_example" -version = "0.0.0" +version = "0.1.0" dependencies = [ "cel", + "serde_json", ] [[package]] @@ -112,6 +113,12 @@ dependencies = [ "serde", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "js-sys" version = "0.3.77" @@ -287,6 +294,12 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "scopeguard" version = "1.2.0" @@ -313,6 +326,18 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.142" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "smallvec" version = "1.15.1" diff --git a/Cargo.toml b/Cargo.toml index 84867ce..71aa4b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,10 +3,11 @@ description = "Example of running cel-rust interpreter in browser" edition = "2024" name = "cel_js_example" publish = false -version = "0.0.0" +version = "0.1.0" [dependencies] cel = "0.11.0" +serde_json = "1.0" [lib] crate-type = ["cdylib", "rlib"] diff --git a/index.html b/index.html index b082291..4239346 100644 --- a/index.html +++ b/index.html @@ -3,20 +3,72 @@ - Starlark evaluator + CEL evaluator @@ -39,7 +91,6 @@ const writeString = (s) => { const encoded = new TextEncoder().encode(s.trim()); const offset = instance.exports.allocation(4 + encoded.byteLength); - // TODO(april): this probably isn't guaranteed to be 4-byte aligned? Might need to fix. const memory = instance.exports.memory.buffer; const uint32s = new Uint32Array(memory, offset, 1); uint32s[0] = encoded.byteLength; @@ -48,29 +99,62 @@ return offset; }; - const content = document.getElementById("input").value; - const offset = instance.exports.evaluate(writeString(content)); + // Combine CEL expression and JSON context + const expression = document.getElementById("input").value; + const context = document.getElementById("context").value; + + // Create a combined input with expression and context separated by a delimiter + const combinedInput = expression + "\n---CONTEXT---\n" + context; + + const offset = instance.exports.evaluate(writeString(combinedInput)); const ok = readU8(offset) != 0; const result = readString(offset + 4); const output = document.getElementById("output"); - output.value = (ok ? "" : "ERROR\n") + result; + + // Style the output based on success/error + if (ok) { + output.className = "success"; + output.value = result; + } else { + output.className = "error"; + output.value = "ERROR\n" + result; + } + }).catch(error => { + const output = document.getElementById("output"); + output.className = "error"; + output.value = "WASM Loading Error: " + error.message; }); } window.addEventListener("load", function () { document.getElementById("input").addEventListener("input", run, false); - run() - }) + document.getElementById("context").addEventListener("input", run, false); + run(); + }); -

Starlark evaluator

+ +

CEL evaluator

- Using cel-rust compiled to web assembly. - Change the input to see it update. + Using cel-rust compiled to WebAssembly. + Change the CEL expression or JSON context to see it update in real-time.

- - + +
+
+ + +
+ +
+ + +
+
+ +
+ + +
- + \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index d4b0d0e..5508135 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,10 @@ +use std::collections::HashMap; use std::mem; use std::slice; use std::str; -use cel::{Context, ExecutionError, Program, Value}; +use cel::{Context, Program, Value}; +use serde_json; #[unsafe(no_mangle)] pub extern "C" fn allocation(n: usize) -> *mut u8 { @@ -21,9 +23,9 @@ pub unsafe extern "C" fn evaluate(s: *const u8) -> *mut u8 { fn evaluate_buffers(input: &[u8]) -> Vec { let contents = str::from_utf8(input).unwrap(); - let result = evaluate_cel(contents); + let result = evaluate_cel_with_context(contents); let success = result.is_ok(); - let message = result.unwrap_or_else(|e| e.to_string()); + let message = result.unwrap_or_else(|e| e); let len = message.len(); let mut buffer = Vec::with_capacity(len + 8); buffer.push(if success { 1 } else { 0 }); @@ -33,10 +35,75 @@ fn evaluate_buffers(input: &[u8]) -> Vec { buffer } -fn evaluate_cel(content: &str) -> Result { - let program = Program::compile(content).unwrap(); - let mut context = Context::default(); - // context.add_function("add", |a: i64, b: i64| a + b); - let value = program.execute(&context)?; - Ok(format!("{:?}", value)) +fn json_to_cel_value(json_value: &serde_json::Value) -> Value { + match json_value { + serde_json::Value::Null => Value::Null, + serde_json::Value::Bool(b) => Value::Bool(*b), + serde_json::Value::Number(n) => { + if let Some(i) = n.as_i64() { + Value::Int(i) + } else if let Some(f) = n.as_f64() { + Value::Float(f) + } else { + Value::Null + } + } + serde_json::Value::String(s) => Value::String(s.clone().into()), + serde_json::Value::Array(arr) => { + let cel_vec: Vec = arr.iter().map(json_to_cel_value).collect(); + Value::List(cel_vec.into()) + } + serde_json::Value::Object(obj) => { + let mut cel_map = HashMap::new(); + for (key, value) in obj.iter() { + cel_map.insert(key.clone(), json_to_cel_value(value)); + } + Value::Map(cel_map.into()) + } + } +} + +fn evaluate_cel_with_context(content: &str) -> Result { + // Split the input to get expression and context + let parts: Vec<&str> = content.split("\n---CONTEXT---\n").collect(); + let expression = parts[0].trim(); + let context_json = if parts.len() > 1 { + parts[1].trim() + } else { + "{}" + }; + + // Parse the CEL expression + let program = match Program::compile(expression) { + Ok(prog) => prog, + Err(e) => return Err(format!("Compilation error: {}", e)), + }; + + // Create context and add JSON values + let mut context = Context::default(); + + // Parse JSON context if provided and not empty + if !context_json.is_empty() && context_json != "{}" { + match serde_json::from_str::(context_json) { + Ok(json_value) => { + if let serde_json::Value::Object(obj) = json_value { + for (key, value) in obj.iter() { + let cel_value = json_to_cel_value(value); + context + .add_variable(key, cel_value) + .map_err(|e| format!("Context error: {}", e))?; + } + } else { + return Err("JSON context must be an object".to_string()); + } + } + Err(e) => return Err(format!("JSON parsing error: {}", e)), + } + } + + // Execute the program + match program.execute(&context) { + Ok(value) => Ok(format!("{:?}", value)), + Err(e) => Err(format!("Execution error: {}", e)), + } }