use wasm_bindgen::prelude::*;
#[wasm_bindgen(js_name = "parseXml")]
pub fn wasm_parse_xml(xml: &str) -> Result<String, JsValue> {
let chart = crate::parse::sanitize::parse_untrusted(
xml,
&crate::parse::sanitize::InputLimits::default(),
)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
serde_json::to_string(&chart).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = "parseJson")]
pub fn wasm_parse_json(json: &str) -> Result<String, JsValue> {
let chart =
crate::parse::json::parse_json(json).map_err(|e| JsValue::from_str(&e.to_string()))?;
serde_json::to_string(&chart).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = "validate")]
pub fn wasm_validate(json: &str) -> Result<String, JsValue> {
let chart =
crate::parse::json::parse_json(json).map_err(|e| JsValue::from_str(&e.to_string()))?;
crate::validate::validate(&chart).map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok("ok".into())
}
#[wasm_bindgen(js_name = "toDot")]
pub fn wasm_to_dot(json: &str) -> Result<String, JsValue> {
let chart =
crate::parse::json::parse_json(json).map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(crate::export::dot::to_dot(&chart))
}
#[wasm_bindgen(js_name = "toMermaid")]
pub fn wasm_to_mermaid(json: &str) -> Result<String, JsValue> {
let chart =
crate::parse::json::parse_json(json).map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(crate::export::mermaid::to_mermaid(&chart))
}
#[wasm_bindgen(js_name = "toXml")]
pub fn wasm_to_xml(json: &str) -> Result<String, JsValue> {
let chart =
crate::parse::json::parse_json(json).map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(crate::export::xml::to_xml(&chart))
}
#[wasm_bindgen(js_name = "flatten")]
pub fn wasm_flatten(json: &str) -> Result<String, JsValue> {
let chart =
crate::parse::json::parse_json(json).map_err(|e| JsValue::from_str(&e.to_string()))?;
let (states, transitions) = crate::flatten::flatten(&chart);
let result = serde_json::json!({
"states": states,
"transitions": transitions,
});
serde_json::to_string(&result).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = "xmlToDot")]
pub fn wasm_xml_to_dot(xml: &str) -> Result<String, JsValue> {
let chart = crate::parse::sanitize::parse_untrusted(
xml,
&crate::parse::sanitize::InputLimits::default(),
)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
crate::validate::validate(&chart).map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(crate::export::dot::to_dot(&chart))
}
#[wasm_bindgen(js_name = "validateAll")]
pub fn wasm_validate_all(json: &str) -> Result<String, JsValue> {
let chart =
crate::parse::json::parse_json(json).map_err(|e| JsValue::from_str(&e.to_string()))?;
let errors = crate::validate::validate_all(&chart);
let error_strings: Vec<String> = errors.iter().map(|e| e.to_string()).collect();
serde_json::to_string(&error_strings).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[cfg(feature = "xstate")]
#[wasm_bindgen(js_name = "parseXstate")]
pub fn wasm_parse_xstate(json: &str) -> Result<String, JsValue> {
let chart =
crate::xstate::import::parse_xstate(json).map_err(|e| JsValue::from_str(&e.to_string()))?;
serde_json::to_string(&chart).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[cfg(feature = "xstate")]
#[wasm_bindgen(js_name = "toXstate")]
pub fn wasm_to_xstate(json: &str) -> Result<String, JsValue> {
let chart =
crate::parse::json::parse_json(json).map_err(|e| JsValue::from_str(&e.to_string()))?;
crate::xstate::export::to_xstate(&chart).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = "simulateStep")]
pub fn wasm_simulate_step(json: &str, current_state: &str, event: &str) -> Result<String, JsValue> {
let chart =
crate::parse::json::parse_json(json).map_err(|e| JsValue::from_str(&e.to_string()))?;
let mut modified_chart = chart;
modified_chart.initial = current_state.into();
let mut sim = crate::simulate::Simulator::new(&modified_chart);
match sim.send(event) {
Ok(new_state) => {
let result = serde_json::json!({
"state": new_state,
"ok": true,
});
serde_json::to_string(&result).map_err(|e| JsValue::from_str(&e.to_string()))
}
Err(e) => {
let result = serde_json::json!({
"state": current_state,
"ok": false,
"error": e.to_string(),
});
serde_json::to_string(&result).map_err(|e| JsValue::from_str(&e.to_string()))
}
}
}
#[wasm_bindgen(js_name = "availableEvents")]
pub fn wasm_available_events(json: &str, current_state: &str) -> Result<String, JsValue> {
let chart =
crate::parse::json::parse_json(json).map_err(|e| JsValue::from_str(&e.to_string()))?;
let index = crate::index::StateIndex::new(&chart);
let mut events = Vec::new();
let mut state_id: Option<&str> = Some(current_state);
while let Some(sid) = state_id {
if let Some(state) = index.state_map().get(sid) {
for t in &state.transitions {
if let Some(event) = &t.event {
let ev = event.to_string();
if !events.contains(&ev) {
events.push(ev);
}
}
}
}
state_id = index.parent_map().get(sid).copied();
}
serde_json::to_string(&events).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = "diff")]
pub fn wasm_diff(json_a: &str, json_b: &str) -> Result<String, JsValue> {
let chart_a =
crate::parse::json::parse_json(json_a).map_err(|e| JsValue::from_str(&e.to_string()))?;
let chart_b =
crate::parse::json::parse_json(json_b).map_err(|e| JsValue::from_str(&e.to_string()))?;
let diffs = crate::diff::diff(&chart_a, &chart_b);
let result: Vec<serde_json::Value> = diffs
.iter()
.map(|d| {
let mut obj = serde_json::json!({ "path": d.path });
match &d.kind {
crate::diff::DiffKind::Changed { old, new } => {
obj["kind"] = "Changed".into();
obj["old"] = old.clone().into();
obj["new"] = new.clone().into();
}
crate::diff::DiffKind::Added { value } => {
obj["kind"] = "Added".into();
obj["value"] = value.clone().into();
}
crate::diff::DiffKind::Removed { value } => {
obj["kind"] = "Removed".into();
obj["value"] = value.clone().into();
}
}
obj
})
.collect();
serde_json::to_string(&result).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[wasm_bindgen(js_name = "xmlDiff")]
pub fn wasm_xml_diff(xml_a: &str, xml_b: &str) -> Result<String, JsValue> {
let limits = crate::parse::sanitize::InputLimits::default();
let chart_a = crate::parse::sanitize::parse_untrusted(xml_a, &limits)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let chart_b = crate::parse::sanitize::parse_untrusted(xml_b, &limits)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let diffs = crate::diff::diff(&chart_a, &chart_b);
let result: Vec<serde_json::Value> = diffs
.iter()
.map(|d| {
let mut obj = serde_json::json!({ "path": d.path });
match &d.kind {
crate::diff::DiffKind::Changed { old, new } => {
obj["kind"] = "Changed".into();
obj["old"] = old.clone().into();
obj["new"] = new.clone().into();
}
crate::diff::DiffKind::Added { value } => {
obj["kind"] = "Added".into();
obj["value"] = value.clone().into();
}
crate::diff::DiffKind::Removed { value } => {
obj["kind"] = "Removed".into();
obj["value"] = value.clone().into();
}
}
obj
})
.collect();
serde_json::to_string(&result).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[cfg(test)]
mod tests {
const SIMPLE_XML: &str = r#"
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="a">
<state id="a"><transition event="go" target="b"/></state>
<final id="b"/>
</scxml>
"#;
#[test]
fn parse_xml_to_json() {
let chart = crate::parse::xml::parse_xml(SIMPLE_XML).unwrap();
let json = serde_json::to_string(&chart).unwrap();
assert!(json.contains("\"initial\":\"a\""));
assert!(json.contains("\"id\":\"a\""));
}
#[test]
fn parse_xml_error_on_invalid() {
let result = crate::parse::xml::parse_xml("<not-scxml/>");
assert!(result.is_err());
}
#[test]
fn json_roundtrip() {
let chart = crate::parse::xml::parse_xml(SIMPLE_XML).unwrap();
let json = serde_json::to_string(&chart).unwrap();
let chart2 = crate::parse::json::parse_json(&json).unwrap();
let json2 = serde_json::to_string(&chart2).unwrap();
assert!(json2.contains("\"initial\":\"a\""));
}
#[test]
fn validate_valid_chart() {
let chart = crate::parse::xml::parse_xml(SIMPLE_XML).unwrap();
crate::validate::validate(&chart).unwrap();
}
#[test]
fn validate_invalid_chart() {
let bad_json = r#"{"initial":"missing","states":[{"id":"a","kind":"atomic","transitions":[],"on_entry":[],"on_exit":[],"children":[],"initial":null}]}"#;
let chart = crate::parse::json::parse_json(bad_json).unwrap();
assert!(crate::validate::validate(&chart).is_err());
}
#[test]
fn to_dot_produces_graph() {
let chart = crate::parse::xml::parse_xml(SIMPLE_XML).unwrap();
let dot = crate::export::dot::to_dot(&chart);
assert!(dot.contains("digraph statechart"));
assert!(dot.contains("\"a\""));
}
#[test]
fn to_xml_roundtrips() {
let chart = crate::parse::xml::parse_xml(SIMPLE_XML).unwrap();
let xml = crate::export::xml::to_xml(&chart);
assert!(xml.contains("<scxml"));
assert!(xml.contains("initial=\"a\""));
let chart2 = crate::parse::xml::parse_xml(&xml).unwrap();
assert_eq!(chart2.initial.as_str(), "a");
}
#[test]
fn flatten_returns_states_and_transitions() {
let chart = crate::parse::xml::parse_xml(SIMPLE_XML).unwrap();
let (states, transitions) = crate::flatten::flatten(&chart);
assert_eq!(states.len(), 2);
assert_eq!(transitions.len(), 1);
}
#[test]
fn xml_to_dot_pipeline() {
let chart = crate::parse::xml::parse_xml(SIMPLE_XML).unwrap();
crate::validate::validate(&chart).unwrap();
let dot = crate::export::dot::to_dot(&chart);
assert!(dot.contains("digraph statechart"));
assert!(dot.contains("\"a\""));
assert!(dot.contains("\"b\""));
}
#[test]
fn xml_to_dot_error_on_invalid() {
let result = crate::parse::xml::parse_xml("<invalid/>");
assert!(result.is_err());
}
}