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