hedl_xml/
to_xml.rs

1// Dweve HEDL - Hierarchical Entity Data Language
2//
3// Copyright (c) 2025 Dweve IP B.V. and individual contributors.
4//
5// SPDX-License-Identifier: Apache-2.0
6//
7// Licensed under the Apache License, Version 2.0 (the "License");
8// you may not use this file except in compliance with the License.
9// You may obtain a copy of the License in the LICENSE file at the
10// root of this repository or at: http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! HEDL to XML conversion
19
20use 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/// Configuration for XML output
28#[derive(Debug, Clone)]
29pub struct ToXmlConfig {
30    /// Pretty-print with indentation
31    pub pretty: bool,
32    /// Indentation string (e.g., "  " or "\t")
33    pub indent: String,
34    /// Root element name
35    pub root_element: String,
36    /// Include HEDL metadata as attributes
37    pub include_metadata: bool,
38    /// Use attributes for scalar values where appropriate
39    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
64/// Convert HEDL Document to XML string
65pub fn to_xml(doc: &Document, config: &ToXmlConfig) -> Result<String, String> {
66    let mut writer = if config.pretty {
67        // new_with_indent takes (inner, indent_char, indent_size)
68        Writer::new_with_indent(Cursor::new(Vec::new()), b' ', config.indent.len())
69    } else {
70        Writer::new(Cursor::new(Vec::new()))
71    };
72
73    // Write XML declaration
74    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    // Write root element
79    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 document content
91    write_root(&mut writer, &doc.root, config)?;
92
93    // Close root element
94    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    // Add type marker for references to distinguish from strings starting with @
136    if matches!(value, Value::Reference(_)) {
137        elem.push_attribute(("__hedl_type__", "ref"));
138    }
139
140    // For simple values, we can use attributes if configured
141    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            // Empty element for null
169        }
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    // Write each row as an item element
219    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    // Per SPEC.md: Node.fields contains ALL values including ID (first column)
241    // MatrixList.schema includes all column names with ID first
242
243    // Write simple values as attributes if configured
244    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    // Check if we need element content (complex values or children)
254    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        // Write fields as elements if not using attributes or if complex
263        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        // Write children with marker attribute so they can be recognized on import
272        for (child_type, child_nodes) in &node.children {
273            for child in child_nodes {
274                // Determine schema for children (would need to be passed down in real implementation)
275                let child_schema = vec!["id".to_string()]; // Simplified
276                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        // Empty element with all attributes
285        writer
286            .write_event(Event::Empty(elem))
287            .map_err(|e| format!("Failed to write empty node: {}", e))?;
288    }
289
290    Ok(())
291}
292
293/// Write a child node with a marker attribute so it can be recognized as a NEST child on import.
294fn 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    // Add marker attribute to indicate this is a NEST child
304    elem.push_attribute(("__hedl_child__", "true"));
305
306    // Write simple values as attributes if configured
307    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    // Check if we need element content (complex values or children)
317    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        // Write fields as elements if not using attributes or if complex
326        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        // Write nested children recursively
335        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        // Empty element with all attributes
347        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    // ==================== ToXmlConfig tests ====================
413
414    #[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    // ==================== to_xml basic tests ====================
465
466    #[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    // ==================== Scalar value tests ====================
512
513    #[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        // Null values produce elements with empty content (may have whitespace in pretty mode)
522        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    // ==================== Reference tests ====================
618
619    #[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    // ==================== Expression tests ====================
647
648    #[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    // ==================== Tensor tests ====================
691
692    #[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    // ==================== Object tests ====================
724
725    #[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    // ==================== List tests ====================
770
771    #[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    // ==================== Special character tests ====================
813
814    #[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        // quick-xml handles escaping automatically
825        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    // ==================== Helper function tests ====================
855
856    #[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    // ==================== Pretty vs compact tests ====================
919
920    #[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    // ==================== use_attributes mode tests ====================
942
943    #[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        // Simple values get value attribute in empty element
955        assert!(xml.contains("value=\"42\""));
956    }
957}