use crate::error::ErrorKind;
use crate::parsing::ast::DateTimeValue;
use crate::parsing::source::Source;
use crate::serialization::data_values_from_map;
use crate::{Engine, Error, SourceType};
use serde::Serialize;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
#[wasm_bindgen(js_name = Engine)]
pub struct WasmEngine {
engine: Rc<RefCell<Engine>>,
}
#[wasm_bindgen]
impl WasmEngine {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
console_error_panic_hook::set_once();
WasmEngine {
engine: Rc::new(RefCell::new(Engine::new())),
}
}
#[wasm_bindgen(js_name = load)]
pub fn load_wasm(&self, code: &str, attribute: &str) -> js_sys::Promise {
let code = code.to_string();
let label = if attribute.trim().is_empty() {
None
} else {
Some(attribute.to_string())
};
let engine = self.engine.clone();
wasm_bindgen_futures::future_to_promise(async move {
let source = match &label {
None => SourceType::Inline,
Some(s) => SourceType::Labeled(s.as_str()),
};
let result = engine.borrow_mut().load(&code, source);
match result {
Ok(()) => Ok(JsValue::UNDEFINED),
Err(load_err) => {
let errors: Vec<JsError> = load_err.errors.iter().map(JsError::from).collect();
Err(errors
.serialize(&js_error_serializer())
.expect("BUG: serialize JsError array"))
}
}
})
}
#[wasm_bindgen(js_name = run)]
pub fn run(
&self,
spec: &str,
rule_names: JsValue,
data_values: JsValue,
effective: Option<String>,
) -> Result<JsValue, JsValue> {
let effective_dt = effective
.as_ref()
.filter(|s| !s.trim().is_empty())
.and_then(|s| s.parse::<DateTimeValue>().ok())
.unwrap_or_else(DateTimeValue::now);
let rule_names = parse_rule_names(&rule_names).map_err(js_err)?;
let data = parse_data_values(&data_values).map_err(js_err)?;
let engine = self.engine.borrow();
let mut response = engine
.run(spec, Some(&effective_dt), data, false)
.map_err(|e| error_to_js(&e))?;
if !rule_names.is_empty() {
response.filter_rules(&rule_names);
}
serialize_engine_json(&response)
}
#[wasm_bindgen(js_name = list)]
pub fn list(&self) -> Result<JsValue, JsValue> {
let engine = self.engine.borrow();
let mut entries: Vec<SpecListEntry> = Vec::new();
for (spec, effective_from, effective_to) in engine.list_specs_with_ranges() {
let plan = engine
.get_plan(&spec.name, effective_from.as_ref())
.map_err(|e| error_to_js(&e))?;
entries.push(SpecListEntry {
name: spec.name.clone(),
effective_from: effective_from.map(|d| d.to_string()),
effective_to: effective_to.map(|d| d.to_string()),
schema: plan.schema(),
});
}
serialize_engine_json(&entries)
}
#[wasm_bindgen(js_name = schema)]
pub fn schema(&self, spec: &str, effective: Option<String>) -> Result<JsValue, JsValue> {
let effective_dt = effective
.as_ref()
.filter(|s| !s.trim().is_empty())
.and_then(|s| s.parse::<DateTimeValue>().ok())
.unwrap_or_else(DateTimeValue::now);
let engine = self.engine.borrow();
let plan = engine
.get_plan(spec, Some(&effective_dt))
.map_err(|e| error_to_js(&e))?;
let schema = plan.schema();
serialize_engine_json(&schema)
}
#[wasm_bindgen(js_name = invert)]
pub fn invert(
&self,
_spec_name: &str,
_rule_name: &str,
_target_json: &str,
_provided_values_json: &str,
) -> Result<JsValue, JsValue> {
Err(js_err("Inversion not implemented"))
}
#[wasm_bindgen(js_name = format)]
pub fn format_wasm(&self, code: &str, attribute: Option<String>) -> Result<JsValue, JsValue> {
let attr = match attribute
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
{
Some(s) => s,
None => SourceType::INLINE_KEY,
};
match crate::format_source(code, attr) {
Ok(formatted) => Ok(JsValue::from_str(&formatted)),
Err(e) => Err(error_to_js(&e)),
}
}
}
#[derive(Serialize)]
struct SpecListEntry {
name: String,
effective_from: Option<String>,
effective_to: Option<String>,
schema: crate::planning::execution_plan::SpecSchema,
}
fn serialize_engine_json<T: Serialize>(v: &T) -> Result<JsValue, JsValue> {
let s = serde_json::to_string(v)
.map_err(|e| js_err(format!("BUG: serde_json::to_string failed: {}", e)))?;
js_sys::JSON::parse(&s).map_err(|e| {
let detail = e
.as_string()
.unwrap_or_else(|| format!("(non-string error from JSON.parse)"));
js_err(format!("BUG: JSON.parse failed: {}", detail))
})
}
fn js_err(msg: impl Into<String>) -> JsValue {
JsValue::from_str(&msg.into())
}
#[derive(Serialize)]
struct JsSource<'a> {
attribute: &'a str,
line: usize,
column: usize,
length: usize,
}
impl<'a> From<&'a Source> for JsSource<'a> {
fn from(s: &'a Source) -> Self {
JsSource {
attribute: &s.attribute,
line: s.span.line,
column: s.span.col,
length: s.span.end.saturating_sub(s.span.start),
}
}
}
#[derive(Serialize)]
struct JsError<'a> {
kind: ErrorKind,
message: &'a str,
related_data: Option<&'a str>,
spec: Option<&'a str>,
related_spec: Option<&'a str>,
source: Option<JsSource<'a>>,
suggestion: Option<&'a str>,
}
impl<'a> From<&'a Error> for JsError<'a> {
fn from(e: &'a Error) -> Self {
JsError {
kind: e.kind(),
message: e.message(),
related_data: e.related_data(),
spec: e.spec(),
related_spec: e.related_spec(),
source: e.source_location().map(JsSource::from),
suggestion: e.suggestion(),
}
}
}
fn js_error_serializer() -> serde_wasm_bindgen::Serializer {
serde_wasm_bindgen::Serializer::new().serialize_missing_as_null(true)
}
fn error_to_js(e: &Error) -> JsValue {
let err = JsError::from(e);
err.serialize(&js_error_serializer())
.expect("BUG: serialize JsError")
}
fn parse_rule_names(v: &JsValue) -> Result<Vec<String>, String> {
if v.is_undefined() || v.is_null() {
return Ok(Vec::new());
}
if js_sys::Array::is_array(v) {
return serde_wasm_bindgen::from_value(v.clone())
.map_err(|e| format!("rule_names must be an array of strings: {}", e));
}
if let Some(s) = v.as_string() {
let trimmed = s.trim();
if trimmed.is_empty() || trimmed == "[]" {
return Ok(Vec::new());
}
return serde_json::from_str::<Vec<String>>(trimmed).map_err(|e| {
format!(
"rule_names must be an array of strings (or JSON array string): {}",
e
)
});
}
Err("rule_names must be an array of strings".into())
}
fn parse_data_values(v: &JsValue) -> Result<HashMap<String, String>, String> {
if v.is_undefined() || v.is_null() {
return Ok(HashMap::new());
}
let map: HashMap<String, serde_json::Value> = serde_wasm_bindgen::from_value(v.clone())
.map_err(|e| format!("data_values must be a plain object: {}", e))?;
Ok(data_values_from_map(map))
}