Skip to main content

camel_api/
xml_convert.rs

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