switch to wasm-bindgen

This commit is contained in:
2025-08-08 07:06:47 +02:00
parent 6ccf7dce24
commit 943fe80e10
7 changed files with 769 additions and 262 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/pkg/
/target/

228
Cargo.lock generated
View File

@@ -11,6 +11,21 @@ dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "antlr4rust"
version = "0.3.0-beta3"
@@ -73,6 +88,15 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cc"
version = "1.2.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
dependencies = [
"shlex",
]
[[package]]
name = "cel"
version = "0.11.0"
@@ -90,13 +114,24 @@ dependencies = [
]
[[package]]
name = "cel_js_example"
version = "0.1.0"
name = "cel-rust-wasm"
version = "0.2.0"
dependencies = [
"cel",
"serde_json",
"chrono",
"console_error_panic_hook",
"serde-wasm-bindgen",
"wasm-bindgen",
"web-sys",
"wee_alloc",
]
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.1"
@@ -109,15 +144,54 @@ version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "itoa"
version = "1.0.15"
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if 1.0.1",
"wasm-bindgen",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "iana-time-zone"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "js-sys"
@@ -163,6 +237,12 @@ version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "memory_units"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@@ -219,7 +299,7 @@ version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
dependencies = [
"cfg-if",
"cfg-if 1.0.1",
"libc",
"redox_syscall",
"smallvec",
@@ -294,12 +374,6 @@ 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"
@@ -315,6 +389,17 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
@@ -327,16 +412,10 @@ dependencies = [
]
[[package]]
name = "serde_json"
version = "1.0.142"
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "smallvec"
@@ -403,7 +482,7 @@ version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"cfg-if 1.0.1",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
@@ -455,6 +534,109 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "wee_alloc"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e"
dependencies = [
"cfg-if 0.1.10",
"libc",
"memory_units",
"winapi",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-result"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.52.6"

View File

@@ -1,13 +1,38 @@
[package]
description = "Example of running cel-rust interpreter in browser"
name = "cel-rust-wasm"
version = "0.2.0"
edition = "2024"
name = "cel_js_example"
publish = false
version = "0.1.0"
[dependencies]
cel = "0.11.0"
serde_json = "1.0"
[lib]
crate-type = ["cdylib", "rlib"]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
cel = "0.11"
serde-wasm-bindgen = "0.6"
chrono = { version = "0.4", features = ["serde"] }
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.6", optional = true }
# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
# compared to the default allocator's ~10K. It is slower than the default
# allocator, however.
wee_alloc = { version = "0.4.5", optional = true }
[dependencies.web-sys]
version = "0.3"
features = [
"console",
]
[features]
default = ["console_error_panic_hook"]
[profile.release]
# Tell `rustc` to optimize for small code size.
opt-level = "s"
lto = true

18
README Normal file
View File

@@ -0,0 +1,18 @@
To build and use this:
1. Install wasm-pack (if you haven't already):
```bash
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
```
2. Build the WASM module:
```bash
wasm-pack build --target web --out-dir pkg
```
3. Serve the HTML file using a local HTTP server (required for ES modules):
```bash
python3 -m http.server 8000
# or
npx serve .
```

View File

@@ -1 +0,0 @@
target/wasm32-unknown-unknown/release/cel_js_example.wasm

View File

@@ -1,160 +1,320 @@
<!doctype html>
<html lang="en-US">
<head>
<html>
<head>
<meta charset="utf-8" />
<title>CEL evaluator</title>
<style type="text/css">
body {
font-family: sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
<title>CEL-Rust WASM-bindgen Example</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.full-width {
grid-column: 1 / -1;
}
.full-width {
grid-column: 1 / -1;
}
textarea {
width: 100%;
height: 15em;
box-sizing: border-box;
border-radius: 5px;
padding: 10px;
border: 2px solid lightgray;
font-family: 'Courier New', monospace;
font-size: 14px;
resize: vertical;
}
textarea,
input {
width: 100%;
box-sizing: border-box;
font-family: "Courier New", monospace;
font-size: 14px;
padding: 10px;
border: 2px solid #ddd;
border-radius: 4px;
}
#output {
background-color: #f8f9fa;
height: 10em;
}
textarea {
height: 120px;
resize: vertical;
}
label {
font-weight: bold;
display: block;
margin-bottom: 5px;
color: #333;
}
.result {
background-color: #f8f9fa;
padding: 10px;
border-radius: 4px;
border: 2px solid #28a745;
white-space: pre-wrap;
font-family: "Courier New", monospace;
min-height: 50px;
}
.error {
color: #d32f2f;
background-color: #ffebee;
}
.error {
background-color: #f8d7da;
border-color: #dc3545;
color: #721c24;
}
.success {
color: #2e7d32;
background-color: #e8f5e9;
}
.success {
background-color: #d4edda;
border-color: #28a745;
color: #155724;
}
h1 {
color: #1976d2;
margin-bottom: 10px;
}
label {
font-weight: bold;
display: block;
margin-bottom: 5px;
}
p {
color: #666;
line-height: 1.4;
}
button {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
a {
color: #1976d2;
}
button:hover {
background-color: #0056b3;
}
.examples {
margin-top: 30px;
}
.example {
background-color: #f1f3f4;
padding: 15px;
border-radius: 4px;
margin-bottom: 10px;
cursor: pointer;
}
.example:hover {
background-color: #e8eaed;
}
.example h4 {
margin: 0 0 10px 0;
color: #1976d2;
}
.example code {
display: block;
background-color: #fff;
padding: 8px;
border-radius: 3px;
margin: 5px 0;
}
</style>
</head>
<body>
<script>
function run() {
WebAssembly.instantiateStreaming(fetch("cel_js_example.wasm"), {}).then(({ instance }) => {
const readString = (offset) => {
const memory = instance.exports.memory.buffer;
const length = new Uint32Array(memory, offset, 1)[0];
const characters = new Uint8Array(memory, offset + 4, length);
return new TextDecoder().decode(characters);
};
const readU8 = (offset) => {
return new Uint8Array(instance.exports.memory.buffer, offset, 1)[0];
};
const writeString = (s) => {
const encoded = new TextEncoder().encode(s.trim());
const offset = instance.exports.allocation(4 + encoded.byteLength);
const memory = instance.exports.memory.buffer;
const uint32s = new Uint32Array(memory, offset, 1);
uint32s[0] = encoded.byteLength;
const uint8s = new Uint8Array(memory, offset + 4, encoded.byteLength);
uint8s.set(encoded);
return offset;
};
// 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");
// 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);
document.getElementById("context").addEventListener("input", run, false);
run();
});
</script>
<h1>CEL evaluator</h1>
</head>
<body>
<h1>CEL-Rust WASM-bindgen Demo</h1>
<p>
Using <a href="https://github.com/cel-rust/cel-rust">cel-rust</a> compiled to WebAssembly.
Change the CEL expression or JSON context to see it update in real-time.
This demo uses <code>wasm-bindgen</code> to provide a clean JavaScript API
for CEL evaluation.
</p>
<div class="container">
<div>
<label for="input">CEL Expression:</label>
<textarea id="input" placeholder="Enter your CEL expression here...">name + " is " + string(age) + " years old"</textarea>
</div>
<div>
<label for="context">JSON Context:</label>
<textarea id="context" placeholder="Enter JSON context here...">{"name": "Alice", "age": 30, "active": true}</textarea>
</div>
<div>
<label for="expression">CEL Expression:</label>
<textarea id="expression" placeholder="Enter CEL expression...">
name + " is " + string(age) + " years old and " + (active ? "active" : "inactive")</textarea
>
</div>
<div>
<label for="context">Context (as JavaScript object):</label>
<textarea
id="context"
placeholder="Enter context as JavaScript object..."
>
{"name": "Alice", "age": 30, "active": true, "scores": [95, 87, 92], "profile": {"country": "US", "level": "senior"}}</textarea
>
</div>
</div>
<div class="full-width">
<label for="output">Result:</label>
<textarea id="output" readonly="readonly"></textarea>
<button onclick="evaluate()">Evaluate CEL Expression</button>
<button onclick="evaluateAsJs()">Evaluate & Return as JS Value</button>
</div>
</body>
</html>
<div class="full-width">
<label>Result:</label>
<div id="result" class="result">Click "Evaluate" to see results...</div>
</div>
<div class="examples">
<h2>Examples (click to try):</h2>
<div class="example" onclick="loadExample(this)">
<h4>Basic String Operations</h4>
<code
data-expression="'Hello ' + name + '!'"
data-context='{"name": "World"}'
>Expression: 'Hello ' + name + '!'</code
>
<code>Context: {"name": "World"}</code>
</div>
<div class="example" onclick="loadExample(this)">
<h4>Math Operations</h4>
<code
data-expression="(price * quantity) * (1 + tax)"
data-context='{"price": 10.50, "quantity": 3, "tax": 0.08}'
>Expression: (price * quantity) * (1 + tax)</code
>
<code>Context: {"price": 10.50, "quantity": 3, "tax": 0.08}</code>
</div>
<div class="example" onclick="loadExample(this)">
<h4>List Operations</h4>
<code
data-expression="scores.filter(s, s > 90).size() + ' high scores out of ' + string(scores.size())"
data-context='{"scores": [85, 92, 78, 96, 88]}'
>Expression: scores.filter(s, s > 90).size() + ' high scores'</code
>
<code>Context: {"scores": [85, 92, 78, 96, 88]}</code>
</div>
<div class="example" onclick="loadExample(this)">
<h4>Conditional Logic</h4>
<code
data-expression="age >= 18 ? (age >= 65 ? 'senior' : 'adult') : 'minor'"
data-context='{"age": 25}'
>Expression: age >= 18 ? (age >= 65 ? 'senior' : 'adult') :
'minor'</code
>
<code>Context: {"age": 25}</code>
</div>
<div class="example" onclick="loadExample(this)">
<h4>Map/Object Access</h4>
<code
data-expression="user.profile.country == 'US' && user.profile.verified"
data-context='{"user": {"profile": {"country": "US", "verified": true}}}'
>Expression: user.profile.country == 'US' &&
user.profile.verified</code
>
<code
>Context: {"user": {"profile": {"country": "US", "verified":
true}}}</code
>
</div>
</div>
<script type="module">
import init, {
evaluate_cel,
evaluate_cel_js,
} from "./pkg/cel_rust_wasm.js";
let wasmModule;
async function initWasm() {
wasmModule = await init();
console.log("WASM module loaded successfully");
// Make functions globally available
window.evaluate_cel = evaluate_cel;
window.evaluate_cel_js = evaluate_cel_js;
// Enable the evaluate button
document.querySelector("button").disabled = false;
}
// Initialize WASM when the page loads
initWasm().catch(console.error);
// Make functions globally available for onclick handlers
window.evaluate = evaluate;
window.evaluateAsJs = evaluateAsJs;
window.loadExample = loadExample;
</script>
<script>
function evaluate() {
if (!window.evaluate_cel) {
document.getElementById("result").textContent =
"WASM module not loaded yet...";
return;
}
const expression = document.getElementById("expression").value;
const contextStr = document.getElementById("context").value;
const resultDiv = document.getElementById("result");
try {
// Parse the context as JavaScript object
const context = contextStr.trim() ? JSON.parse(contextStr) : {};
// Evaluate using the string result version
const result = window.evaluate_cel(expression, context);
if (result.success) {
resultDiv.className = "result success";
resultDiv.textContent = `Success: ${result.result}`;
} else {
resultDiv.className = "result error";
resultDiv.textContent = `Error: ${result.error}`;
}
} catch (e) {
resultDiv.className = "result error";
resultDiv.textContent = `JavaScript Error: ${e.message}`;
}
}
function evaluateAsJs() {
if (!window.evaluate_cel_js) {
document.getElementById("result").textContent =
"WASM module not loaded yet...";
return;
}
const expression = document.getElementById("expression").value;
const contextStr = document.getElementById("context").value;
const resultDiv = document.getElementById("result");
try {
// Parse the context as JavaScript object
const context = contextStr.trim() ? JSON.parse(contextStr) : {};
// Evaluate using the JavaScript value result version
const result = window.evaluate_cel_js(expression, context);
resultDiv.className = "result success";
resultDiv.textContent = `Success (JS Value): ${JSON.stringify(result, null, 2)}`;
// Log the actual JavaScript object to console
console.log("CEL Result as JavaScript value:", result);
} catch (e) {
resultDiv.className = "result error";
resultDiv.textContent = `Error: ${e}`;
}
}
function loadExample(element) {
const codeElement = element.querySelector("code[data-expression]");
const expression = codeElement.getAttribute("data-expression");
const context = codeElement.getAttribute("data-context");
document.getElementById("expression").value = expression;
document.getElementById("context").value = context;
// Auto-evaluate
setTimeout(evaluate, 100);
}
// Auto-evaluate on input change
document.getElementById("expression").addEventListener("input", () => {
setTimeout(evaluate, 300);
});
document.getElementById("context").addEventListener("input", () => {
setTimeout(evaluate, 300);
});
</script>
</body>
</html>

View File

@@ -1,109 +1,231 @@
use std::collections::HashMap;
use std::mem;
use std::slice;
use std::str;
use cel::{Context, Program, Value};
use serde_json;
use serde_wasm_bindgen;
use std::collections::HashMap;
use wasm_bindgen::prelude::*;
#[unsafe(no_mangle)]
pub extern "C" fn allocation(n: usize) -> *mut u8 {
mem::ManuallyDrop::new(Vec::with_capacity(n)).as_mut_ptr()
// 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);
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn evaluate(s: *const u8) -> *mut u8 {
unsafe {
let length = u32::from_le_bytes(*(s as *const [u8; 4])) as usize;
let input = slice::from_raw_parts(s.offset(4), length);
let output = evaluate_buffers(input);
mem::ManuallyDrop::new(output).as_mut_ptr()
macro_rules! console_log {
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}
#[wasm_bindgen]
pub struct CelResult {
success: bool,
result: String,
error: Option<String>,
}
#[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<String> {
self.error.clone()
}
}
fn evaluate_buffers(input: &[u8]) -> Vec<u8> {
let contents = str::from_utf8(input).unwrap();
let result = evaluate_cel_with_context(contents);
let success = result.is_ok();
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 });
buffer.extend(vec![0; 3]);
buffer.extend_from_slice(&(len as u32).to_le_bytes());
buffer.extend_from_slice(message.as_bytes());
buffer
}
/// Convert a JavaScript value to a CEL Value recursively
fn js_value_to_cel_value(js_val: &JsValue) -> Result<Value, String> {
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();
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
}
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);
}
serde_json::Value::String(s) => Value::String(s.clone().into()),
serde_json::Value::Array(arr) => {
let cel_vec: Vec<Value> = 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<String, String> {
// 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()
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))
}
}
// 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::<serde_json::Value>(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());
}
/// Convert a CEL Value back to a JavaScript value
fn cel_value_to_js_value(cel_val: &Value) -> Result<JsValue, String> {
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);
}
Err(e) => return Err(format!("JSON parsing error: {}", e)),
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<JsValue, JsValue> {
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<Value, String> {
// 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
match program.execute(&context) {
Ok(value) => Ok(format!("{:?}", value)),
Err(e) => Err(format!("Execution error: {}", e)),
}
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!");
}