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, reader.decoder())?;
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, 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
226fn 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 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
284fn 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("&"),
362 '<' => out.push_str("<"),
363 '>' => out.push_str(">"),
364 '"' => out.push_str("""),
365 '\'' => out.push_str("'"),
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 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>&<></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="&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 #[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("<&>"'"));
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 #[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 #[test]
976 fn xml_to_json_unicode_element_names() {
977 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}