1use crate::error::CamelError;
2use quick_xml::Reader;
3use quick_xml::XmlVersion;
4use quick_xml::events::Event;
5
6pub 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
22pub 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 _ => {}
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
73pub 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
87pub 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 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 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 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 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
225fn 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 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
283fn 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("&"),
361 '<' => out.push_str("<"),
362 '>' => out.push_str(">"),
363 '"' => out.push_str("""),
364 '\'' => out.push_str("'"),
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 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>&<></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="&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 #[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("<&>"'"));
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 #[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 #[test]
971 fn xml_to_json_unicode_element_names() {
972 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}