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::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/// 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 - pass structs for schema lookup
91    write_root(&mut writer, &doc.root, config, &doc.structs)?;
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    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    // Add type marker for references to distinguish from strings starting with @
138    if matches!(value, Value::Reference(_)) {
139        elem.push_attribute(("__hedl_type__", "ref"));
140    }
141
142    // For simple values, we can use attributes if configured
143    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            // Empty element for null
171        }
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    // Write each row as an item element
223    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    // Per SPEC.md: Node.fields contains ALL values including ID (first column)
246    // MatrixList.schema includes all column names with ID first
247
248    // Write simple values as attributes if configured
249    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    // Check if we need element content (complex values or children)
259    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        // ISSUE 5 FIX: Write fields as elements
268        // If using attributes mode and have complex fields, write only complex fields as elements
269        // Simple fields are already in attributes, so don't duplicate them
270        if !config.use_attributes {
271            // Not using attributes: write all fields as elements
272            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            // Using attributes but have complex values: write only complex fields as elements
279            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        // else: using attributes and no complex values, all fields already in attributes
286
287        // Write children with marker attribute so they can be recognized on import
288        if let Some(children) = node.children() {
289            for (child_type, child_nodes) in children {
290                for child in child_nodes {
291                    // ISSUE 4 FIX: Look up child schema from structs instead of hardcoding ["id"]
292                    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        // Empty element with all attributes
307        writer
308            .write_event(Event::Empty(elem))
309            .map_err(|e| format!("Failed to write empty node: {}", e))?;
310    }
311
312    Ok(())
313}
314
315/// Write a child node with a marker attribute so it can be recognized as a NEST child on import.
316fn 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    // Add marker attribute to indicate this is a NEST child
327    elem.push_attribute(("__hedl_child__", "true"));
328
329    // Write simple values as attributes if configured
330    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    // Check if we need element content (complex values or children)
340    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        // ISSUE 5 FIX: Write fields as elements (same fix as write_node)
349        if !config.use_attributes {
350            // Not using attributes: write all fields as elements
351            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            // Using attributes but have complex values: write only complex fields as elements
358            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        // else: using attributes and no complex values, all fields already in attributes
365
366        // Write nested children recursively
367        if let Some(children) = node.children() {
368            for (child_type, child_nodes) in children {
369                for child in child_nodes {
370                    // ISSUE 4 FIX: Look up nested child schema from structs
371                    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        // Empty element with all attributes
386        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    // ==================== ToXmlConfig tests ====================
452
453    #[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    // ==================== to_xml basic tests ====================
504
505    #[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    // ==================== Scalar value tests ====================
551
552    #[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        // Null values produce elements with empty content (may have whitespace in pretty mode)
561        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    // ==================== Reference tests ====================
657
658    #[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    // ==================== Expression tests ====================
686
687    #[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    // ==================== Tensor tests ====================
730
731    #[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    // ==================== Object tests ====================
767
768    #[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    // ==================== List tests ====================
813
814    #[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    // ==================== Special character tests ====================
856
857    #[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        // quick-xml handles escaping automatically
868        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    // ==================== Helper function tests ====================
898
899    #[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    // ==================== Pretty vs compact tests ====================
964
965    #[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    // ==================== use_attributes mode tests ====================
987
988    #[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        // Simple values get value attribute in empty element
1000        assert!(xml.contains("value=\"42\""));
1001    }
1002}