Skip to main content

camel_api/
xml_convert.rs

1use crate::error::CamelError;
2use quick_xml::Reader;
3use quick_xml::XmlVersion;
4use quick_xml::events::Event;
5
6fn check_root_element(depth: usize, root_count: &mut usize) -> Result<(), CamelError> {
7    if depth == 0 {
8        *root_count += 1;
9        if *root_count > 1 {
10            return Err(CamelError::TypeConversionFailed(
11                "multiple root elements found".into(),
12            ));
13        }
14    }
15    Ok(())
16}
17
18/// Validate that the input is well-formed XML.
19///
20/// This performs syntax validation only.
21pub fn validate_xml(input: &str) -> Result<(), CamelError> {
22    let mut reader = Reader::from_str(input);
23    reader.config_mut().trim_text(true);
24    let mut buf = Vec::new();
25    let mut depth = 0usize;
26    let mut root_count = 0usize;
27
28    loop {
29        match reader.read_event_into(&mut buf) {
30            Ok(Event::Start(_)) => {
31                check_root_element(depth, &mut root_count)?;
32                depth += 1;
33            }
34            Ok(Event::Empty(_)) => {
35                check_root_element(depth, &mut root_count)?;
36            }
37            Ok(Event::End(_)) => {
38                depth = depth.saturating_sub(1);
39            }
40            Ok(Event::DocType(_)) => {
41                return Err(CamelError::TypeConversionFailed(
42                    "DOCTYPE is not allowed in XML body".into(),
43                ));
44            }
45            Ok(Event::Eof) => break,
46            Err(e) => {
47                return Err(CamelError::TypeConversionFailed(format!(
48                    "invalid XML at position {}: {e}",
49                    reader.error_position()
50                )));
51            }
52            // PI events (<?target ...?>) are allowed — they cannot carry
53            // external entity references. The XML declaration (<?xml ...?>)
54            // is emitted as Event::Decl, not Event::PI.
55            _ => {}
56        }
57        buf.clear();
58    }
59
60    if root_count == 0 {
61        return Err(CamelError::TypeConversionFailed(
62            "empty XML: no root element found".into(),
63        ));
64    }
65
66    Ok(())
67}
68
69/// Convert an XML string to a JSON value.
70///
71/// Whitespace in text content is **trimmed** (leading/trailing) consistently with
72/// Apache Camel XJ behavior. For example, `<name> Alice </name>` produces
73/// `{"name": "Alice"}` — the surrounding spaces are removed. Indentation whitespace
74/// between elements is also ignored via `trim_text(true)` on the reader.
75///
76/// # Errors
77/// Returns `CamelError::TypeConversionFailed` if the input is not well-formed XML,
78/// contains multiple root elements, or is empty.
79pub fn xml_to_json(input: &str) -> Result<serde_json::Value, CamelError> {
80    let mut reader = Reader::from_str(input);
81    reader.config_mut().trim_text(true);
82
83    let mut stack: Vec<XmlNode> = Vec::new();
84    let mut got_root = false;
85    let mut result: Option<serde_json::Value> = None;
86
87    loop {
88        match reader.read_event() {
89            Ok(Event::Start(e)) => {
90                if result.is_some() {
91                    return Err(CamelError::TypeConversionFailed(
92                        "multiple root elements found".into(),
93                    ));
94                }
95                got_root = true;
96                let name = local_name(&e);
97                let attrs = parse_attrs(&e, reader.decoder())?;
98                stack.push(XmlNode {
99                    name,
100                    attrs,
101                    children: serde_json::Map::new(),
102                    text: String::new(),
103                });
104            }
105            Ok(Event::Empty(e)) => {
106                if result.is_some() {
107                    return Err(CamelError::TypeConversionFailed(
108                        "multiple root elements found".into(),
109                    ));
110                }
111                got_root = true;
112                let name = local_name(&e);
113                let attrs = parse_attrs(&e, reader.decoder())?;
114                let value = if attrs.is_empty() {
115                    serde_json::Value::Null
116                } else {
117                    serde_json::Value::Object(attrs)
118                };
119                if let Some(parent) = stack.last_mut() {
120                    insert_child(&mut parent.children, name, value);
121                } else {
122                    result = Some(serde_json::Value::Object(single_entry_map(name, value)));
123                }
124            }
125            Ok(Event::Text(e)) => {
126                let raw = String::from_utf8(e.to_vec()).map_err(|err| {
127                    CamelError::TypeConversionFailed(format!("invalid UTF-8 in XML text: {err}"))
128                })?;
129                let text = quick_xml::escape::unescape(&raw).map_err(|err| {
130                    CamelError::TypeConversionFailed(format!("cannot unescape XML text: {err}"))
131                })?;
132                if let Some(node) = stack.last_mut() {
133                    node.text.push_str(&text);
134                }
135            }
136            Ok(Event::GeneralRef(e)) => {
137                let ref_name = String::from_utf8(e.to_vec()).map_err(|err| {
138                    CamelError::TypeConversionFailed(format!("invalid UTF-8 in XML ref: {err}"))
139                })?;
140                let escaped = format!("&{ref_name};");
141                let text = quick_xml::escape::unescape(&escaped).map_err(|err| {
142                    CamelError::TypeConversionFailed(format!(
143                        "cannot unescape XML ref &{ref_name};: {err}"
144                    ))
145                })?;
146                if let Some(node) = stack.last_mut() {
147                    node.text.push_str(&text);
148                }
149            }
150            Ok(Event::CData(e)) => {
151                let text = String::from_utf8_lossy(e.as_ref()).into_owned();
152                if let Some(node) = stack.last_mut() {
153                    node.text.push_str(&text);
154                }
155            }
156            Ok(Event::End(_)) => {
157                let node = stack.pop().ok_or_else(|| {
158                    CamelError::TypeConversionFailed("unexpected closing tag".into())
159                })?;
160                let name = node.name.clone();
161                let value = build_node_value(node);
162                if let Some(parent) = stack.last_mut() {
163                    insert_child(&mut parent.children, name, value);
164                } else {
165                    result = Some(serde_json::Value::Object(single_entry_map(name, value)));
166                }
167            }
168            Ok(Event::Eof) => {
169                if !got_root {
170                    return Err(CamelError::TypeConversionFailed(
171                        "empty XML: no root element found".into(),
172                    ));
173                }
174                if let Some(res) = result {
175                    return Ok(res);
176                }
177                break;
178            }
179            Err(e) => {
180                return Err(CamelError::TypeConversionFailed(format!(
181                    "invalid XML at position {}: {e}",
182                    reader.error_position()
183                )));
184            }
185            _ => {}
186        }
187    }
188
189    Err(CamelError::TypeConversionFailed(
190        "unexpected end of XML input".into(),
191    ))
192}
193
194/// Validate that a string is a valid XML element/attribute name per the XML Name production.
195///
196/// Uses Unicode-aware checks: NameStartChar accepts any Unicode alphabetic character,
197/// `_`, or `:`. NameChar accepts any Unicode alphanumeric, `_`, `-`, `.`, or `:`.
198/// This is intentionally permissive — invalid names are ultimately rejected by
199/// `quick-xml` when writing.
200fn is_valid_xml_name(name: &str) -> bool {
201    let mut chars = name.chars();
202    match chars.next() {
203        Some(c) if c.is_alphabetic() || c == '_' || c == ':' => {}
204        _ => return false,
205    }
206    chars.all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.' || c == ':')
207}
208
209pub fn json_to_xml(value: &serde_json::Value) -> Result<String, CamelError> {
210    let obj = value.as_object().ok_or_else(|| {
211        CamelError::TypeConversionFailed(
212            "cannot convert to XML: top-level value must be a JSON object".into(),
213        )
214    })?;
215
216    // Filter out special keys (@attr, #text) to find actual element keys
217    let element_keys: Vec<&String> = obj
218        .keys()
219        .filter(|k| !k.starts_with('@') && **k != "#text")
220        .collect();
221
222    if element_keys.is_empty() {
223        return Err(CamelError::TypeConversionFailed(
224            "cannot convert to XML: JSON object must contain exactly one root element".into(),
225        ));
226    }
227    if element_keys.len() > 1 {
228        return Err(CamelError::TypeConversionFailed(format!(
229            "cannot convert to XML: expected exactly one root element, found {} ({})",
230            element_keys.len(),
231            element_keys
232                .iter()
233                .map(|k| k.as_str())
234                .collect::<Vec<_>>()
235                .join(", ")
236        )));
237    }
238
239    let root_key = element_keys[0];
240    if !is_valid_xml_name(root_key) {
241        return Err(CamelError::TypeConversionFailed(format!(
242            "invalid XML element name: {root_key:?}"
243        )));
244    }
245
246    let child = &obj[root_key];
247    let mut output = String::new();
248    serialize_node(&mut output, root_key, child)?;
249    Ok(output)
250}
251
252/// Convert any JSON value to its string representation for XML serialization.
253fn value_as_str(val: &serde_json::Value) -> String {
254    match val {
255        serde_json::Value::String(s) => s.clone(),
256        serde_json::Value::Number(n) => n.to_string(),
257        serde_json::Value::Bool(b) => b.to_string(),
258        serde_json::Value::Null => String::new(),
259        serde_json::Value::Array(_) | serde_json::Value::Object(_) => val.to_string(),
260    }
261}
262
263fn serialize_node(
264    output: &mut String,
265    tag: &str,
266    value: &serde_json::Value,
267) -> Result<(), CamelError> {
268    if !is_valid_xml_name(tag) {
269        return Err(CamelError::TypeConversionFailed(format!(
270            "invalid XML element name: {tag:?}"
271        )));
272    }
273    match value {
274        serde_json::Value::Null => {
275            output.push_str(&format!("<{tag}/>"));
276        }
277        serde_json::Value::String(s) => {
278            output.push_str(&format!("<{tag}>{}</{tag}>", escape_xml_text(s)));
279        }
280        serde_json::Value::Number(n) => {
281            output.push_str(&format!("<{tag}>{n}</{tag}>"));
282        }
283        serde_json::Value::Bool(b) => {
284            output.push_str(&format!("<{tag}>{b}</{tag}>"));
285        }
286        serde_json::Value::Array(arr) => {
287            for item in arr {
288                serialize_node(output, tag, item)?;
289            }
290        }
291        serde_json::Value::Object(map) => {
292            let mut attrs = String::new();
293            let mut children = String::new();
294            let mut text = String::new();
295
296            for (key, val) in map {
297                if let Some(attr_name) = key.strip_prefix('@') {
298                    if !is_valid_xml_name(attr_name) {
299                        return Err(CamelError::TypeConversionFailed(format!(
300                            "invalid XML attribute name: {attr_name:?}"
301                        )));
302                    }
303                    attrs.push_str(&format!(
304                        r#" {}="{}""#,
305                        attr_name,
306                        escape_xml_text(&value_as_str(val))
307                    ));
308                } else if key == "#text" {
309                    text = escape_xml_text(&value_as_str(val));
310                } else {
311                    serialize_node(&mut children, key, val)?;
312                }
313            }
314
315            if children.is_empty() && text.is_empty() {
316                output.push_str(&format!("<{tag}{attrs}/>"));
317            } else {
318                output.push_str(&format!("<{tag}{attrs}>{text}{children}</{tag}>"));
319            }
320        }
321    }
322    Ok(())
323}
324
325fn escape_xml_text(s: &str) -> String {
326    let mut out = String::with_capacity(s.len());
327    for c in s.chars() {
328        match c {
329            '&' => out.push_str("&amp;"),
330            '<' => out.push_str("&lt;"),
331            '>' => out.push_str("&gt;"),
332            '"' => out.push_str("&quot;"),
333            '\'' => out.push_str("&apos;"),
334            _ => out.push(c),
335        }
336    }
337    out
338}
339
340struct XmlNode {
341    name: String,
342    attrs: serde_json::Map<String, serde_json::Value>,
343    children: serde_json::Map<String, serde_json::Value>,
344    text: String,
345}
346
347fn local_name(e: &quick_xml::events::BytesStart<'_>) -> String {
348    String::from_utf8_lossy(e.local_name().as_ref()).into_owned()
349}
350
351fn parse_attrs(
352    e: &quick_xml::events::BytesStart<'_>,
353    decoder: quick_xml::Decoder,
354) -> Result<serde_json::Map<String, serde_json::Value>, CamelError> {
355    let mut map = serde_json::Map::new();
356    for attr_result in e.attributes() {
357        let attr = attr_result.map_err(|err| {
358            CamelError::TypeConversionFailed(format!("cannot parse attribute: {err}"))
359        })?;
360
361        let full_name = String::from_utf8_lossy(attr.key.as_ref());
362        if full_name == "xmlns" || full_name.starts_with("xmlns:") {
363            continue;
364        }
365
366        let key = format!(
367            "@{}",
368            String::from_utf8_lossy(attr.key.local_name().as_ref())
369        );
370        let val = attr
371            .decoded_and_normalized_value(XmlVersion::Implicit1_0, decoder)
372            .map_err(|err| {
373                CamelError::TypeConversionFailed(format!("cannot unescape attribute value: {err}"))
374            })?;
375        map.insert(key, serde_json::Value::String(val.to_string()));
376    }
377    Ok(map)
378}
379
380fn build_node_value(node: XmlNode) -> serde_json::Value {
381    let has_attrs = !node.attrs.is_empty();
382    let has_children = !node.children.is_empty();
383    let trimmed = node.text.trim();
384
385    if has_children {
386        let mut map = node.attrs;
387        if !trimmed.is_empty() {
388            map.insert(
389                "#text".to_string(),
390                serde_json::Value::String(trimmed.to_string()),
391            );
392        }
393        for (k, v) in node.children {
394            insert_child(&mut map, k, v);
395        }
396        serde_json::Value::Object(map)
397    } else if has_attrs {
398        let mut map = node.attrs;
399        if !trimmed.is_empty() {
400            map.insert(
401                "#text".to_string(),
402                serde_json::Value::String(trimmed.to_string()),
403            );
404        }
405        serde_json::Value::Object(map)
406    } else if trimmed.is_empty() {
407        serde_json::Value::Null
408    } else {
409        serde_json::Value::String(trimmed.to_string())
410    }
411}
412
413fn insert_child(
414    map: &mut serde_json::Map<String, serde_json::Value>,
415    name: String,
416    value: serde_json::Value,
417) {
418    match map.remove(&name) {
419        None => {
420            map.insert(name, value);
421        }
422        Some(serde_json::Value::Array(mut arr)) => {
423            arr.push(value);
424            map.insert(name, serde_json::Value::Array(arr));
425        }
426        Some(existing) => {
427            map.insert(name, serde_json::Value::Array(vec![existing, value]));
428        }
429    }
430}
431
432fn single_entry_map(
433    key: String,
434    value: serde_json::Value,
435) -> serde_json::Map<String, serde_json::Value> {
436    let mut m = serde_json::Map::new();
437    m.insert(key, value);
438    m
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444    use serde_json::json;
445
446    #[test]
447    fn simple_element() {
448        let xml = "<root><name>Alice</name></root>";
449        let result = xml_to_json(xml).unwrap();
450        assert_eq!(result, json!({"root": {"name": "Alice"}}));
451    }
452
453    #[test]
454    fn nested_elements() {
455        let xml = "<root><user><city>Madrid</city></user></root>";
456        let result = xml_to_json(xml).unwrap();
457        assert_eq!(result, json!({"root": {"user": {"city": "Madrid"}}}));
458    }
459
460    #[test]
461    fn repeated_siblings_become_array() {
462        let xml = "<root><item>a</item><item>b</item></root>";
463        let result = xml_to_json(xml).unwrap();
464        assert_eq!(result, json!({"root": {"item": ["a", "b"]}}));
465    }
466
467    #[test]
468    fn single_sibling_is_scalar() {
469        let xml = "<root><item>only</item></root>";
470        let result = xml_to_json(xml).unwrap();
471        assert_eq!(result, json!({"root": {"item": "only"}}));
472    }
473
474    #[test]
475    fn attributes_use_at_prefix() {
476        let xml = r#"<root id="123"><name>Alice</name></root>"#;
477        let result = xml_to_json(xml).unwrap();
478        assert_eq!(result, json!({"root": {"@id": "123", "name": "Alice"}}));
479    }
480
481    #[test]
482    fn text_with_attrs_uses_hash_text() {
483        let xml = r#"<root id="1">hello</root>"#;
484        let result = xml_to_json(xml).unwrap();
485        assert_eq!(result, json!({"root": {"@id": "1", "#text": "hello"}}));
486    }
487
488    #[test]
489    fn self_closing_no_attrs_is_null() {
490        let xml = "<root><empty/></root>";
491        let result = xml_to_json(xml).unwrap();
492        assert_eq!(result, json!({"root": {"empty": null}}));
493    }
494
495    #[test]
496    fn self_closing_with_attrs_is_object() {
497        let xml = r#"<root><link href="http://example.com"/></root>"#;
498        let result = xml_to_json(xml).unwrap();
499        assert_eq!(
500            result,
501            json!({"root": {"link": {"@href": "http://example.com"}}})
502        );
503    }
504
505    #[test]
506    fn text_with_children_uses_hash_text() {
507        let xml = "<root>hello<child>world</child></root>";
508        let result = xml_to_json(xml).unwrap();
509        assert_eq!(
510            result,
511            json!({"root": {"#text": "hello", "child": "world"}})
512        );
513    }
514
515    #[test]
516    fn repeated_siblings_with_attrs_become_array() {
517        let xml = r#"<root><item id="1">a</item><item id="2">b</item></root>"#;
518        let result = xml_to_json(xml).unwrap();
519        assert_eq!(
520            result,
521            json!({"root": {"item": [{"@id": "1", "#text": "a"}, {"@id": "2", "#text": "b"}]}})
522        );
523    }
524
525    #[test]
526    fn parent_with_only_child_elements_no_hash_text() {
527        let xml = "<person><name>John</name><age>30</age></person>";
528        let result = xml_to_json(xml).unwrap();
529        assert_eq!(result, json!({"person": {"name": "John", "age": "30"}}));
530    }
531
532    #[test]
533    fn invalid_xml_returns_error() {
534        let result = xml_to_json("not xml <unclosed");
535        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
536    }
537
538    #[test]
539    fn empty_string_returns_error() {
540        let result = xml_to_json("");
541        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
542    }
543
544    #[test]
545    fn validate_xml_valid() {
546        assert!(validate_xml("<root/>").is_ok());
547    }
548
549    #[test]
550    fn validate_xml_rejects_doctype() {
551        let result = validate_xml("<!DOCTYPE root><root/>");
552        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
553    }
554
555    #[test]
556    fn validate_xml_rejects_multiple_roots() {
557        let result = validate_xml("<a/><b/>");
558        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
559    }
560
561    #[test]
562    fn validate_xml_rejects_empty() {
563        let result = validate_xml("");
564        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
565    }
566
567    #[test]
568    fn validate_xml_rejects_whitespace_only() {
569        let result = validate_xml("   \n\t  ");
570        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
571    }
572
573    #[test]
574    fn validate_xml_accepts_prolog() {
575        assert!(validate_xml(r#"<?xml version=\"1.0\"?><root/>"#).is_ok());
576    }
577
578    #[test]
579    fn validate_xml_rejects_prolog_only() {
580        let result = validate_xml(r#"<?xml version=\"1.0\"?>"#);
581        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
582    }
583
584    #[test]
585    fn xml_prolog_accepted() {
586        let xml = r#"<?xml version="1.0"?><root><a>1</a></root>"#;
587        let result = xml_to_json(xml).unwrap();
588        assert_eq!(result, json!({"root": {"a": "1"}}));
589    }
590
591    #[test]
592    fn complex_nested_with_arrays_and_attrs() {
593        let xml = r#"<order id="123">
594            <item>coffee</item>
595            <item>tea</item>
596            <status active="true">pending</status>
597        </order>"#;
598        let result = xml_to_json(xml).unwrap();
599        assert_eq!(
600            result,
601            json!({
602                "order": {
603                    "@id": "123",
604                    "item": ["coffee", "tea"],
605                    "status": {"@active": "true", "#text": "pending"}
606                }
607            })
608        );
609    }
610
611    #[test]
612    fn cdata_treated_as_text() {
613        let xml = "<root><msg><![CDATA[hello <world>]]></msg></root>";
614        let result = xml_to_json(xml).unwrap();
615        assert_eq!(result, json!({"root": {"msg": "hello <world>"}}));
616    }
617
618    #[test]
619    fn comments_ignored() {
620        let xml = "<root><!-- a comment --><a>1</a></root>";
621        let result = xml_to_json(xml).unwrap();
622        assert_eq!(result, json!({"root": {"a": "1"}}));
623    }
624
625    #[test]
626    fn whitespace_text_around_children_not_included() {
627        let xml = "<root>\n  <a>1</a>\n</root>";
628        let result = xml_to_json(xml).unwrap();
629        assert_eq!(result, json!({"root": {"a": "1"}}));
630    }
631
632    #[test]
633    fn test_whitespace_trimmed() {
634        // Documents that leading/trailing whitespace in text content is trimmed,
635        // consistent with Apache Camel XJ behavior.
636        let xml = "<name> Alice </name>";
637        let result = xml_to_json(xml).unwrap();
638        assert_eq!(result, json!({"name": "Alice"}));
639    }
640
641    #[test]
642    fn xml_entity_escaping_decoded() {
643        let xml = "<root><a>&amp;&lt;&gt;</a></root>";
644        let result = xml_to_json(xml).unwrap();
645        assert_eq!(result, json!({"root": {"a": "&<>"}}));
646    }
647
648    #[test]
649    fn attribute_entity_escaping_decoded() {
650        let xml = r#"<root a="&amp;val"/>"#;
651        let result = xml_to_json(xml).unwrap();
652        assert_eq!(result, json!({"root": {"@a": "&val"}}));
653    }
654
655    #[test]
656    fn self_closing_root() {
657        let xml = "<root/>";
658        let result = xml_to_json(xml).unwrap();
659        assert_eq!(result, json!({"root": null}));
660    }
661
662    #[test]
663    fn multiple_root_elements_returns_error() {
664        let result = xml_to_json("<a/><b/>");
665        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
666    }
667
668    #[test]
669    fn default_namespace_filtered() {
670        let xml = r#"<root xmlns="http://example.com"><a>1</a></root>"#;
671        let result = xml_to_json(xml).unwrap();
672        assert_eq!(result, json!({"root": {"a": "1"}}));
673    }
674
675    #[test]
676    fn prefixed_namespace_filtered() {
677        let xml = r#"<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><a>1</a></root>"#;
678        let result = xml_to_json(xml).unwrap();
679        assert_eq!(result, json!({"root": {"a": "1"}}));
680    }
681
682    #[test]
683    fn multiple_namespaces_filtered() {
684        let xml = r#"<root xmlns="http://default.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema"><a>1</a></root>"#;
685        let result = xml_to_json(xml).unwrap();
686        assert_eq!(result, json!({"root": {"a": "1"}}));
687    }
688
689    #[test]
690    fn mixed_namespace_and_regular_attrs() {
691        let xml = r#"<root xmlns="http://example.com" id="123"><a>1</a></root>"#;
692        let result = xml_to_json(xml).unwrap();
693        assert_eq!(result, json!({"root": {"@id": "123", "a": "1"}}));
694    }
695
696    #[test]
697    fn namespace_like_regular_attr_preserved() {
698        let xml = r#"<root xmlnsAttribute="value"><a>1</a></root>"#;
699        let result = xml_to_json(xml).unwrap();
700        assert_eq!(
701            result,
702            json!({"root": {"@xmlnsAttribute": "value", "a": "1"}})
703        );
704    }
705
706    #[test]
707    fn prefixed_element_names_stripped() {
708        let xml = "<ns:root><ns:a>1</ns:a></ns:root>";
709        let result = xml_to_json(xml).unwrap();
710        assert_eq!(result, json!({"root": {"a": "1"}}));
711    }
712
713    // --- json_to_xml tests (Task 2) ---
714
715    #[test]
716    fn json_to_xml_simple_object() {
717        let json = json!({"root": {"name": "Alice"}});
718        let result = json_to_xml(&json).unwrap();
719        assert_eq!(result, "<root><name>Alice</name></root>");
720    }
721
722    #[test]
723    fn json_to_xml_array() {
724        let json = json!({"root": {"item": ["a", "b"]}});
725        let result = json_to_xml(&json).unwrap();
726        assert_eq!(result, "<root><item>a</item><item>b</item></root>");
727    }
728
729    #[test]
730    fn json_to_xml_attributes() {
731        let json = json!({"root": {"@id": "123", "name": "Alice"}});
732        let result = json_to_xml(&json).unwrap();
733        assert!(result.contains(r#" id="123""#));
734        assert!(result.contains("<name>Alice</name>"));
735    }
736
737    #[test]
738    fn json_to_xml_null_element() {
739        let json = json!({"root": {"empty": null}});
740        let result = json_to_xml(&json).unwrap();
741        assert_eq!(result, "<root><empty/></root>");
742    }
743
744    #[test]
745    fn json_to_xml_hash_text() {
746        let json = json!({"root": {"@id": "1", "#text": "hello"}});
747        let result = json_to_xml(&json).unwrap();
748        assert!(result.contains(r#" id="1""#));
749        assert!(result.contains(">hello</root>"));
750    }
751
752    #[test]
753    fn json_to_xml_nested() {
754        let json = json!({"root": {"user": {"city": "Madrid"}}});
755        let result = json_to_xml(&json).unwrap();
756        assert_eq!(result, "<root><user><city>Madrid</city></user></root>");
757    }
758
759    #[test]
760    fn json_to_xml_non_object_returns_error() {
761        let json = json!("just a string");
762        let result = json_to_xml(&json);
763        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
764    }
765
766    #[test]
767    fn json_to_xml_array_with_attrs() {
768        let json =
769            json!({"root": {"item": [{"@id": "1", "#text": "a"}, {"@id": "2", "#text": "b"}]}});
770        let result = json_to_xml(&json).unwrap();
771        assert!(result.contains(r#" id="1""#));
772        assert!(result.contains(r#" id="2""#));
773        assert!(result.contains(">a<"));
774        assert!(result.contains(">b<"));
775    }
776
777    #[test]
778    fn json_to_xml_number_value() {
779        let json = json!({"root": {"count": 42}});
780        let result = json_to_xml(&json).unwrap();
781        assert!(result.contains("<count>42</count>"));
782    }
783
784    #[test]
785    fn json_to_xml_bool_value() {
786        let json = json!({"root": {"active": true}});
787        let result = json_to_xml(&json).unwrap();
788        assert!(result.contains("<active>true</active>"));
789    }
790
791    #[test]
792    fn json_to_xml_escapes_special_chars() {
793        let json = json!({"root": {"a": "<&>\"'"}});
794        let result = json_to_xml(&json).unwrap();
795        assert!(result.contains("&lt;&amp;&gt;&quot;&apos;"));
796    }
797
798    #[test]
799    fn json_to_xml_empty_object_becomes_self_closing() {
800        let json = json!({"root": {"empty": {}}});
801        let result = json_to_xml(&json).unwrap();
802        assert!(result.contains("<empty/>"));
803    }
804
805    #[test]
806    fn json_to_xml_number_as_attr() {
807        let json = json!({"root": {"@count": 42, "#text": "hello"}});
808        let result = json_to_xml(&json).unwrap();
809        assert!(result.contains(r#" count="42""#));
810        assert!(result.contains(">hello</root>"));
811    }
812
813    #[test]
814    fn json_to_xml_bool_as_attr() {
815        let json = json!({"root": {"@active": true, "#text": "data"}});
816        let result = json_to_xml(&json).unwrap();
817        assert!(result.contains(r#" active="true""#));
818    }
819
820    #[test]
821    fn json_to_xml_number_as_text() {
822        let json = json!({"root": {"@id": "1", "#text": 42}});
823        let result = json_to_xml(&json).unwrap();
824        assert!(result.contains(r#" id="1""#));
825        assert!(result.contains(">42</root>"));
826    }
827
828    #[test]
829    fn json_to_xml_bool_as_text() {
830        let json = json!({"root": {"#text": true}});
831        let result = json_to_xml(&json).unwrap();
832        assert!(result.contains(">true</root>"));
833    }
834
835    // --- json_to_xml validation tests ---
836
837    #[test]
838    fn json_to_xml_multiple_roots_returns_error() {
839        let json = json!({"root1": {"a": "1"}, "root2": {"b": "2"}});
840        let result = json_to_xml(&json);
841        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
842        let err = result.unwrap_err().to_string();
843        assert!(err.contains("exactly one root element"));
844        assert!(err.contains("root1"));
845        assert!(err.contains("root2"));
846    }
847
848    #[test]
849    fn json_to_xml_empty_object_returns_error() {
850        let json = json!({});
851        let result = json_to_xml(&json);
852        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
853    }
854
855    #[test]
856    fn json_to_xml_only_attrs_returns_error() {
857        let json = json!({"@id": "1", "#text": "hello"});
858        let result = json_to_xml(&json);
859        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
860    }
861
862    #[test]
863    fn json_to_xml_invalid_element_name_space() {
864        let json = json!({"my element": {"a": "1"}});
865        let result = json_to_xml(&json);
866        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
867        let err = result.unwrap_err().to_string();
868        assert!(err.contains("invalid XML element name"));
869    }
870
871    #[test]
872    fn json_to_xml_invalid_element_name_starts_with_digit() {
873        let json = json!({"123abc": {"a": "1"}});
874        let result = json_to_xml(&json);
875        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
876    }
877
878    #[test]
879    fn json_to_xml_invalid_element_name_special_chars() {
880        let json = json!({"<script>": {"a": "1"}});
881        let result = json_to_xml(&json);
882        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
883    }
884
885    #[test]
886    fn json_to_xml_invalid_child_element_name() {
887        let json = json!({"root": {"bad name": "value"}});
888        let result = json_to_xml(&json);
889        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
890    }
891
892    #[test]
893    fn json_to_xml_invalid_attribute_name() {
894        let json = json!({"root": {"@bad attr": "value"}});
895        let result = json_to_xml(&json);
896        assert!(matches!(result, Err(CamelError::TypeConversionFailed(_))));
897    }
898
899    #[test]
900    fn json_to_xml_valid_names_with_hyphens_and_underscores() {
901        let json = json!({"my-root": {"child_element": {"sub-item": "val"}}});
902        let result = json_to_xml(&json).unwrap();
903        assert!(result.contains("<my-root>"));
904        assert!(result.contains("<child_element>"));
905        assert!(result.contains("<sub-item>"));
906    }
907
908    // --- Unicode element name tests ---
909
910    #[test]
911    fn xml_to_json_unicode_element_names() {
912        // Unicode names are valid XML NameStartChar (alphabetic in Unicode)
913        let xml = "<café><nombre>María</nombre></café>";
914        let result = xml_to_json(xml).unwrap();
915        assert_eq!(result, json!({"café": {"nombre": "María"}}));
916    }
917
918    #[test]
919    fn xml_to_json_unicode_cjk_element_names() {
920        let xml = "<日本語><値>テスト</値></日本語>";
921        let result = xml_to_json(xml).unwrap();
922        assert_eq!(result, json!({"日本語": {"値": "テスト"}}));
923    }
924
925    #[test]
926    fn xml_to_json_unicode_spanish_element_names() {
927        let xml = "<ñamapa><dirección>Calle Mayor</dirección></ñamapa>";
928        let result = xml_to_json(xml).unwrap();
929        assert_eq!(result, json!({"ñamapa": {"dirección": "Calle Mayor"}}));
930    }
931
932    #[test]
933    fn json_to_xml_unicode_element_names() {
934        let json = json!({"café": {"nombre": "María"}});
935        let result = json_to_xml(&json).unwrap();
936        assert!(result.contains("<café>"));
937        assert!(result.contains("<nombre>María</nombre>"));
938    }
939
940    #[test]
941    fn json_to_xml_unicode_cjk_element_names() {
942        let json = json!({"日本語": {"値": "テスト"}});
943        let result = json_to_xml(&json).unwrap();
944        assert!(result.contains("<日本語>"));
945        assert!(result.contains("<値>テスト</値>"));
946    }
947}