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