1use serde_json::{Map, Number, Value};
14use thiserror::Error;
15use xmltree::Error as XmlWriteError;
16use xmltree::{Element, XMLNode};
17
18pub mod scjson_props;
19
20const COLLAPSE_ATTRS: &[&str] = &[
59 "expr", "cond", "event", "target", "delay", "location", "name", "src", "id",
60];
61
62const SCXML_ELEMS: &[&str] = &[
64 "scxml",
65 "state",
66 "parallel",
67 "final",
68 "history",
69 "transition",
70 "invoke",
71 "finalize",
72 "datamodel",
73 "data",
74 "onentry",
75 "onexit",
76 "log",
77 "send",
78 "cancel",
79 "raise",
80 "assign",
81 "script",
82 "foreach",
83 "param",
84 "if",
85 "elseif",
86 "else",
87 "content",
88 "donedata",
89 "initial",
90];
91
92#[derive(Debug, Error)]
94pub enum ScjsonError {
95 #[error("XML parse error: {0}")]
96 Xml(#[from] xmltree::ParseError),
97 #[error("XML write error: {0}")]
98 XmlWrite(#[from] XmlWriteError),
99 #[error("JSON parse error: {0}")]
100 Json(#[from] serde_json::Error),
101 #[error("unsupported document")]
102 Unsupported,
103}
104
105fn append_child(map: &mut Map<String, Value>, key: &str, val: Value) {
106 match map.get_mut(key) {
107 Some(Value::Array(arr)) => arr.push(val),
108 Some(other) => {
109 let old = other.take();
110 *other = Value::Array(vec![old, val]);
111 }
112 None => {
113 map.insert(key.to_string(), Value::Array(vec![val]));
114 }
115 }
116}
117
118fn any_element_to_value(elem: &Element) -> Value {
119 let mut map = Map::new();
120 map.insert("qname".into(), Value::String(elem.name.clone()));
121 let text = elem.get_text().map(|c| c.into_owned()).unwrap_or_default();
122 map.insert("text".into(), Value::String(text));
123 if !elem.attributes.is_empty() {
124 let mut attrs = Map::new();
125 for (k, v) in &elem.attributes {
126 attrs.insert(k.clone(), Value::String(v.clone()));
127 }
128 map.insert("attributes".into(), Value::Object(attrs));
129 }
130 if !elem.children.is_empty() {
131 let mut children = Vec::new();
132 for c in &elem.children {
133 if let XMLNode::Element(e) = c {
134 children.push(any_element_to_value(e));
135 }
136 }
137 if !children.is_empty() {
138 map.insert("children".into(), Value::Array(children));
139 }
140 }
141 Value::Object(map)
142}
143
144fn element_to_map(elem: &Element) -> Map<String, Value> {
145 let mut map = Map::new();
146 for (k, v) in &elem.attributes {
147 match (elem.name.as_str(), k.as_str()) {
148 ("transition", "target") => {
149 let vals: Vec<Value> = v
150 .split_whitespace()
151 .map(|s| Value::String(s.to_string()))
152 .collect();
153 map.insert("target".into(), Value::Array(vals));
154 }
155 (_, "initial") => {
156 let vals: Vec<Value> = v
157 .split_whitespace()
158 .map(|s| Value::String(s.to_string()))
159 .collect();
160 if elem.name == "scxml" {
161 map.insert("initial".into(), Value::Array(vals));
162 } else {
163 map.insert("initial_attribute".into(), Value::Array(vals));
164 }
165 }
166 (_, "version") => {
167 if let Ok(n) = v.parse::<f64>() {
168 if let Some(num) = Number::from_f64(n) {
169 map.insert("version".into(), Value::Number(num));
170 }
171 } else {
172 map.insert("version".into(), Value::String(v.clone()));
173 }
174 }
175 (_, "datamodel") => {
176 map.insert("datamodel_attribute".into(), Value::String(v.clone()));
177 }
178 (_, "type") => {
179 map.insert("type_value".into(), Value::String(v.clone()));
180 }
181 (_, "raise") => {
182 map.insert("raise_value".into(), Value::String(v.clone()));
183 }
184 ("send", "delay") => {
185 map.insert("delay".into(), Value::String(v.clone()));
186 }
187 ("send", "event") => {
188 map.insert("event".into(), Value::String(v.clone()));
189 }
190 (_, "xmlns") => {}
191 _ => {
192 map.insert(k.clone(), Value::String(v.clone()));
193 }
194 }
195 }
196
197 if elem.name == "assign" && !map.contains_key("type_value") {
198 map.insert(
199 "type_value".to_string(),
200 Value::String("replacechildren".into()),
201 );
202 }
203 if elem.name == "send" {
204 map.entry("type_value".to_string())
205 .or_insert_with(|| Value::String("scxml".into()));
206 map.entry("delay".to_string())
207 .or_insert_with(|| Value::String("0s".into()));
208 }
209 if elem.name == "invoke" {
210 map.entry("type_value".to_string())
211 .or_insert_with(|| Value::String("scxml".into()));
212 map.entry("autoforward".to_string())
213 .or_insert_with(|| Value::String("false".into()));
214 }
215
216 let mut text_items = Vec::new();
217 for child in &elem.children {
218 match child {
219 XMLNode::Element(e) => {
220 if SCXML_ELEMS.contains(&e.name.as_str()) {
221 let key = match e.name.as_str() {
222 "if" => "if_value",
223 "else" => "else_value",
224 "raise" => "raise_value",
225 name => name,
226 };
227 let child_map = element_to_map(e);
228 let target_key = if e.name == "scxml" && elem.name != "scxml" {
229 "content"
230 } else if elem.name == "content" && e.name == "scxml" {
231 "content"
232 } else {
233 key
234 };
235 if (elem.name == "initial" || elem.name == "history") && e.name == "transition" {
236 map.insert(target_key.to_string(), Value::Object(child_map));
237 } else {
238 append_child(&mut map, target_key, Value::Object(child_map));
239 }
240 } else {
241 let val = any_element_to_value(e);
242 append_child(&mut map, "content", val);
243 }
244 }
245 XMLNode::Text(t) => {
246 if !t.trim().is_empty() {
247 text_items.push(Value::String(t.to_string()));
248 }
249 }
250 _ => {}
251 }
252 }
253 if !text_items.is_empty() {
254 for item in text_items {
255 append_child(&mut map, "content", item);
256 }
257 }
258
259 if elem.name == "scxml" {
260 if !map.contains_key("version") {
261 map.insert(
262 "version".into(),
263 Value::Number(Number::from_f64(1.0).unwrap()),
264 );
265 }
266 map.entry("datamodel_attribute".to_string())
267 .or_insert_with(|| Value::String("null".into()));
268 } else if elem.name == "donedata" {
269 if let Some(Value::Array(arr)) = map.get_mut("content") {
270 if arr.len() == 1 {
271 if let Some(item) = arr.pop() {
272 map.insert("content".into(), item);
273 }
274 }
275 }
276 }
277 map
278}
279
280fn join_tokens(v: &Value) -> Option<String> {
281 match v {
282 Value::Array(arr) => {
283 if arr.iter().all(|x| x.is_string()) {
284 let parts: Vec<String> = arr
285 .iter()
286 .filter_map(|x| x.as_str().map(|s| s.to_string()))
287 .collect();
288 Some(parts.join(" "))
289 } else {
290 None
291 }
292 }
293 Value::String(s) => Some(s.clone()),
294 _ => None,
295 }
296}
297
298fn map_to_element(name: &str, map: &Map<String, Value>) -> Element {
299 if name == "scxml" && map.len() == 1 {
300 if let Some(Value::Array(arr)) = map.get("content") {
301 if arr.len() == 1 {
302 if let Some(Value::Object(obj)) = arr.get(0) {
303 return map_to_element("scxml", obj);
304 }
305 }
306 }
307 }
308 let mut elem_name = name.to_string();
309 if let Some(Value::String(q)) = map.get("qname") {
310 elem_name = q.clone();
311 }
312 let mut elem = Element::new(&elem_name);
313 if name == "scxml" {
314 elem.attributes
315 .insert("xmlns".into(), "http://www.w3.org/2005/07/scxml".into());
316 } else if !elem_name.contains(':')
317 && !elem_name.contains('{')
318 && !SCXML_ELEMS.contains(&elem_name.as_str())
319 {
320 elem.attributes.insert("xmlns".into(), String::new());
321 }
322 if let Some(Value::String(text)) = map.get("text") {
323 if !text.is_empty() {
324 elem.children.push(XMLNode::Text(text.clone()));
325 }
326 }
327 if let Some(Value::Object(attrs)) = map.get("attributes") {
328 for (k, v) in attrs {
329 if let Some(s) = v.as_str() {
330 elem.attributes.insert(k.clone(), s.to_string());
331 }
332 }
333 }
334 for (k, v) in map {
335 if ["qname", "text", "attributes"].contains(&k.as_str()) {
336 continue;
337 }
338 if k == "content" {
339 match v {
340 Value::Array(arr) => {
341 if name == "invoke" {
342 for item in arr {
343 match item {
344 Value::String(s) => {
345 let mut c = Element::new("content");
346 c.children.push(XMLNode::Text(s.clone()));
347 elem.children.push(XMLNode::Element(c));
348 }
349 Value::Object(obj) => {
350 let child_name = if obj.contains_key("state")
351 || obj.contains_key("final")
352 || obj.contains_key("version")
353 || obj.contains_key("datamodel_attribute")
354 {
355 "scxml"
356 } else {
357 "content"
358 };
359 let child = map_to_element(child_name, obj);
360 elem.children.push(XMLNode::Element(child));
361 }
362 _ => {}
363 }
364 }
365 } else if name == "script" {
366 for item in arr {
367 if let Value::String(s) = item {
368 elem.children.push(XMLNode::Text(s.clone()));
369 }
370 }
371 } else {
372 for item in arr {
373 match item {
374 Value::String(s) => elem.children.push(XMLNode::Text(s.clone())),
375 Value::Object(obj) => {
376 let child_name = if obj.contains_key("state")
377 || obj.contains_key("final")
378 || obj.contains_key("version")
379 || obj.contains_key("datamodel_attribute")
380 {
381 "scxml"
382 } else {
383 "content"
384 };
385 let child = map_to_element(child_name, obj);
386 elem.children.push(XMLNode::Element(child));
387 }
388 _ => {}
389 }
390 }
391 }
392 }
393 Value::Object(obj) => {
394 let child_name = if obj.contains_key("state")
395 || obj.contains_key("final")
396 || obj.contains_key("version")
397 || obj.contains_key("datamodel_attribute")
398 {
399 "scxml"
400 } else {
401 "content"
402 };
403 let child = map_to_element(child_name, obj);
404 elem.children.push(XMLNode::Element(child));
405 }
406 Value::String(s) => {
407 if name == "script" {
408 elem.children.push(XMLNode::Text(s.clone()));
409 } else {
410 let mut c = Element::new("content");
411 c.children.push(XMLNode::Text(s.clone()));
412 elem.children.push(XMLNode::Element(c));
413 }
414 }
415 _ => {}
416 }
417 continue;
418 }
419 if k.ends_with("_attribute") {
420 let attr = k.trim_end_matches("_attribute");
421 if let Some(val) = join_tokens(v) {
422 elem.attributes.insert(attr.into(), val);
423 }
424 continue;
425 }
426 if k == "datamodel_attribute" {
427 if let Some(val) = join_tokens(v) {
428 elem.attributes.insert("datamodel".into(), val);
429 }
430 continue;
431 }
432 if k == "type_value" {
433 if let Some(val) = join_tokens(v) {
434 elem.attributes.insert("type".into(), val);
435 }
436 continue;
437 }
438 if k == "raise_value" {
439 if let Some(val) = join_tokens(v) {
440 elem.attributes.insert("raise".into(), val);
441 continue;
442 }
443 }
444 if name == "transition" && k == "target" {
445 if let Some(val) = join_tokens(v) {
446 elem.attributes.insert("target".into(), val);
447 }
448 continue;
449 }
450 if k == "delay" || k == "event" || k == "initial" {
451 if let Some(val) = join_tokens(v) {
452 elem.attributes.insert(k.clone(), val);
453 continue;
454 }
455 }
456 if let Some(val) = join_tokens(v) {
457 elem.attributes.insert(k.clone(), val);
458 continue;
459 }
460 match v {
461 Value::Array(arr) => {
462 let child_name = match k.as_str() {
463 "if_value" => "if",
464 "else_value" => "else",
465 "raise_value" => "raise",
466 other => other,
467 };
468 for item in arr {
469 if let Value::Object(obj) = item {
470 let child = map_to_element(child_name, obj);
471 elem.children.push(XMLNode::Element(child));
472 } else if let Value::String(text) = item {
473 elem.children
474 .push(XMLNode::Element(map_to_element(child_name, &Map::new())));
475 elem.children.push(XMLNode::Text(text.clone()));
476 }
477 }
478 }
479 Value::Object(obj) => {
480 let child_name = match k.as_str() {
481 "if_value" => "if",
482 "else_value" => "else",
483 "raise_value" => "raise",
484 other => other,
485 };
486 let child = map_to_element(child_name, obj);
487 elem.children.push(XMLNode::Element(child));
488 }
489 Value::String(s) => {
490 if k == "version" {
491 elem.attributes.insert("version".into(), s.clone());
492 } else {
493 elem.children
494 .push(XMLNode::Element(map_to_element(k, &Map::new())));
495 elem.children.push(XMLNode::Text(s.clone()));
496 }
497 }
498 Value::Number(n) => {
499 if k == "version" {
500 elem.attributes.insert("version".into(), n.to_string());
501 }
502 }
503 _ => {}
504 }
505 }
506 elem
507}
508
509fn collapse_whitespace(value: &mut Value) {
514 match value {
515 Value::Array(arr) => {
516 for v in arr {
517 collapse_whitespace(v);
518 }
519 }
520 Value::Object(map) => {
521 let keys: Vec<String> = map.keys().cloned().collect();
522 for k in keys {
523 if let Some(v) = map.get_mut(&k) {
524 if (k.ends_with("_attribute") || COLLAPSE_ATTRS.contains(&k.as_str()))
525 && v.is_string()
526 {
527 if let Some(s) = v.as_str() {
528 let collapsed = s.replace(['\n', '\r', '\t'], " ");
529 *v = Value::String(collapsed);
530 }
531 } else {
532 collapse_whitespace(v);
533 }
534 }
535 }
536 }
537 _ => {}
538 }
539}
540
541fn remove_empty(value: &mut Value) -> bool {
542 match value {
543 Value::Object(map) => {
544 let keys: Vec<String> = map.keys().cloned().collect();
545 for k in keys {
546 if let Some(v) = map.get_mut(&k) {
547 if remove_empty(v) {
548 map.remove(&k);
549 }
550 }
551 }
552 map.is_empty()
553 }
554 Value::Array(arr) => {
555 arr.retain(|v| {
556 let mut v = v.clone();
557 !remove_empty(&mut v)
558 });
559 arr.is_empty()
560 }
561 Value::Null => true,
562 Value::String(s) => s.is_empty(),
563 _ => false,
564 }
565}
566
567pub fn xml_to_json(xml: &str, omit_empty: bool) -> Result<String, ScjsonError> {
576 let root = Element::parse(xml.as_bytes())?;
577 if root.name != "scxml" {
578 return Err(ScjsonError::Unsupported);
579 }
580 let map = element_to_map(&root);
582 let mut value = Value::Object(map);
583 collapse_whitespace(&mut value);
584 if omit_empty {
585 remove_empty(&mut value);
586 }
587 Ok(serde_json::to_string_pretty(&value)?)
588}
589
590pub fn json_to_xml_opts(json_str: &str, omit_empty: bool) -> Result<String, ScjsonError> {
599 let mut v: Value = serde_json::from_str(json_str)?;
600 if omit_empty {
601 remove_empty(&mut v);
602 }
603 let obj = v.as_object().ok_or(ScjsonError::Unsupported)?;
604 let elem = map_to_element("scxml", obj);
605 let mut out = Vec::new();
606 elem.write(&mut out)?;
607 Ok(String::from_utf8(out).unwrap())
608}
609
610pub fn json_to_xml(json_str: &str) -> Result<String, ScjsonError> {
618 json_to_xml_opts(json_str, true)
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624
625 #[test]
626 fn round_trip_simple() {
627 let xml = "<scxml xmlns=\"http://www.w3.org/2005/07/scxml\"/>";
628 let json = xml_to_json(xml, true).unwrap();
629 assert!(json.contains("version"));
630 let xml_rt = json_to_xml(&json).unwrap();
631 assert!(xml_rt.contains("scxml"));
632 }
633}