mirror of
https://github.com/duhanbalci/dexpr.git
synced 2026-07-01 16:19:16 +00:00
initial commit
This commit is contained in:
2
wasm/.gitignore
vendored
Normal file
2
wasm/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
/pkg
|
||||
1009
wasm/Cargo.lock
generated
Normal file
1009
wasm/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
wasm/Cargo.toml
Normal file
23
wasm/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "dexpr-wasm"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
dexpr = { path = ".." }
|
||||
wasm-bindgen = "0.2"
|
||||
js-sys = "0.3"
|
||||
serde_json = "1"
|
||||
rust_decimal = "1.41.0"
|
||||
smol_str = "0.3.6"
|
||||
indexmap = "2"
|
||||
# Required for rand/crypto to work in WASM (browser crypto.getRandomValues)
|
||||
getrandom_03 = { package = "getrandom", version = "0.3", features = ["wasm_js"] }
|
||||
getrandom_04 = { package = "getrandom", version = "0.4", features = ["wasm_js"] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
lto = true
|
||||
192
wasm/src/lib.rs
Normal file
192
wasm/src/lib.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
use dexpr::ast::value::Value;
|
||||
use dexpr::compiler::Compiler;
|
||||
use dexpr::language_info::LanguageInfo;
|
||||
use dexpr::vm::VM;
|
||||
use indexmap::IndexMap;
|
||||
use smol_str::SmolStr;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// Convert a dexpr Value to a serde_json::Value for JS interop.
|
||||
fn value_to_json(val: &Value) -> serde_json::Value {
|
||||
match val {
|
||||
Value::Null => serde_json::Value::Null,
|
||||
Value::Boolean(b) => serde_json::Value::Bool(*b),
|
||||
Value::Number(n) => {
|
||||
if n.scale() == 0 {
|
||||
if let Ok(i) = n.to_string().parse::<i64>() {
|
||||
return serde_json::Value::Number(i.into());
|
||||
}
|
||||
}
|
||||
if let Some(f) = serde_json::Number::from_f64(n.to_string().parse::<f64>().unwrap_or(0.0)) {
|
||||
serde_json::Value::Number(f)
|
||||
} else {
|
||||
serde_json::Value::String(n.to_string())
|
||||
}
|
||||
}
|
||||
Value::String(s) => serde_json::Value::String(s.to_string()),
|
||||
Value::NumberList(list) => {
|
||||
serde_json::Value::Array(list.iter().map(|n| value_to_json(&Value::Number(*n))).collect())
|
||||
}
|
||||
Value::StringList(list) => {
|
||||
serde_json::Value::Array(list.iter().map(|s| serde_json::Value::String(s.to_string())).collect())
|
||||
}
|
||||
Value::Object(map) => {
|
||||
let obj: serde_json::Map<String, serde_json::Value> = map
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), value_to_json(v)))
|
||||
.collect();
|
||||
serde_json::Value::Object(obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// dexpr engine for browser use via WASM.
|
||||
///
|
||||
/// Holds global variables and language metadata.
|
||||
/// Compile + execute expressions in one call via `execute()`.
|
||||
#[wasm_bindgen]
|
||||
pub struct DexprEngine {
|
||||
globals: IndexMap<SmolStr, Value>,
|
||||
language_info: LanguageInfo,
|
||||
external_fns: Vec<(String, js_sys::Function)>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl DexprEngine {
|
||||
/// Create a new engine instance.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
globals: IndexMap::new(),
|
||||
language_info: LanguageInfo::builtin(),
|
||||
external_fns: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a global variable from a JSON value string.
|
||||
///
|
||||
/// ```js
|
||||
/// engine.setGlobal("customer", '{"name": "Alice", "age": 30}');
|
||||
/// engine.setGlobal("price", "100");
|
||||
/// engine.setGlobal("name", '"hello"');
|
||||
/// ```
|
||||
#[wasm_bindgen(js_name = setGlobal)]
|
||||
pub fn set_global(&mut self, name: &str, json: &str) -> Result<(), JsError> {
|
||||
let value = Value::from_json(json).map_err(|e| JsError::new(&e))?;
|
||||
self.update_language_info(name, &value);
|
||||
self.globals.insert(SmolStr::new(name), value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a global variable as JSON string. Returns undefined if not found.
|
||||
#[wasm_bindgen(js_name = getGlobal)]
|
||||
pub fn get_global(&self, name: &str) -> Option<String> {
|
||||
self.globals
|
||||
.get(name)
|
||||
.map(|v| serde_json::to_string(&value_to_json(v)).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Register an external function callable from dexpr code.
|
||||
///
|
||||
/// The JS function receives arguments as a JSON array string
|
||||
/// and must return a JSON value string.
|
||||
///
|
||||
/// ```js
|
||||
/// engine.registerFunction("getRate", (argsJson) => {
|
||||
/// const args = JSON.parse(argsJson);
|
||||
/// return JSON.stringify(34.5);
|
||||
/// });
|
||||
/// ```
|
||||
#[wasm_bindgen(js_name = registerFunction)]
|
||||
pub fn register_function(&mut self, name: &str, f: js_sys::Function) {
|
||||
self.external_fns.push((name.to_string(), f));
|
||||
}
|
||||
|
||||
/// Compile and execute dexpr source code. Returns the result as JSON string.
|
||||
///
|
||||
/// ```js
|
||||
/// const result = engine.execute('customer.name.upper()');
|
||||
/// const parsed = JSON.parse(result);
|
||||
/// // → "ALICE"
|
||||
/// ```
|
||||
pub fn execute(&mut self, source: &str) -> Result<String, JsError> {
|
||||
// Compile
|
||||
let mut compiler = Compiler::new();
|
||||
let (bytecode, debug_info) = compiler
|
||||
.compile_from_source(source)
|
||||
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||
|
||||
// Create VM
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_debug_info(&debug_info);
|
||||
|
||||
// Apply globals
|
||||
for (name, value) in &self.globals {
|
||||
vm.set_global(name, value.clone());
|
||||
}
|
||||
|
||||
// Apply external functions
|
||||
for (name, js_fn) in &self.external_fns {
|
||||
let js_fn = js_fn.clone();
|
||||
vm.register_function(name, move |args: &[Value]| {
|
||||
let json_args: Vec<serde_json::Value> = args.iter().map(value_to_json).collect();
|
||||
let args_str = serde_json::to_string(&json_args).unwrap_or_default();
|
||||
|
||||
let this = JsValue::NULL;
|
||||
let js_arg = JsValue::from_str(&args_str);
|
||||
let result = js_fn
|
||||
.call1(&this, &js_arg)
|
||||
.map_err(|e| format!("JS function error: {:?}", e))?;
|
||||
|
||||
let result_str = result
|
||||
.as_string()
|
||||
.ok_or_else(|| "External function must return a JSON string".to_string())?;
|
||||
Value::from_json(&result_str)
|
||||
});
|
||||
}
|
||||
|
||||
// Execute
|
||||
let result = vm.execute().map_err(|e| JsError::new(&e.to_string()))?;
|
||||
|
||||
// Collect modified globals back
|
||||
for (name, _) in self.globals.clone().iter() {
|
||||
if let Some(val) = vm.get_global(name) {
|
||||
self.globals.insert(name.clone(), val.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Return result as JSON
|
||||
let json = serde_json::to_string(&value_to_json(&result))
|
||||
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
/// Get language metadata JSON for the CodeMirror editor.
|
||||
/// Includes built-in + registered functions/methods and variables with field types.
|
||||
/// Variables are auto-populated from setGlobal calls.
|
||||
///
|
||||
/// ```js
|
||||
/// const metadata = JSON.parse(engine.languageInfo());
|
||||
/// const extensions = [basicSetup, dexpr(metadata)];
|
||||
/// ```
|
||||
#[wasm_bindgen(js_name = languageInfo)]
|
||||
pub fn language_info(&self) -> String {
|
||||
self.language_info.to_json()
|
||||
}
|
||||
|
||||
/// Add a host function to the language metadata (for editor autocomplete).
|
||||
/// Call this alongside registerFunction so the editor knows about it.
|
||||
#[wasm_bindgen(js_name = addFunctionInfo)]
|
||||
pub fn add_function_info(&mut self, name: &str, signature: &str, doc: Option<String>) {
|
||||
let name: &'static str = Box::leak(name.to_string().into_boxed_str());
|
||||
let signature: &'static str = Box::leak(signature.to_string().into_boxed_str());
|
||||
let doc: Option<&'static str> = doc.map(|d| &*Box::leak(d.into_boxed_str()));
|
||||
self.language_info.add_function(name, signature, doc);
|
||||
}
|
||||
|
||||
/// Internal: update language info when a global is set.
|
||||
fn update_language_info(&mut self, name: &str, value: &Value) {
|
||||
self.language_info.variables.retain(|v| v.name != name);
|
||||
self.language_info.add_value(name, value, None);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user