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