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