1use hedl_core::lex::Tensor;
21use hedl_core::{Document, Item, MatrixList, Node, Value};
22use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
23use quick_xml::Writer;
24use std::collections::BTreeMap;
25use std::io::Cursor;
26
27#[derive(Debug, Clone)]
29pub struct ToXmlConfig {
30 pub pretty: bool,
32 pub indent: String,
34 pub root_element: String,
36 pub include_metadata: bool,
38 pub use_attributes: bool,
40}
41
42impl Default for ToXmlConfig {
43 fn default() -> Self {
44 Self {
45 pretty: true,
46 indent: " ".to_string(),
47 root_element: "hedl".to_string(),
48 include_metadata: false,
49 use_attributes: false,
50 }
51 }
52}
53
54impl hedl_core::convert::ExportConfig for ToXmlConfig {
55 fn include_metadata(&self) -> bool {
56 self.include_metadata
57 }
58
59 fn pretty(&self) -> bool {
60 self.pretty
61 }
62}
63
64pub fn to_xml(doc: &Document, config: &ToXmlConfig) -> Result<String, String> {
66 let mut writer = if config.pretty {
67 Writer::new_with_indent(Cursor::new(Vec::new()), b' ', config.indent.len())
69 } else {
70 Writer::new(Cursor::new(Vec::new()))
71 };
72
73 writer
75 .write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))
76 .map_err(|e| format!("Failed to write XML declaration: {}", e))?;
77
78 let mut root = BytesStart::new(&config.root_element);
80 if config.include_metadata {
81 root.push_attribute((
82 "version",
83 format!("{}.{}", doc.version.0, doc.version.1).as_str(),
84 ));
85 }
86 writer
87 .write_event(Event::Start(root))
88 .map_err(|e| format!("Failed to write root element: {}", e))?;
89
90 write_root(&mut writer, &doc.root, config, &doc.structs)?;
92
93 writer
95 .write_event(Event::End(BytesEnd::new(&config.root_element)))
96 .map_err(|e| format!("Failed to close root element: {}", e))?;
97
98 let result = writer.into_inner().into_inner();
99 String::from_utf8(result).map_err(|e| format!("Invalid UTF-8 in XML output: {}", e))
100}
101
102fn write_root<W: std::io::Write>(
103 writer: &mut Writer<W>,
104 root: &BTreeMap<String, Item>,
105 config: &ToXmlConfig,
106 structs: &BTreeMap<String, Vec<String>>,
107) -> Result<(), String> {
108 for (key, item) in root {
109 write_item(writer, key, item, config, structs)?;
110 }
111 Ok(())
112}
113
114fn write_item<W: std::io::Write>(
115 writer: &mut Writer<W>,
116 key: &str,
117 item: &Item,
118 config: &ToXmlConfig,
119 structs: &BTreeMap<String, Vec<String>>,
120) -> Result<(), String> {
121 match item {
122 Item::Scalar(value) => write_scalar_element(writer, key, value, config)?,
123 Item::Object(obj) => write_object(writer, key, obj, config, structs)?,
124 Item::List(list) => write_matrix_list(writer, key, list, config, structs)?,
125 }
126 Ok(())
127}
128
129fn write_scalar_element<W: std::io::Write>(
130 writer: &mut Writer<W>,
131 key: &str,
132 value: &Value,
133 config: &ToXmlConfig,
134) -> Result<(), String> {
135 let mut elem = BytesStart::new(key);
136
137 if matches!(value, Value::Reference(_)) {
139 elem.push_attribute(("__hedl_type__", "ref"));
140 }
141
142 if config.use_attributes && is_simple_value(value) {
144 elem.push_attribute(("value", escape_attribute_value(value).as_str()));
145 writer
146 .write_event(Event::Empty(elem))
147 .map_err(|e| format!("Failed to write empty element: {}", e))?;
148 } else {
149 writer
150 .write_event(Event::Start(elem.clone()))
151 .map_err(|e| format!("Failed to write start element: {}", e))?;
152
153 write_value_content(writer, value, config)?;
154
155 writer
156 .write_event(Event::End(BytesEnd::new(key)))
157 .map_err(|e| format!("Failed to write end element: {}", e))?;
158 }
159
160 Ok(())
161}
162
163fn write_value_content<W: std::io::Write>(
164 writer: &mut Writer<W>,
165 value: &Value,
166 config: &ToXmlConfig,
167) -> Result<(), String> {
168 match value {
169 Value::Null => {
170 }
172 Value::Bool(b) => write_text(writer, &b.to_string())?,
173 Value::Int(n) => write_text(writer, &n.to_string())?,
174 Value::Float(f) => write_text(writer, &f.to_string())?,
175 Value::String(s) => write_text(writer, s)?,
176 Value::Tensor(t) => write_tensor(writer, t, config)?,
177 Value::Reference(r) => write_text(writer, &r.to_ref_string())?,
178 Value::Expression(e) => write_text(writer, &format!("$({})", e))?,
179 Value::List(items) => write_list(writer, items, config)?,
180 }
181 Ok(())
182}
183
184fn write_object<W: std::io::Write>(
185 writer: &mut Writer<W>,
186 key: &str,
187 obj: &BTreeMap<String, Item>,
188 config: &ToXmlConfig,
189 structs: &BTreeMap<String, Vec<String>>,
190) -> Result<(), String> {
191 let elem = BytesStart::new(key);
192 writer
193 .write_event(Event::Start(elem))
194 .map_err(|e| format!("Failed to write object start: {}", e))?;
195
196 for (child_key, child_item) in obj {
197 write_item(writer, child_key, child_item, config, structs)?;
198 }
199
200 writer
201 .write_event(Event::End(BytesEnd::new(key)))
202 .map_err(|e| format!("Failed to write object end: {}", e))?;
203
204 Ok(())
205}
206
207fn write_matrix_list<W: std::io::Write>(
208 writer: &mut Writer<W>,
209 key: &str,
210 list: &MatrixList,
211 config: &ToXmlConfig,
212 structs: &BTreeMap<String, Vec<String>>,
213) -> Result<(), String> {
214 let mut list_elem = BytesStart::new(key);
215 if config.include_metadata {
216 list_elem.push_attribute(("type", list.type_name.as_str()));
217 }
218
219 writer
220 .write_event(Event::Start(list_elem))
221 .map_err(|e| format!("Failed to write list start: {}", e))?;
222
223 let item_name = list.type_name.to_lowercase();
225 for row in &list.rows {
226 write_node(writer, &item_name, row, &list.schema, config, structs)?;
227 }
228
229 writer
230 .write_event(Event::End(BytesEnd::new(key)))
231 .map_err(|e| format!("Failed to write list end: {}", e))?;
232
233 Ok(())
234}
235
236fn write_node<W: std::io::Write>(
237 writer: &mut Writer<W>,
238 elem_name: &str,
239 node: &Node,
240 schema: &[String],
241 config: &ToXmlConfig,
242 structs: &BTreeMap<String, Vec<String>>,
243) -> Result<(), String> {
244 let mut elem = BytesStart::new(elem_name);
245
246 if config.use_attributes {
251 for (i, field) in node.fields.iter().enumerate() {
252 if is_simple_value(field) && i < schema.len() {
253 let attr_value = escape_attribute_value(field);
254 elem.push_attribute((schema[i].as_str(), attr_value.as_str()));
255 }
256 }
257 }
258
259 let has_complex_values = node.fields.iter().any(|v| !is_simple_value(v));
261 let has_children = node.children().map(|c| !c.is_empty()).unwrap_or(false);
262
263 if !config.use_attributes || has_complex_values || has_children {
264 writer
265 .write_event(Event::Start(elem))
266 .map_err(|e| format!("Failed to write node start: {}", e))?;
267
268 if !config.use_attributes {
272 for (i, field) in node.fields.iter().enumerate() {
274 if i < schema.len() {
275 write_scalar_element(writer, &schema[i], field, config)?;
276 }
277 }
278 } else if has_complex_values {
279 for (i, field) in node.fields.iter().enumerate() {
281 if i < schema.len() && !is_simple_value(field) {
282 write_scalar_element(writer, &schema[i], field, config)?;
283 }
284 }
285 }
286 if let Some(children) = node.children() {
290 for (child_type, child_nodes) in children {
291 for child in child_nodes {
292 let default_schema = vec!["id".to_string()];
294 let child_schema = structs
295 .get(child_type)
296 .map(|s| s.as_slice())
297 .unwrap_or(&default_schema);
298 write_child_node(writer, child_type, child, child_schema, config, structs)?;
299 }
300 }
301 }
302
303 writer
304 .write_event(Event::End(BytesEnd::new(elem_name)))
305 .map_err(|e| format!("Failed to write node end: {}", e))?;
306 } else {
307 writer
309 .write_event(Event::Empty(elem))
310 .map_err(|e| format!("Failed to write empty node: {}", e))?;
311 }
312
313 Ok(())
314}
315
316fn write_child_node<W: std::io::Write>(
318 writer: &mut Writer<W>,
319 elem_name: &str,
320 node: &Node,
321 schema: &[String],
322 config: &ToXmlConfig,
323 structs: &BTreeMap<String, Vec<String>>,
324) -> Result<(), String> {
325 let mut elem = BytesStart::new(elem_name);
326
327 elem.push_attribute(("__hedl_child__", "true"));
329
330 if config.use_attributes {
332 for (i, field) in node.fields.iter().enumerate() {
333 if is_simple_value(field) && i < schema.len() {
334 let attr_value = escape_attribute_value(field);
335 elem.push_attribute((schema[i].as_str(), attr_value.as_str()));
336 }
337 }
338 }
339
340 let has_complex_values = node.fields.iter().any(|v| !is_simple_value(v));
342 let has_children = node.children().map(|c| !c.is_empty()).unwrap_or(false);
343
344 if !config.use_attributes || has_complex_values || has_children {
345 writer
346 .write_event(Event::Start(elem))
347 .map_err(|e| format!("Failed to write child node start: {}", e))?;
348
349 if !config.use_attributes {
351 for (i, field) in node.fields.iter().enumerate() {
353 if i < schema.len() {
354 write_scalar_element(writer, &schema[i], field, config)?;
355 }
356 }
357 } else if has_complex_values {
358 for (i, field) in node.fields.iter().enumerate() {
360 if i < schema.len() && !is_simple_value(field) {
361 write_scalar_element(writer, &schema[i], field, config)?;
362 }
363 }
364 }
365 if let Some(children) = node.children() {
369 for (child_type, child_nodes) in children {
370 for child in child_nodes {
371 let default_schema = vec!["id".to_string()];
373 let child_schema = structs
374 .get(child_type)
375 .map(|s| s.as_slice())
376 .unwrap_or(&default_schema);
377 write_child_node(writer, child_type, child, child_schema, config, structs)?;
378 }
379 }
380 }
381
382 writer
383 .write_event(Event::End(BytesEnd::new(elem_name)))
384 .map_err(|e| format!("Failed to write child node end: {}", e))?;
385 } else {
386 writer
388 .write_event(Event::Empty(elem))
389 .map_err(|e| format!("Failed to write empty child node: {}", e))?;
390 }
391
392 Ok(())
393}
394
395fn write_tensor<W: std::io::Write>(
396 writer: &mut Writer<W>,
397 tensor: &Tensor,
398 _config: &ToXmlConfig,
399) -> Result<(), String> {
400 match tensor {
401 Tensor::Scalar(n) => write_text(writer, &n.to_string())?,
402 Tensor::Array(items) => {
403 for item in items {
404 let elem = BytesStart::new("item");
405 writer
406 .write_event(Event::Start(elem))
407 .map_err(|e| format!("Failed to write tensor item start: {}", e))?;
408
409 write_tensor(writer, item, _config)?;
410
411 writer
412 .write_event(Event::End(BytesEnd::new("item")))
413 .map_err(|e| format!("Failed to write tensor item end: {}", e))?;
414 }
415 }
416 }
417 Ok(())
418}
419
420fn write_list<W: std::io::Write>(
423 writer: &mut Writer<W>,
424 items: &[Value],
425 config: &ToXmlConfig,
426) -> Result<(), String> {
427 for item in items {
428 let elem = BytesStart::new("item");
429 writer
430 .write_event(Event::Start(elem))
431 .map_err(|e| format!("Failed to write list item start: {}", e))?;
432
433 write_value_content(writer, item, config)?;
434
435 writer
436 .write_event(Event::End(BytesEnd::new("item")))
437 .map_err(|e| format!("Failed to write list item end: {}", e))?;
438 }
439 Ok(())
440}
441
442fn write_text<W: std::io::Write>(writer: &mut Writer<W>, text: &str) -> Result<(), String> {
443 writer
444 .write_event(Event::Text(BytesText::new(text)))
445 .map_err(|e| format!("Failed to write text: {}", e))
446}
447
448fn is_simple_value(value: &Value) -> bool {
449 matches!(
450 value,
451 Value::Null | Value::Bool(_) | Value::Int(_) | Value::Float(_) | Value::String(_)
452 )
453}
454
455fn escape_attribute_value(value: &Value) -> String {
456 match value {
457 Value::Null => String::new(),
458 Value::Bool(b) => b.to_string(),
459 Value::Int(n) => n.to_string(),
460 Value::Float(f) => f.to_string(),
461 Value::String(s) => s.to_string(),
462 Value::Reference(r) => r.to_ref_string(),
463 Value::Expression(e) => format!("$({})", e),
464 Value::Tensor(_) => "[tensor]".to_string(),
465 Value::List(_) => "[list]".to_string(),
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472 use hedl_core::lex::{Expression, Span};
473 use hedl_core::{Document, Reference};
474
475 #[test]
478 fn test_to_xml_config_default() {
479 let config = ToXmlConfig::default();
480 assert!(config.pretty);
481 assert_eq!(config.indent, " ");
482 assert_eq!(config.root_element, "hedl");
483 assert!(!config.include_metadata);
484 assert!(!config.use_attributes);
485 }
486
487 #[test]
488 fn test_to_xml_config_debug() {
489 let config = ToXmlConfig::default();
490 let debug = format!("{:?}", config);
491 assert!(debug.contains("ToXmlConfig"));
492 assert!(debug.contains("pretty"));
493 assert!(debug.contains("indent"));
494 assert!(debug.contains("root_element"));
495 }
496
497 #[test]
498 fn test_to_xml_config_clone() {
499 let config = ToXmlConfig {
500 pretty: false,
501 indent: "\t".to_string(),
502 root_element: "custom".to_string(),
503 include_metadata: true,
504 use_attributes: true,
505 };
506 let cloned = config.clone();
507 assert!(!cloned.pretty);
508 assert_eq!(cloned.indent, "\t");
509 assert_eq!(cloned.root_element, "custom");
510 assert!(cloned.include_metadata);
511 assert!(cloned.use_attributes);
512 }
513
514 #[test]
515 fn test_to_xml_config_all_options() {
516 let config = ToXmlConfig {
517 pretty: true,
518 indent: " ".to_string(),
519 root_element: "document".to_string(),
520 include_metadata: true,
521 use_attributes: true,
522 };
523 assert!(config.pretty);
524 assert_eq!(config.indent.len(), 4);
525 }
526
527 #[test]
530 fn test_empty_document() {
531 let doc = Document::new((2, 0));
532 let config = ToXmlConfig::default();
533 let xml = to_xml(&doc, &config).unwrap();
534 assert!(xml.contains("<?xml"));
535 assert!(xml.contains("<hedl"));
536 assert!(xml.contains("</hedl>"));
537 }
538
539 #[test]
540 fn test_empty_document_compact() {
541 let doc = Document::new((2, 0));
542 let config = ToXmlConfig {
543 pretty: false,
544 ..Default::default()
545 };
546 let xml = to_xml(&doc, &config).unwrap();
547 assert!(xml.contains("<?xml"));
548 assert!(xml.contains("<hedl></hedl>"));
549 }
550
551 #[test]
552 fn test_custom_root_element() {
553 let doc = Document::new((2, 0));
554 let config = ToXmlConfig {
555 root_element: "custom_root".to_string(),
556 ..Default::default()
557 };
558 let xml = to_xml(&doc, &config).unwrap();
559 assert!(xml.contains("<custom_root"));
560 assert!(xml.contains("</custom_root>"));
561 }
562
563 #[test]
564 fn test_with_metadata() {
565 let doc = Document::new((2, 5));
566 let config = ToXmlConfig {
567 include_metadata: true,
568 ..Default::default()
569 };
570 let xml = to_xml(&doc, &config).unwrap();
571 assert!(xml.contains("version=\"2.5\""));
572 }
573
574 #[test]
577 fn test_scalar_null() {
578 let mut doc = Document::new((2, 0));
579 doc.root
580 .insert("null_val".to_string(), Item::Scalar(Value::Null));
581
582 let config = ToXmlConfig::default();
583 let xml = to_xml(&doc, &config).unwrap();
584 assert!(xml.contains("<null_val>") && xml.contains("</null_val>"));
586 }
587
588 #[test]
589 fn test_scalar_bool_true() {
590 let mut doc = Document::new((2, 0));
591 doc.root
592 .insert("val".to_string(), Item::Scalar(Value::Bool(true)));
593
594 let config = ToXmlConfig::default();
595 let xml = to_xml(&doc, &config).unwrap();
596 assert!(xml.contains("<val>true</val>"));
597 }
598
599 #[test]
600 fn test_scalar_bool_false() {
601 let mut doc = Document::new((2, 0));
602 doc.root
603 .insert("val".to_string(), Item::Scalar(Value::Bool(false)));
604
605 let config = ToXmlConfig::default();
606 let xml = to_xml(&doc, &config).unwrap();
607 assert!(xml.contains("<val>false</val>"));
608 }
609
610 #[test]
611 fn test_scalar_int_positive() {
612 let mut doc = Document::new((2, 0));
613 doc.root
614 .insert("val".to_string(), Item::Scalar(Value::Int(42)));
615
616 let config = ToXmlConfig::default();
617 let xml = to_xml(&doc, &config).unwrap();
618 assert!(xml.contains("<val>42</val>"));
619 }
620
621 #[test]
622 fn test_scalar_int_negative() {
623 let mut doc = Document::new((2, 0));
624 doc.root
625 .insert("val".to_string(), Item::Scalar(Value::Int(-100)));
626
627 let config = ToXmlConfig::default();
628 let xml = to_xml(&doc, &config).unwrap();
629 assert!(xml.contains("<val>-100</val>"));
630 }
631
632 #[test]
633 fn test_scalar_int_zero() {
634 let mut doc = Document::new((2, 0));
635 doc.root
636 .insert("val".to_string(), Item::Scalar(Value::Int(0)));
637
638 let config = ToXmlConfig::default();
639 let xml = to_xml(&doc, &config).unwrap();
640 assert!(xml.contains("<val>0</val>"));
641 }
642
643 #[test]
644 fn test_scalar_float() {
645 let mut doc = Document::new((2, 0));
646 doc.root
647 .insert("val".to_string(), Item::Scalar(Value::Float(3.5)));
648
649 let config = ToXmlConfig::default();
650 let xml = to_xml(&doc, &config).unwrap();
651 assert!(xml.contains("<val>3.5</val>"));
652 }
653
654 #[test]
655 fn test_scalar_string() {
656 let mut doc = Document::new((2, 0));
657 doc.root.insert(
658 "val".to_string(),
659 Item::Scalar(Value::String("hello".to_string().into())),
660 );
661
662 let config = ToXmlConfig::default();
663 let xml = to_xml(&doc, &config).unwrap();
664 assert!(xml.contains("<val>hello</val>"));
665 }
666
667 #[test]
668 fn test_scalar_string_empty() {
669 let mut doc = Document::new((2, 0));
670 doc.root.insert(
671 "val".to_string(),
672 Item::Scalar(Value::String("".to_string().into())),
673 );
674
675 let config = ToXmlConfig::default();
676 let xml = to_xml(&doc, &config).unwrap();
677 assert!(xml.contains("<val></val>") || xml.contains("<val/>"));
678 }
679
680 #[test]
683 fn test_scalar_reference_local() {
684 let mut doc = Document::new((2, 0));
685 doc.root.insert(
686 "ref".to_string(),
687 Item::Scalar(Value::Reference(Reference::local("user123"))),
688 );
689
690 let config = ToXmlConfig::default();
691 let xml = to_xml(&doc, &config).unwrap();
692 assert!(xml.contains("@user123"));
693 assert!(xml.contains("__hedl_type__=\"ref\""));
694 }
695
696 #[test]
697 fn test_scalar_reference_qualified() {
698 let mut doc = Document::new((2, 0));
699 doc.root.insert(
700 "ref".to_string(),
701 Item::Scalar(Value::Reference(Reference::qualified("User", "456"))),
702 );
703
704 let config = ToXmlConfig::default();
705 let xml = to_xml(&doc, &config).unwrap();
706 assert!(xml.contains("@User:456"));
707 }
708
709 #[test]
712 fn test_scalar_expression_identifier() {
713 let mut doc = Document::new((2, 0));
714 doc.root.insert(
715 "expr".to_string(),
716 Item::Scalar(Value::Expression(Box::new(Expression::Identifier {
717 name: "foo".to_string(),
718 span: Span::synthetic(),
719 }))),
720 );
721
722 let config = ToXmlConfig::default();
723 let xml = to_xml(&doc, &config).unwrap();
724 assert!(xml.contains("$(foo)"));
725 }
726
727 #[test]
728 fn test_scalar_expression_call() {
729 let mut doc = Document::new((2, 0));
730 doc.root.insert(
731 "expr".to_string(),
732 Item::Scalar(Value::Expression(Box::new(Expression::Call {
733 name: "add".to_string(),
734 args: vec![
735 Expression::Identifier {
736 name: "x".to_string(),
737 span: Span::synthetic(),
738 },
739 Expression::Literal {
740 value: hedl_core::lex::ExprLiteral::Int(1),
741 span: Span::synthetic(),
742 },
743 ],
744 span: Span::synthetic(),
745 }))),
746 );
747
748 let config = ToXmlConfig::default();
749 let xml = to_xml(&doc, &config).unwrap();
750 assert!(xml.contains("$(add(x, 1))"));
751 }
752
753 #[test]
756 fn test_tensor_1d() {
757 let mut doc = Document::new((2, 0));
758 let tensor = Tensor::Array(vec![
759 Tensor::Scalar(1.0),
760 Tensor::Scalar(2.0),
761 Tensor::Scalar(3.0),
762 ]);
763 doc.root.insert(
764 "tensor".to_string(),
765 Item::Scalar(Value::Tensor(Box::new(tensor))),
766 );
767
768 let config = ToXmlConfig::default();
769 let xml = to_xml(&doc, &config).unwrap();
770 assert!(xml.contains("<tensor>"));
771 assert!(xml.contains("<item>1</item>"));
772 assert!(xml.contains("<item>2</item>"));
773 assert!(xml.contains("<item>3</item>"));
774 }
775
776 #[test]
777 fn test_tensor_scalar() {
778 let mut doc = Document::new((2, 0));
779 let tensor = Tensor::Scalar(42.5);
780 doc.root.insert(
781 "tensor".to_string(),
782 Item::Scalar(Value::Tensor(Box::new(tensor))),
783 );
784
785 let config = ToXmlConfig::default();
786 let xml = to_xml(&doc, &config).unwrap();
787 assert!(xml.contains("<tensor>42.5</tensor>"));
788 }
789
790 #[test]
793 fn test_nested_object() {
794 let mut doc = Document::new((2, 0));
795 let mut inner = BTreeMap::new();
796 inner.insert(
797 "name".to_string(),
798 Item::Scalar(Value::String("test".to_string().into())),
799 );
800 inner.insert("value".to_string(), Item::Scalar(Value::Int(100)));
801 doc.root.insert("config".to_string(), Item::Object(inner));
802
803 let config = ToXmlConfig::default();
804 let xml = to_xml(&doc, &config).unwrap();
805
806 assert!(xml.contains("<config>"));
807 assert!(xml.contains("<name>test</name>"));
808 assert!(xml.contains("<value>100</value>"));
809 assert!(xml.contains("</config>"));
810 }
811
812 #[test]
813 fn test_deeply_nested_object() {
814 let mut doc = Document::new((2, 0));
815
816 let mut level3 = BTreeMap::new();
817 level3.insert("deep".to_string(), Item::Scalar(Value::Int(42)));
818
819 let mut level2 = BTreeMap::new();
820 level2.insert("nested".to_string(), Item::Object(level3));
821
822 let mut level1 = BTreeMap::new();
823 level1.insert("inner".to_string(), Item::Object(level2));
824
825 doc.root.insert("outer".to_string(), Item::Object(level1));
826
827 let config = ToXmlConfig::default();
828 let xml = to_xml(&doc, &config).unwrap();
829
830 assert!(xml.contains("<outer>"));
831 assert!(xml.contains("<inner>"));
832 assert!(xml.contains("<nested>"));
833 assert!(xml.contains("<deep>42</deep>"));
834 }
835
836 #[test]
839 fn test_matrix_list() {
840 let mut doc = Document::new((2, 0));
841 let mut list = MatrixList::new("User", vec!["id".to_string(), "name".to_string()]);
842 list.add_row(Node::new(
843 "User",
844 "u1",
845 vec![
846 Value::String("u1".to_string().into()),
847 Value::String("Alice".to_string().into()),
848 ],
849 ));
850 doc.root.insert("users".to_string(), Item::List(list));
851
852 let config = ToXmlConfig::default();
853 let xml = to_xml(&doc, &config).unwrap();
854
855 assert!(xml.contains("<users>"));
856 assert!(xml.contains("<user>"));
857 assert!(xml.contains("</users>"));
858 }
859
860 #[test]
861 fn test_matrix_list_with_metadata() {
862 let mut doc = Document::new((2, 0));
863 let mut list = MatrixList::new("User", vec!["id".to_string()]);
864 list.add_row(Node::new(
865 "User",
866 "u1",
867 vec![Value::String("u1".to_string().into())],
868 ));
869 doc.root.insert("users".to_string(), Item::List(list));
870
871 let config = ToXmlConfig {
872 include_metadata: true,
873 ..Default::default()
874 };
875 let xml = to_xml(&doc, &config).unwrap();
876 assert!(xml.contains("type=\"User\""));
877 }
878
879 #[test]
882 fn test_special_characters_ampersand() {
883 let mut doc = Document::new((2, 0));
884 doc.root.insert(
885 "text".to_string(),
886 Item::Scalar(Value::String("hello & goodbye".to_string().into())),
887 );
888
889 let config = ToXmlConfig::default();
890 let xml = to_xml(&doc, &config).unwrap();
891 assert!(xml.contains("<text>"));
893 }
894
895 #[test]
896 fn test_special_characters_angle_brackets() {
897 let mut doc = Document::new((2, 0));
898 doc.root.insert(
899 "text".to_string(),
900 Item::Scalar(Value::String("hello <tag> goodbye".to_string().into())),
901 );
902
903 let config = ToXmlConfig::default();
904 let xml = to_xml(&doc, &config).unwrap();
905 assert!(xml.contains("<text>"));
906 }
907
908 #[test]
909 fn test_special_characters_quotes() {
910 let mut doc = Document::new((2, 0));
911 doc.root.insert(
912 "text".to_string(),
913 Item::Scalar(Value::String("hello \"quoted\"".to_string().into())),
914 );
915
916 let config = ToXmlConfig::default();
917 let xml = to_xml(&doc, &config).unwrap();
918 assert!(xml.contains("<text>"));
919 }
920
921 #[test]
924 fn test_is_simple_value() {
925 assert!(is_simple_value(&Value::Null));
926 assert!(is_simple_value(&Value::Bool(true)));
927 assert!(is_simple_value(&Value::Int(42)));
928 assert!(is_simple_value(&Value::Float(3.5)));
929 assert!(is_simple_value(&Value::String("hello".to_string().into())));
930 assert!(!is_simple_value(&Value::Reference(Reference::local("x"))));
931 assert!(!is_simple_value(&Value::Tensor(Box::new(Tensor::Scalar(
932 1.0
933 )))));
934 }
935
936 #[test]
937 fn test_escape_attribute_value_null() {
938 assert_eq!(escape_attribute_value(&Value::Null), "");
939 }
940
941 #[test]
942 fn test_escape_attribute_value_bool() {
943 assert_eq!(escape_attribute_value(&Value::Bool(true)), "true");
944 assert_eq!(escape_attribute_value(&Value::Bool(false)), "false");
945 }
946
947 #[test]
948 fn test_escape_attribute_value_int() {
949 assert_eq!(escape_attribute_value(&Value::Int(42)), "42");
950 assert_eq!(escape_attribute_value(&Value::Int(-100)), "-100");
951 }
952
953 #[test]
954 fn test_escape_attribute_value_float() {
955 assert_eq!(escape_attribute_value(&Value::Float(3.5)), "3.5");
956 }
957
958 #[test]
959 fn test_escape_attribute_value_string() {
960 assert_eq!(
961 escape_attribute_value(&Value::String("hello".to_string().into())),
962 "hello"
963 );
964 }
965
966 #[test]
967 fn test_escape_attribute_value_reference() {
968 let ref_val = Value::Reference(Reference::local("user1"));
969 assert_eq!(escape_attribute_value(&ref_val), "@user1");
970 }
971
972 #[test]
973 fn test_escape_attribute_value_expression() {
974 let expr = Value::Expression(Box::new(Expression::Identifier {
975 name: "foo".to_string(),
976 span: Span::default(),
977 }));
978 assert_eq!(escape_attribute_value(&expr), "$(foo)");
979 }
980
981 #[test]
982 fn test_escape_attribute_value_tensor() {
983 let tensor = Value::Tensor(Box::new(Tensor::Scalar(1.0)));
984 assert_eq!(escape_attribute_value(&tensor), "[tensor]");
985 }
986
987 #[test]
988 fn test_escape_attribute_value_list() {
989 let list = Value::List(Box::new(vec![
990 Value::String("a".to_string().into()),
991 Value::String("b".to_string().into()),
992 ]));
993 assert_eq!(escape_attribute_value(&list), "[list]");
994 }
995
996 #[test]
999 fn test_list_string_values() {
1000 let mut doc = Document::new((1, 1));
1001 doc.root.insert(
1002 "roles".to_string(),
1003 Item::Scalar(Value::List(Box::new(vec![
1004 Value::String("admin".to_string().into()),
1005 Value::String("editor".to_string().into()),
1006 Value::String("viewer".to_string().into()),
1007 ]))),
1008 );
1009
1010 let config = ToXmlConfig::default();
1011 let xml = to_xml(&doc, &config).unwrap();
1012 assert!(xml.contains("<roles>"));
1013 assert!(xml.contains("<item>admin</item>"));
1014 assert!(xml.contains("<item>editor</item>"));
1015 assert!(xml.contains("<item>viewer</item>"));
1016 assert!(xml.contains("</roles>"));
1017 }
1018
1019 #[test]
1020 fn test_list_bool_values() {
1021 let mut doc = Document::new((1, 1));
1022 doc.root.insert(
1023 "flags".to_string(),
1024 Item::Scalar(Value::List(Box::new(vec![
1025 Value::Bool(true),
1026 Value::Bool(false),
1027 Value::Bool(true),
1028 ]))),
1029 );
1030
1031 let config = ToXmlConfig::default();
1032 let xml = to_xml(&doc, &config).unwrap();
1033 assert!(xml.contains("<item>true</item>"));
1034 assert!(xml.contains("<item>false</item>"));
1035 }
1036
1037 #[test]
1038 fn test_list_mixed_values() {
1039 let mut doc = Document::new((1, 1));
1040 doc.root.insert(
1041 "mixed".to_string(),
1042 Item::Scalar(Value::List(Box::new(vec![
1043 Value::String("text".to_string().into()),
1044 Value::Int(42),
1045 Value::Bool(true),
1046 Value::Null,
1047 ]))),
1048 );
1049
1050 let config = ToXmlConfig::default();
1051 let xml = to_xml(&doc, &config).unwrap();
1052 assert!(xml.contains("<item>text</item>"));
1053 assert!(xml.contains("<item>42</item>"));
1054 assert!(xml.contains("<item>true</item>"));
1055 }
1056
1057 #[test]
1058 fn test_list_empty() {
1059 let mut doc = Document::new((1, 1));
1060 doc.root.insert(
1061 "empty".to_string(),
1062 Item::Scalar(Value::List(Box::default())),
1063 );
1064
1065 let config = ToXmlConfig::default();
1066 let xml = to_xml(&doc, &config).unwrap();
1067 assert!(xml.contains("<empty>"));
1068 assert!(xml.contains("</empty>"));
1069 }
1070
1071 #[test]
1072 fn test_list_with_references() {
1073 let mut doc = Document::new((1, 1));
1074 doc.root.insert(
1075 "refs".to_string(),
1076 Item::Scalar(Value::List(Box::new(vec![
1077 Value::Reference(Reference::local("user1")),
1078 Value::Reference(Reference::qualified("User", "2")),
1079 ]))),
1080 );
1081
1082 let config = ToXmlConfig::default();
1083 let xml = to_xml(&doc, &config).unwrap();
1084 assert!(xml.contains("<item>@user1</item>"));
1085 assert!(xml.contains("<item>@User:2</item>"));
1086 }
1087
1088 #[test]
1091 fn test_pretty_vs_compact() {
1092 let mut doc = Document::new((2, 0));
1093 doc.root
1094 .insert("val".to_string(), Item::Scalar(Value::Int(42)));
1095
1096 let config_pretty = ToXmlConfig {
1097 pretty: true,
1098 ..Default::default()
1099 };
1100 let config_compact = ToXmlConfig {
1101 pretty: false,
1102 ..Default::default()
1103 };
1104
1105 let xml_pretty = to_xml(&doc, &config_pretty).unwrap();
1106 let xml_compact = to_xml(&doc, &config_compact).unwrap();
1107
1108 assert!(xml_pretty.len() > xml_compact.len());
1109 }
1110
1111 #[test]
1114 fn test_use_attributes_simple() {
1115 let mut doc = Document::new((2, 0));
1116 doc.root
1117 .insert("val".to_string(), Item::Scalar(Value::Int(42)));
1118
1119 let config = ToXmlConfig {
1120 use_attributes: true,
1121 ..Default::default()
1122 };
1123 let xml = to_xml(&doc, &config).unwrap();
1124 assert!(xml.contains("value=\"42\""));
1126 }
1127}