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 }
180 Ok(())
181}
182
183fn write_object<W: std::io::Write>(
184 writer: &mut Writer<W>,
185 key: &str,
186 obj: &BTreeMap<String, Item>,
187 config: &ToXmlConfig,
188 structs: &BTreeMap<String, Vec<String>>,
189) -> Result<(), String> {
190 let elem = BytesStart::new(key);
191 writer
192 .write_event(Event::Start(elem))
193 .map_err(|e| format!("Failed to write object start: {}", e))?;
194
195 for (child_key, child_item) in obj {
196 write_item(writer, child_key, child_item, config, structs)?;
197 }
198
199 writer
200 .write_event(Event::End(BytesEnd::new(key)))
201 .map_err(|e| format!("Failed to write object end: {}", e))?;
202
203 Ok(())
204}
205
206fn write_matrix_list<W: std::io::Write>(
207 writer: &mut Writer<W>,
208 key: &str,
209 list: &MatrixList,
210 config: &ToXmlConfig,
211 structs: &BTreeMap<String, Vec<String>>,
212) -> Result<(), String> {
213 let mut list_elem = BytesStart::new(key);
214 if config.include_metadata {
215 list_elem.push_attribute(("type", list.type_name.as_str()));
216 }
217
218 writer
219 .write_event(Event::Start(list_elem))
220 .map_err(|e| format!("Failed to write list start: {}", e))?;
221
222 let item_name = list.type_name.to_lowercase();
224 for row in &list.rows {
225 write_node(writer, &item_name, row, &list.schema, config, structs)?;
226 }
227
228 writer
229 .write_event(Event::End(BytesEnd::new(key)))
230 .map_err(|e| format!("Failed to write list end: {}", e))?;
231
232 Ok(())
233}
234
235fn write_node<W: std::io::Write>(
236 writer: &mut Writer<W>,
237 elem_name: &str,
238 node: &Node,
239 schema: &[String],
240 config: &ToXmlConfig,
241 structs: &BTreeMap<String, Vec<String>>,
242) -> Result<(), String> {
243 let mut elem = BytesStart::new(elem_name);
244
245 if config.use_attributes {
250 for (i, field) in node.fields.iter().enumerate() {
251 if is_simple_value(field) && i < schema.len() {
252 let attr_value = escape_attribute_value(field);
253 elem.push_attribute((schema[i].as_str(), attr_value.as_str()));
254 }
255 }
256 }
257
258 let has_complex_values = node.fields.iter().any(|v| !is_simple_value(v));
260 let has_children = node.children().map(|c| !c.is_empty()).unwrap_or(false);
261
262 if !config.use_attributes || has_complex_values || has_children {
263 writer
264 .write_event(Event::Start(elem))
265 .map_err(|e| format!("Failed to write node start: {}", e))?;
266
267 if !config.use_attributes {
271 for (i, field) in node.fields.iter().enumerate() {
273 if i < schema.len() {
274 write_scalar_element(writer, &schema[i], field, config)?;
275 }
276 }
277 } else if has_complex_values {
278 for (i, field) in node.fields.iter().enumerate() {
280 if i < schema.len() && !is_simple_value(field) {
281 write_scalar_element(writer, &schema[i], field, config)?;
282 }
283 }
284 }
285 if let Some(children) = node.children() {
289 for (child_type, child_nodes) in children {
290 for child in child_nodes {
291 let default_schema = vec!["id".to_string()];
293 let child_schema = structs
294 .get(child_type)
295 .map(|s| s.as_slice())
296 .unwrap_or(&default_schema);
297 write_child_node(writer, child_type, child, child_schema, config, structs)?;
298 }
299 }
300 }
301
302 writer
303 .write_event(Event::End(BytesEnd::new(elem_name)))
304 .map_err(|e| format!("Failed to write node end: {}", e))?;
305 } else {
306 writer
308 .write_event(Event::Empty(elem))
309 .map_err(|e| format!("Failed to write empty node: {}", e))?;
310 }
311
312 Ok(())
313}
314
315fn write_child_node<W: std::io::Write>(
317 writer: &mut Writer<W>,
318 elem_name: &str,
319 node: &Node,
320 schema: &[String],
321 config: &ToXmlConfig,
322 structs: &BTreeMap<String, Vec<String>>,
323) -> Result<(), String> {
324 let mut elem = BytesStart::new(elem_name);
325
326 elem.push_attribute(("__hedl_child__", "true"));
328
329 if config.use_attributes {
331 for (i, field) in node.fields.iter().enumerate() {
332 if is_simple_value(field) && i < schema.len() {
333 let attr_value = escape_attribute_value(field);
334 elem.push_attribute((schema[i].as_str(), attr_value.as_str()));
335 }
336 }
337 }
338
339 let has_complex_values = node.fields.iter().any(|v| !is_simple_value(v));
341 let has_children = node.children().map(|c| !c.is_empty()).unwrap_or(false);
342
343 if !config.use_attributes || has_complex_values || has_children {
344 writer
345 .write_event(Event::Start(elem))
346 .map_err(|e| format!("Failed to write child node start: {}", e))?;
347
348 if !config.use_attributes {
350 for (i, field) in node.fields.iter().enumerate() {
352 if i < schema.len() {
353 write_scalar_element(writer, &schema[i], field, config)?;
354 }
355 }
356 } else if has_complex_values {
357 for (i, field) in node.fields.iter().enumerate() {
359 if i < schema.len() && !is_simple_value(field) {
360 write_scalar_element(writer, &schema[i], field, config)?;
361 }
362 }
363 }
364 if let Some(children) = node.children() {
368 for (child_type, child_nodes) in children {
369 for child in child_nodes {
370 let default_schema = vec!["id".to_string()];
372 let child_schema = structs
373 .get(child_type)
374 .map(|s| s.as_slice())
375 .unwrap_or(&default_schema);
376 write_child_node(writer, child_type, child, child_schema, config, structs)?;
377 }
378 }
379 }
380
381 writer
382 .write_event(Event::End(BytesEnd::new(elem_name)))
383 .map_err(|e| format!("Failed to write child node end: {}", e))?;
384 } else {
385 writer
387 .write_event(Event::Empty(elem))
388 .map_err(|e| format!("Failed to write empty child node: {}", e))?;
389 }
390
391 Ok(())
392}
393
394fn write_tensor<W: std::io::Write>(
395 writer: &mut Writer<W>,
396 tensor: &Tensor,
397 _config: &ToXmlConfig,
398) -> Result<(), String> {
399 match tensor {
400 Tensor::Scalar(n) => write_text(writer, &n.to_string())?,
401 Tensor::Array(items) => {
402 for item in items {
403 let elem = BytesStart::new("item");
404 writer
405 .write_event(Event::Start(elem))
406 .map_err(|e| format!("Failed to write tensor item start: {}", e))?;
407
408 write_tensor(writer, item, _config)?;
409
410 writer
411 .write_event(Event::End(BytesEnd::new("item")))
412 .map_err(|e| format!("Failed to write tensor item end: {}", e))?;
413 }
414 }
415 }
416 Ok(())
417}
418
419fn write_text<W: std::io::Write>(writer: &mut Writer<W>, text: &str) -> Result<(), String> {
420 writer
421 .write_event(Event::Text(BytesText::new(text)))
422 .map_err(|e| format!("Failed to write text: {}", e))
423}
424
425fn is_simple_value(value: &Value) -> bool {
426 matches!(
427 value,
428 Value::Null | Value::Bool(_) | Value::Int(_) | Value::Float(_) | Value::String(_)
429 )
430}
431
432fn escape_attribute_value(value: &Value) -> String {
433 match value {
434 Value::Null => String::new(),
435 Value::Bool(b) => b.to_string(),
436 Value::Int(n) => n.to_string(),
437 Value::Float(f) => f.to_string(),
438 Value::String(s) => s.to_string(),
439 Value::Reference(r) => r.to_ref_string(),
440 Value::Expression(e) => format!("$({})", e),
441 Value::Tensor(_) => "[tensor]".to_string(),
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448 use hedl_core::lex::{Expression, Span};
449 use hedl_core::{Document, Reference};
450
451 #[test]
454 fn test_to_xml_config_default() {
455 let config = ToXmlConfig::default();
456 assert!(config.pretty);
457 assert_eq!(config.indent, " ");
458 assert_eq!(config.root_element, "hedl");
459 assert!(!config.include_metadata);
460 assert!(!config.use_attributes);
461 }
462
463 #[test]
464 fn test_to_xml_config_debug() {
465 let config = ToXmlConfig::default();
466 let debug = format!("{:?}", config);
467 assert!(debug.contains("ToXmlConfig"));
468 assert!(debug.contains("pretty"));
469 assert!(debug.contains("indent"));
470 assert!(debug.contains("root_element"));
471 }
472
473 #[test]
474 fn test_to_xml_config_clone() {
475 let config = ToXmlConfig {
476 pretty: false,
477 indent: "\t".to_string(),
478 root_element: "custom".to_string(),
479 include_metadata: true,
480 use_attributes: true,
481 };
482 let cloned = config.clone();
483 assert!(!cloned.pretty);
484 assert_eq!(cloned.indent, "\t");
485 assert_eq!(cloned.root_element, "custom");
486 assert!(cloned.include_metadata);
487 assert!(cloned.use_attributes);
488 }
489
490 #[test]
491 fn test_to_xml_config_all_options() {
492 let config = ToXmlConfig {
493 pretty: true,
494 indent: " ".to_string(),
495 root_element: "document".to_string(),
496 include_metadata: true,
497 use_attributes: true,
498 };
499 assert!(config.pretty);
500 assert_eq!(config.indent.len(), 4);
501 }
502
503 #[test]
506 fn test_empty_document() {
507 let doc = Document::new((1, 0));
508 let config = ToXmlConfig::default();
509 let xml = to_xml(&doc, &config).unwrap();
510 assert!(xml.contains("<?xml"));
511 assert!(xml.contains("<hedl"));
512 assert!(xml.contains("</hedl>"));
513 }
514
515 #[test]
516 fn test_empty_document_compact() {
517 let doc = Document::new((1, 0));
518 let config = ToXmlConfig {
519 pretty: false,
520 ..Default::default()
521 };
522 let xml = to_xml(&doc, &config).unwrap();
523 assert!(xml.contains("<?xml"));
524 assert!(xml.contains("<hedl></hedl>"));
525 }
526
527 #[test]
528 fn test_custom_root_element() {
529 let doc = Document::new((1, 0));
530 let config = ToXmlConfig {
531 root_element: "custom_root".to_string(),
532 ..Default::default()
533 };
534 let xml = to_xml(&doc, &config).unwrap();
535 assert!(xml.contains("<custom_root"));
536 assert!(xml.contains("</custom_root>"));
537 }
538
539 #[test]
540 fn test_with_metadata() {
541 let doc = Document::new((2, 5));
542 let config = ToXmlConfig {
543 include_metadata: true,
544 ..Default::default()
545 };
546 let xml = to_xml(&doc, &config).unwrap();
547 assert!(xml.contains("version=\"2.5\""));
548 }
549
550 #[test]
553 fn test_scalar_null() {
554 let mut doc = Document::new((1, 0));
555 doc.root
556 .insert("null_val".to_string(), Item::Scalar(Value::Null));
557
558 let config = ToXmlConfig::default();
559 let xml = to_xml(&doc, &config).unwrap();
560 assert!(xml.contains("<null_val>") && xml.contains("</null_val>"));
562 }
563
564 #[test]
565 fn test_scalar_bool_true() {
566 let mut doc = Document::new((1, 0));
567 doc.root
568 .insert("val".to_string(), Item::Scalar(Value::Bool(true)));
569
570 let config = ToXmlConfig::default();
571 let xml = to_xml(&doc, &config).unwrap();
572 assert!(xml.contains("<val>true</val>"));
573 }
574
575 #[test]
576 fn test_scalar_bool_false() {
577 let mut doc = Document::new((1, 0));
578 doc.root
579 .insert("val".to_string(), Item::Scalar(Value::Bool(false)));
580
581 let config = ToXmlConfig::default();
582 let xml = to_xml(&doc, &config).unwrap();
583 assert!(xml.contains("<val>false</val>"));
584 }
585
586 #[test]
587 fn test_scalar_int_positive() {
588 let mut doc = Document::new((1, 0));
589 doc.root
590 .insert("val".to_string(), Item::Scalar(Value::Int(42)));
591
592 let config = ToXmlConfig::default();
593 let xml = to_xml(&doc, &config).unwrap();
594 assert!(xml.contains("<val>42</val>"));
595 }
596
597 #[test]
598 fn test_scalar_int_negative() {
599 let mut doc = Document::new((1, 0));
600 doc.root
601 .insert("val".to_string(), Item::Scalar(Value::Int(-100)));
602
603 let config = ToXmlConfig::default();
604 let xml = to_xml(&doc, &config).unwrap();
605 assert!(xml.contains("<val>-100</val>"));
606 }
607
608 #[test]
609 fn test_scalar_int_zero() {
610 let mut doc = Document::new((1, 0));
611 doc.root
612 .insert("val".to_string(), Item::Scalar(Value::Int(0)));
613
614 let config = ToXmlConfig::default();
615 let xml = to_xml(&doc, &config).unwrap();
616 assert!(xml.contains("<val>0</val>"));
617 }
618
619 #[test]
620 fn test_scalar_float() {
621 let mut doc = Document::new((1, 0));
622 doc.root
623 .insert("val".to_string(), Item::Scalar(Value::Float(3.5)));
624
625 let config = ToXmlConfig::default();
626 let xml = to_xml(&doc, &config).unwrap();
627 assert!(xml.contains("<val>3.5</val>"));
628 }
629
630 #[test]
631 fn test_scalar_string() {
632 let mut doc = Document::new((1, 0));
633 doc.root.insert(
634 "val".to_string(),
635 Item::Scalar(Value::String("hello".to_string().into())),
636 );
637
638 let config = ToXmlConfig::default();
639 let xml = to_xml(&doc, &config).unwrap();
640 assert!(xml.contains("<val>hello</val>"));
641 }
642
643 #[test]
644 fn test_scalar_string_empty() {
645 let mut doc = Document::new((1, 0));
646 doc.root.insert(
647 "val".to_string(),
648 Item::Scalar(Value::String("".to_string().into())),
649 );
650
651 let config = ToXmlConfig::default();
652 let xml = to_xml(&doc, &config).unwrap();
653 assert!(xml.contains("<val></val>") || xml.contains("<val/>"));
654 }
655
656 #[test]
659 fn test_scalar_reference_local() {
660 let mut doc = Document::new((1, 0));
661 doc.root.insert(
662 "ref".to_string(),
663 Item::Scalar(Value::Reference(Reference::local("user123"))),
664 );
665
666 let config = ToXmlConfig::default();
667 let xml = to_xml(&doc, &config).unwrap();
668 assert!(xml.contains("@user123"));
669 assert!(xml.contains("__hedl_type__=\"ref\""));
670 }
671
672 #[test]
673 fn test_scalar_reference_qualified() {
674 let mut doc = Document::new((1, 0));
675 doc.root.insert(
676 "ref".to_string(),
677 Item::Scalar(Value::Reference(Reference::qualified("User", "456"))),
678 );
679
680 let config = ToXmlConfig::default();
681 let xml = to_xml(&doc, &config).unwrap();
682 assert!(xml.contains("@User:456"));
683 }
684
685 #[test]
688 fn test_scalar_expression_identifier() {
689 let mut doc = Document::new((1, 0));
690 doc.root.insert(
691 "expr".to_string(),
692 Item::Scalar(Value::Expression(Box::new(Expression::Identifier {
693 name: "foo".to_string(),
694 span: Span::synthetic(),
695 }))),
696 );
697
698 let config = ToXmlConfig::default();
699 let xml = to_xml(&doc, &config).unwrap();
700 assert!(xml.contains("$(foo)"));
701 }
702
703 #[test]
704 fn test_scalar_expression_call() {
705 let mut doc = Document::new((1, 0));
706 doc.root.insert(
707 "expr".to_string(),
708 Item::Scalar(Value::Expression(Box::new(Expression::Call {
709 name: "add".to_string(),
710 args: vec![
711 Expression::Identifier {
712 name: "x".to_string(),
713 span: Span::synthetic(),
714 },
715 Expression::Literal {
716 value: hedl_core::lex::ExprLiteral::Int(1),
717 span: Span::synthetic(),
718 },
719 ],
720 span: Span::synthetic(),
721 }))),
722 );
723
724 let config = ToXmlConfig::default();
725 let xml = to_xml(&doc, &config).unwrap();
726 assert!(xml.contains("$(add(x, 1))"));
727 }
728
729 #[test]
732 fn test_tensor_1d() {
733 let mut doc = Document::new((1, 0));
734 let tensor = Tensor::Array(vec![
735 Tensor::Scalar(1.0),
736 Tensor::Scalar(2.0),
737 Tensor::Scalar(3.0),
738 ]);
739 doc.root.insert(
740 "tensor".to_string(),
741 Item::Scalar(Value::Tensor(Box::new(tensor))),
742 );
743
744 let config = ToXmlConfig::default();
745 let xml = to_xml(&doc, &config).unwrap();
746 assert!(xml.contains("<tensor>"));
747 assert!(xml.contains("<item>1</item>"));
748 assert!(xml.contains("<item>2</item>"));
749 assert!(xml.contains("<item>3</item>"));
750 }
751
752 #[test]
753 fn test_tensor_scalar() {
754 let mut doc = Document::new((1, 0));
755 let tensor = Tensor::Scalar(42.5);
756 doc.root.insert(
757 "tensor".to_string(),
758 Item::Scalar(Value::Tensor(Box::new(tensor))),
759 );
760
761 let config = ToXmlConfig::default();
762 let xml = to_xml(&doc, &config).unwrap();
763 assert!(xml.contains("<tensor>42.5</tensor>"));
764 }
765
766 #[test]
769 fn test_nested_object() {
770 let mut doc = Document::new((1, 0));
771 let mut inner = BTreeMap::new();
772 inner.insert(
773 "name".to_string(),
774 Item::Scalar(Value::String("test".to_string().into())),
775 );
776 inner.insert("value".to_string(), Item::Scalar(Value::Int(100)));
777 doc.root.insert("config".to_string(), Item::Object(inner));
778
779 let config = ToXmlConfig::default();
780 let xml = to_xml(&doc, &config).unwrap();
781
782 assert!(xml.contains("<config>"));
783 assert!(xml.contains("<name>test</name>"));
784 assert!(xml.contains("<value>100</value>"));
785 assert!(xml.contains("</config>"));
786 }
787
788 #[test]
789 fn test_deeply_nested_object() {
790 let mut doc = Document::new((1, 0));
791
792 let mut level3 = BTreeMap::new();
793 level3.insert("deep".to_string(), Item::Scalar(Value::Int(42)));
794
795 let mut level2 = BTreeMap::new();
796 level2.insert("nested".to_string(), Item::Object(level3));
797
798 let mut level1 = BTreeMap::new();
799 level1.insert("inner".to_string(), Item::Object(level2));
800
801 doc.root.insert("outer".to_string(), Item::Object(level1));
802
803 let config = ToXmlConfig::default();
804 let xml = to_xml(&doc, &config).unwrap();
805
806 assert!(xml.contains("<outer>"));
807 assert!(xml.contains("<inner>"));
808 assert!(xml.contains("<nested>"));
809 assert!(xml.contains("<deep>42</deep>"));
810 }
811
812 #[test]
815 fn test_matrix_list() {
816 let mut doc = Document::new((1, 0));
817 let mut list = MatrixList::new("User", vec!["id".to_string(), "name".to_string()]);
818 list.add_row(Node::new(
819 "User",
820 "u1",
821 vec![
822 Value::String("u1".to_string().into()),
823 Value::String("Alice".to_string().into()),
824 ],
825 ));
826 doc.root.insert("users".to_string(), Item::List(list));
827
828 let config = ToXmlConfig::default();
829 let xml = to_xml(&doc, &config).unwrap();
830
831 assert!(xml.contains("<users>"));
832 assert!(xml.contains("<user>"));
833 assert!(xml.contains("</users>"));
834 }
835
836 #[test]
837 fn test_matrix_list_with_metadata() {
838 let mut doc = Document::new((1, 0));
839 let mut list = MatrixList::new("User", vec!["id".to_string()]);
840 list.add_row(Node::new(
841 "User",
842 "u1",
843 vec![Value::String("u1".to_string().into())],
844 ));
845 doc.root.insert("users".to_string(), Item::List(list));
846
847 let config = ToXmlConfig {
848 include_metadata: true,
849 ..Default::default()
850 };
851 let xml = to_xml(&doc, &config).unwrap();
852 assert!(xml.contains("type=\"User\""));
853 }
854
855 #[test]
858 fn test_special_characters_ampersand() {
859 let mut doc = Document::new((1, 0));
860 doc.root.insert(
861 "text".to_string(),
862 Item::Scalar(Value::String("hello & goodbye".to_string().into())),
863 );
864
865 let config = ToXmlConfig::default();
866 let xml = to_xml(&doc, &config).unwrap();
867 assert!(xml.contains("<text>"));
869 }
870
871 #[test]
872 fn test_special_characters_angle_brackets() {
873 let mut doc = Document::new((1, 0));
874 doc.root.insert(
875 "text".to_string(),
876 Item::Scalar(Value::String("hello <tag> goodbye".to_string().into())),
877 );
878
879 let config = ToXmlConfig::default();
880 let xml = to_xml(&doc, &config).unwrap();
881 assert!(xml.contains("<text>"));
882 }
883
884 #[test]
885 fn test_special_characters_quotes() {
886 let mut doc = Document::new((1, 0));
887 doc.root.insert(
888 "text".to_string(),
889 Item::Scalar(Value::String("hello \"quoted\"".to_string().into())),
890 );
891
892 let config = ToXmlConfig::default();
893 let xml = to_xml(&doc, &config).unwrap();
894 assert!(xml.contains("<text>"));
895 }
896
897 #[test]
900 fn test_is_simple_value() {
901 assert!(is_simple_value(&Value::Null));
902 assert!(is_simple_value(&Value::Bool(true)));
903 assert!(is_simple_value(&Value::Int(42)));
904 assert!(is_simple_value(&Value::Float(3.5)));
905 assert!(is_simple_value(&Value::String("hello".to_string().into())));
906 assert!(!is_simple_value(&Value::Reference(Reference::local("x"))));
907 assert!(!is_simple_value(&Value::Tensor(Box::new(Tensor::Scalar(
908 1.0
909 )))));
910 }
911
912 #[test]
913 fn test_escape_attribute_value_null() {
914 assert_eq!(escape_attribute_value(&Value::Null), "");
915 }
916
917 #[test]
918 fn test_escape_attribute_value_bool() {
919 assert_eq!(escape_attribute_value(&Value::Bool(true)), "true");
920 assert_eq!(escape_attribute_value(&Value::Bool(false)), "false");
921 }
922
923 #[test]
924 fn test_escape_attribute_value_int() {
925 assert_eq!(escape_attribute_value(&Value::Int(42)), "42");
926 assert_eq!(escape_attribute_value(&Value::Int(-100)), "-100");
927 }
928
929 #[test]
930 fn test_escape_attribute_value_float() {
931 assert_eq!(escape_attribute_value(&Value::Float(3.5)), "3.5");
932 }
933
934 #[test]
935 fn test_escape_attribute_value_string() {
936 assert_eq!(
937 escape_attribute_value(&Value::String("hello".to_string().into())),
938 "hello"
939 );
940 }
941
942 #[test]
943 fn test_escape_attribute_value_reference() {
944 let ref_val = Value::Reference(Reference::local("user1"));
945 assert_eq!(escape_attribute_value(&ref_val), "@user1");
946 }
947
948 #[test]
949 fn test_escape_attribute_value_expression() {
950 let expr = Value::Expression(Box::new(Expression::Identifier {
951 name: "foo".to_string(),
952 span: Span::default(),
953 }));
954 assert_eq!(escape_attribute_value(&expr), "$(foo)");
955 }
956
957 #[test]
958 fn test_escape_attribute_value_tensor() {
959 let tensor = Value::Tensor(Box::new(Tensor::Scalar(1.0)));
960 assert_eq!(escape_attribute_value(&tensor), "[tensor]");
961 }
962
963 #[test]
966 fn test_pretty_vs_compact() {
967 let mut doc = Document::new((1, 0));
968 doc.root
969 .insert("val".to_string(), Item::Scalar(Value::Int(42)));
970
971 let config_pretty = ToXmlConfig {
972 pretty: true,
973 ..Default::default()
974 };
975 let config_compact = ToXmlConfig {
976 pretty: false,
977 ..Default::default()
978 };
979
980 let xml_pretty = to_xml(&doc, &config_pretty).unwrap();
981 let xml_compact = to_xml(&doc, &config_compact).unwrap();
982
983 assert!(xml_pretty.len() > xml_compact.len());
984 }
985
986 #[test]
989 fn test_use_attributes_simple() {
990 let mut doc = Document::new((1, 0));
991 doc.root
992 .insert("val".to_string(), Item::Scalar(Value::Int(42)));
993
994 let config = ToXmlConfig {
995 use_attributes: true,
996 ..Default::default()
997 };
998 let xml = to_xml(&doc, &config).unwrap();
999 assert!(xml.contains("value=\"42\""));
1001 }
1002}