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