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