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