scjson/
lib.rs

1/*!
2"""
3Agent Name: rust-lib
4
5Part of the scjson project.
6Developed by Softoboros Technology Inc.
7Licensed under the BSD 1-Clause License.
8"""
9*/
10
11//! Library providing basic SCXML <-> scjson conversion.
12
13use serde_json::{Map, Number, Value};
14use thiserror::Error;
15use xmltree::Error as XmlWriteError;
16use xmltree::{Element, XMLNode};
17
18pub mod scjson_props;
19
20/// Attribute name mappings used during conversion.
21// const ATTRIBUTE_MAP: &[(&str, &str)] = &[
22//     ("datamodel", "datamodel_attribute"),
23//     ("initial", "initial_attribute"),
24//     ("type", "type_value"),
25//     ("raise", "raise_value"),
26// ];
27// NOTE: reserved for future use when attribute renaming is implemented.
28
29/// Keys that should always be arrays in the output.
30// const ARRAY_KEYS: &[&str] = &[
31//     "assign",
32//     "cancel",
33//     "content",
34//     "data",
35//     "datamodel",
36//     "donedata",
37//     "final",
38//     "finalize",
39//     "foreach",
40//     "history",
41//     "if_value",
42//     "initial",
43//     "invoke",
44//     "log",
45//     "onentry",
46//     "onexit",
47//     "other_element",
48//     "parallel",
49//     "param",
50//     "raise_value",
51//     "script",
52//     "send",
53//     "state",
54// ];
55// NOTE: may be reintroduced when enforcing array types during parsing.
56
57/// Attributes whose whitespace should be collapsed.
58const COLLAPSE_ATTRS: &[&str] = &[
59    "expr", "cond", "event", "target", "delay", "location", "name", "src", "id",
60];
61
62/// Known SCXML element names used for conversion.
63const 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/// Errors produced by conversion routines.
93#[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
509/// Collapse newlines and tabs in attribute values recursively.
510///
511/// # Parameters
512/// - `value`: Mutable JSON value to normalise.
513fn 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
567/// Convert an SCXML string to scjson.
568///
569/// # Parameters
570/// - `xml`: XML input string.
571/// - `omit_empty`: Remove empty fields when `true`.
572///
573/// # Returns
574/// JSON string representing the document.
575pub 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 mut map = element_to_map(&root); // retained for potential future mutations
581    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
590/// Convert a scjson string to SCXML using options.
591///
592/// # Parameters
593/// - `json_str`: JSON input string.
594/// - `omit_empty`: Remove empty fields when `true`.
595///
596/// # Returns
597/// XML string representing the document.
598pub 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
610/// Convert a scjson string to SCXML.
611///
612/// # Parameters
613/// - `json_str`: JSON input string.
614///
615/// # Returns
616/// XML string representing the document.
617pub 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}