switch to wasm-bindgen
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
/pkg/
|
||||
/target/
|
||||
|
||||
228
Cargo.lock
generated
228
Cargo.lock
generated
@@ -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"
|
||||
|
||||
43
Cargo.toml
43
Cargo.toml
@@ -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
18
README
Normal 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 .
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
target/wasm32-unknown-unknown/release/cel_js_example.wasm
|
||||
358
index.html
358
index.html
@@ -1,12 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
|
||||
<head>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>CEL evaluator</title>
|
||||
<style type="text/css">
|
||||
<title>CEL-Rust WASM-bindgen Example</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
@@ -23,138 +22,299 @@
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
textarea {
|
||||
textarea,
|
||||
input {
|
||||
width: 100%;
|
||||
height: 15em;
|
||||
box-sizing: border-box;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
border: 2px solid lightgray;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-family: "Courier New", monospace;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 120px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
#output {
|
||||
.result {
|
||||
background-color: #f8f9fa;
|
||||
height: 10em;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
border: 2px solid #28a745;
|
||||
white-space: pre-wrap;
|
||||
font-family: "Courier New", monospace;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: #f8d7da;
|
||||
border-color: #dc3545;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: #d4edda;
|
||||
border-color: #28a745;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #d32f2f;
|
||||
background-color: #ffebee;
|
||||
button {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #2e7d32;
|
||||
background-color: #e8f5e9;
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #1976d2;
|
||||
.examples {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.example {
|
||||
background-color: #f1f3f4;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
.example:hover {
|
||||
background-color: #e8eaed;
|
||||
}
|
||||
|
||||
a {
|
||||
.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>
|
||||
<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">JSON Context:</label>
|
||||
<textarea id="context" placeholder="Enter JSON context here...">{"name": "Alice", "age": 30, "active": true}</textarea>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
290
src/lib.rs
290
src/lib.rs
@@ -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
|
||||
}
|
||||
|
||||
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)
|
||||
/// 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 {
|
||||
Value::Null
|
||||
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);
|
||||
}
|
||||
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) => {
|
||||
|
||||
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 (key, value) in obj.iter() {
|
||||
cel_map.insert(key.clone(), json_to_cel_value(value));
|
||||
}
|
||||
Value::Map(cel_map.into())
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
} else {
|
||||
"{}"
|
||||
};
|
||||
/// 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);
|
||||
}
|
||||
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
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the CEL expression
|
||||
let program = match Program::compile(expression) {
|
||||
Ok(prog) => prog,
|
||||
Err(e) => return Err(format!("Compilation error: {}", e)),
|
||||
};
|
||||
/// 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);
|
||||
|
||||
// Create context and add JSON values
|
||||
let mut context = Context::default();
|
||||
match result {
|
||||
Ok(value) => CelResult {
|
||||
success: true,
|
||||
result: format!("{:?}", value),
|
||||
error: None,
|
||||
},
|
||||
Err(error) => CelResult {
|
||||
success: false,
|
||||
result: String::new(),
|
||||
error: Some(error),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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))?;
|
||||
/// 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("JSON context must be an object".to_string());
|
||||
}
|
||||
}
|
||||
Err(e) => return Err(format!("JSON parsing error: {}", e)),
|
||||
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!");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user