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