Skip to main content

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        Value::List(items) => write_list(writer, items, config)?,
180    }
181    Ok(())
182}
183
184fn write_object<W: std::io::Write>(
185    writer: &mut Writer<W>,
186    key: &str,
187    obj: &BTreeMap<String, Item>,
188    config: &ToXmlConfig,
189    structs: &BTreeMap<String, Vec<String>>,
190) -> Result<(), String> {
191    let elem = BytesStart::new(key);
192    writer
193        .write_event(Event::Start(elem))
194        .map_err(|e| format!("Failed to write object start: {}", e))?;
195
196    for (child_key, child_item) in obj {
197        write_item(writer, child_key, child_item, config, structs)?;
198    }
199
200    writer
201        .write_event(Event::End(BytesEnd::new(key)))
202        .map_err(|e| format!("Failed to write object end: {}", e))?;
203
204    Ok(())
205}
206
207fn write_matrix_list<W: std::io::Write>(
208    writer: &mut Writer<W>,
209    key: &str,
210    list: &MatrixList,
211    config: &ToXmlConfig,
212    structs: &BTreeMap<String, Vec<String>>,
213) -> Result<(), String> {
214    let mut list_elem = BytesStart::new(key);
215    if config.include_metadata {
216        list_elem.push_attribute(("type", list.type_name.as_str()));
217    }
218
219    writer
220        .write_event(Event::Start(list_elem))
221        .map_err(|e| format!("Failed to write list start: {}", e))?;
222
223    // Write each row as an item element
224    let item_name = list.type_name.to_lowercase();
225    for row in &list.rows {
226        write_node(writer, &item_name, row, &list.schema, config, structs)?;
227    }
228
229    writer
230        .write_event(Event::End(BytesEnd::new(key)))
231        .map_err(|e| format!("Failed to write list end: {}", e))?;
232
233    Ok(())
234}
235
236fn write_node<W: std::io::Write>(
237    writer: &mut Writer<W>,
238    elem_name: &str,
239    node: &Node,
240    schema: &[String],
241    config: &ToXmlConfig,
242    structs: &BTreeMap<String, Vec<String>>,
243) -> Result<(), String> {
244    let mut elem = BytesStart::new(elem_name);
245
246    // Per SPEC.md: Node.fields contains ALL values including ID (first column)
247    // MatrixList.schema includes all column names with ID first
248
249    // Write simple values as attributes if configured
250    if config.use_attributes {
251        for (i, field) in node.fields.iter().enumerate() {
252            if is_simple_value(field) && i < schema.len() {
253                let attr_value = escape_attribute_value(field);
254                elem.push_attribute((schema[i].as_str(), attr_value.as_str()));
255            }
256        }
257    }
258
259    // Check if we need element content (complex values or children)
260    let has_complex_values = node.fields.iter().any(|v| !is_simple_value(v));
261    let has_children = node.children().map(|c| !c.is_empty()).unwrap_or(false);
262
263    if !config.use_attributes || has_complex_values || has_children {
264        writer
265            .write_event(Event::Start(elem))
266            .map_err(|e| format!("Failed to write node start: {}", e))?;
267
268        // ISSUE 5 FIX: Write fields as elements
269        // If using attributes mode and have complex fields, write only complex fields as elements
270        // Simple fields are already in attributes, so don't duplicate them
271        if !config.use_attributes {
272            // Not using attributes: write all fields as elements
273            for (i, field) in node.fields.iter().enumerate() {
274                if i < schema.len() {
275                    write_scalar_element(writer, &schema[i], field, config)?;
276                }
277            }
278        } else if has_complex_values {
279            // Using attributes but have complex values: write only complex fields as elements
280            for (i, field) in node.fields.iter().enumerate() {
281                if i < schema.len() && !is_simple_value(field) {
282                    write_scalar_element(writer, &schema[i], field, config)?;
283                }
284            }
285        }
286        // else: using attributes and no complex values, all fields already in attributes
287
288        // Write children with marker attribute so they can be recognized on import
289        if let Some(children) = node.children() {
290            for (child_type, child_nodes) in children {
291                for child in child_nodes {
292                    // ISSUE 4 FIX: Look up child schema from structs instead of hardcoding ["id"]
293                    let default_schema = vec!["id".to_string()];
294                    let child_schema = structs
295                        .get(child_type)
296                        .map(|s| s.as_slice())
297                        .unwrap_or(&default_schema);
298                    write_child_node(writer, child_type, child, child_schema, config, structs)?;
299                }
300            }
301        }
302
303        writer
304            .write_event(Event::End(BytesEnd::new(elem_name)))
305            .map_err(|e| format!("Failed to write node end: {}", e))?;
306    } else {
307        // Empty element with all attributes
308        writer
309            .write_event(Event::Empty(elem))
310            .map_err(|e| format!("Failed to write empty node: {}", e))?;
311    }
312
313    Ok(())
314}
315
316/// Write a child node with a marker attribute so it can be recognized as a NEST child on import.
317fn write_child_node<W: std::io::Write>(
318    writer: &mut Writer<W>,
319    elem_name: &str,
320    node: &Node,
321    schema: &[String],
322    config: &ToXmlConfig,
323    structs: &BTreeMap<String, Vec<String>>,
324) -> Result<(), String> {
325    let mut elem = BytesStart::new(elem_name);
326
327    // Add marker attribute to indicate this is a NEST child
328    elem.push_attribute(("__hedl_child__", "true"));
329
330    // Write simple values as attributes if configured
331    if config.use_attributes {
332        for (i, field) in node.fields.iter().enumerate() {
333            if is_simple_value(field) && i < schema.len() {
334                let attr_value = escape_attribute_value(field);
335                elem.push_attribute((schema[i].as_str(), attr_value.as_str()));
336            }
337        }
338    }
339
340    // Check if we need element content (complex values or children)
341    let has_complex_values = node.fields.iter().any(|v| !is_simple_value(v));
342    let has_children = node.children().map(|c| !c.is_empty()).unwrap_or(false);
343
344    if !config.use_attributes || has_complex_values || has_children {
345        writer
346            .write_event(Event::Start(elem))
347            .map_err(|e| format!("Failed to write child node start: {}", e))?;
348
349        // ISSUE 5 FIX: Write fields as elements (same fix as write_node)
350        if !config.use_attributes {
351            // Not using attributes: write all fields as elements
352            for (i, field) in node.fields.iter().enumerate() {
353                if i < schema.len() {
354                    write_scalar_element(writer, &schema[i], field, config)?;
355                }
356            }
357        } else if has_complex_values {
358            // Using attributes but have complex values: write only complex fields as elements
359            for (i, field) in node.fields.iter().enumerate() {
360                if i < schema.len() && !is_simple_value(field) {
361                    write_scalar_element(writer, &schema[i], field, config)?;
362                }
363            }
364        }
365        // else: using attributes and no complex values, all fields already in attributes
366
367        // Write nested children recursively
368        if let Some(children) = node.children() {
369            for (child_type, child_nodes) in children {
370                for child in child_nodes {
371                    // ISSUE 4 FIX: Look up nested child schema from structs
372                    let default_schema = vec!["id".to_string()];
373                    let child_schema = structs
374                        .get(child_type)
375                        .map(|s| s.as_slice())
376                        .unwrap_or(&default_schema);
377                    write_child_node(writer, child_type, child, child_schema, config, structs)?;
378                }
379            }
380        }
381
382        writer
383            .write_event(Event::End(BytesEnd::new(elem_name)))
384            .map_err(|e| format!("Failed to write child node end: {}", e))?;
385    } else {
386        // Empty element with all attributes
387        writer
388            .write_event(Event::Empty(elem))
389            .map_err(|e| format!("Failed to write empty child node: {}", e))?;
390    }
391
392    Ok(())
393}
394
395fn write_tensor<W: std::io::Write>(
396    writer: &mut Writer<W>,
397    tensor: &Tensor,
398    _config: &ToXmlConfig,
399) -> Result<(), String> {
400    match tensor {
401        Tensor::Scalar(n) => write_text(writer, &n.to_string())?,
402        Tensor::Array(items) => {
403            for item in items {
404                let elem = BytesStart::new("item");
405                writer
406                    .write_event(Event::Start(elem))
407                    .map_err(|e| format!("Failed to write tensor item start: {}", e))?;
408
409                write_tensor(writer, item, _config)?;
410
411                writer
412                    .write_event(Event::End(BytesEnd::new("item")))
413                    .map_err(|e| format!("Failed to write tensor item end: {}", e))?;
414            }
415        }
416    }
417    Ok(())
418}
419
420/// Write a list (from `Value::List`) to XML.
421/// Lists are written as sequences of <item> elements containing the individual values.
422fn write_list<W: std::io::Write>(
423    writer: &mut Writer<W>,
424    items: &[Value],
425    config: &ToXmlConfig,
426) -> Result<(), String> {
427    for item in items {
428        let elem = BytesStart::new("item");
429        writer
430            .write_event(Event::Start(elem))
431            .map_err(|e| format!("Failed to write list item start: {}", e))?;
432
433        write_value_content(writer, item, config)?;
434
435        writer
436            .write_event(Event::End(BytesEnd::new("item")))
437            .map_err(|e| format!("Failed to write list item end: {}", e))?;
438    }
439    Ok(())
440}
441
442fn write_text<W: std::io::Write>(writer: &mut Writer<W>, text: &str) -> Result<(), String> {
443    writer
444        .write_event(Event::Text(BytesText::new(text)))
445        .map_err(|e| format!("Failed to write text: {}", e))
446}
447
448fn is_simple_value(value: &Value) -> bool {
449    matches!(
450        value,
451        Value::Null | Value::Bool(_) | Value::Int(_) | Value::Float(_) | Value::String(_)
452    )
453}
454
455fn escape_attribute_value(value: &Value) -> String {
456    match value {
457        Value::Null => String::new(),
458        Value::Bool(b) => b.to_string(),
459        Value::Int(n) => n.to_string(),
460        Value::Float(f) => f.to_string(),
461        Value::String(s) => s.to_string(),
462        Value::Reference(r) => r.to_ref_string(),
463        Value::Expression(e) => format!("$({})", e),
464        Value::Tensor(_) => "[tensor]".to_string(),
465        Value::List(_) => "[list]".to_string(),
466    }
467}
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472    use hedl_core::lex::{Expression, Span};
473    use hedl_core::{Document, Reference};
474
475    // ==================== ToXmlConfig tests ====================
476
477    #[test]
478    fn test_to_xml_config_default() {
479        let config = ToXmlConfig::default();
480        assert!(config.pretty);
481        assert_eq!(config.indent, "  ");
482        assert_eq!(config.root_element, "hedl");
483        assert!(!config.include_metadata);
484        assert!(!config.use_attributes);
485    }
486
487    #[test]
488    fn test_to_xml_config_debug() {
489        let config = ToXmlConfig::default();
490        let debug = format!("{:?}", config);
491        assert!(debug.contains("ToXmlConfig"));
492        assert!(debug.contains("pretty"));
493        assert!(debug.contains("indent"));
494        assert!(debug.contains("root_element"));
495    }
496
497    #[test]
498    fn test_to_xml_config_clone() {
499        let config = ToXmlConfig {
500            pretty: false,
501            indent: "\t".to_string(),
502            root_element: "custom".to_string(),
503            include_metadata: true,
504            use_attributes: true,
505        };
506        let cloned = config.clone();
507        assert!(!cloned.pretty);
508        assert_eq!(cloned.indent, "\t");
509        assert_eq!(cloned.root_element, "custom");
510        assert!(cloned.include_metadata);
511        assert!(cloned.use_attributes);
512    }
513
514    #[test]
515    fn test_to_xml_config_all_options() {
516        let config = ToXmlConfig {
517            pretty: true,
518            indent: "    ".to_string(),
519            root_element: "document".to_string(),
520            include_metadata: true,
521            use_attributes: true,
522        };
523        assert!(config.pretty);
524        assert_eq!(config.indent.len(), 4);
525    }
526
527    // ==================== to_xml basic tests ====================
528
529    #[test]
530    fn test_empty_document() {
531        let doc = Document::new((2, 0));
532        let config = ToXmlConfig::default();
533        let xml = to_xml(&doc, &config).unwrap();
534        assert!(xml.contains("<?xml"));
535        assert!(xml.contains("<hedl"));
536        assert!(xml.contains("</hedl>"));
537    }
538
539    #[test]
540    fn test_empty_document_compact() {
541        let doc = Document::new((2, 0));
542        let config = ToXmlConfig {
543            pretty: false,
544            ..Default::default()
545        };
546        let xml = to_xml(&doc, &config).unwrap();
547        assert!(xml.contains("<?xml"));
548        assert!(xml.contains("<hedl></hedl>"));
549    }
550
551    #[test]
552    fn test_custom_root_element() {
553        let doc = Document::new((2, 0));
554        let config = ToXmlConfig {
555            root_element: "custom_root".to_string(),
556            ..Default::default()
557        };
558        let xml = to_xml(&doc, &config).unwrap();
559        assert!(xml.contains("<custom_root"));
560        assert!(xml.contains("</custom_root>"));
561    }
562
563    #[test]
564    fn test_with_metadata() {
565        let doc = Document::new((2, 5));
566        let config = ToXmlConfig {
567            include_metadata: true,
568            ..Default::default()
569        };
570        let xml = to_xml(&doc, &config).unwrap();
571        assert!(xml.contains("version=\"2.5\""));
572    }
573
574    // ==================== Scalar value tests ====================
575
576    #[test]
577    fn test_scalar_null() {
578        let mut doc = Document::new((2, 0));
579        doc.root
580            .insert("null_val".to_string(), Item::Scalar(Value::Null));
581
582        let config = ToXmlConfig::default();
583        let xml = to_xml(&doc, &config).unwrap();
584        // Null values produce elements with empty content (may have whitespace in pretty mode)
585        assert!(xml.contains("<null_val>") && xml.contains("</null_val>"));
586    }
587
588    #[test]
589    fn test_scalar_bool_true() {
590        let mut doc = Document::new((2, 0));
591        doc.root
592            .insert("val".to_string(), Item::Scalar(Value::Bool(true)));
593
594        let config = ToXmlConfig::default();
595        let xml = to_xml(&doc, &config).unwrap();
596        assert!(xml.contains("<val>true</val>"));
597    }
598
599    #[test]
600    fn test_scalar_bool_false() {
601        let mut doc = Document::new((2, 0));
602        doc.root
603            .insert("val".to_string(), Item::Scalar(Value::Bool(false)));
604
605        let config = ToXmlConfig::default();
606        let xml = to_xml(&doc, &config).unwrap();
607        assert!(xml.contains("<val>false</val>"));
608    }
609
610    #[test]
611    fn test_scalar_int_positive() {
612        let mut doc = Document::new((2, 0));
613        doc.root
614            .insert("val".to_string(), Item::Scalar(Value::Int(42)));
615
616        let config = ToXmlConfig::default();
617        let xml = to_xml(&doc, &config).unwrap();
618        assert!(xml.contains("<val>42</val>"));
619    }
620
621    #[test]
622    fn test_scalar_int_negative() {
623        let mut doc = Document::new((2, 0));
624        doc.root
625            .insert("val".to_string(), Item::Scalar(Value::Int(-100)));
626
627        let config = ToXmlConfig::default();
628        let xml = to_xml(&doc, &config).unwrap();
629        assert!(xml.contains("<val>-100</val>"));
630    }
631
632    #[test]
633    fn test_scalar_int_zero() {
634        let mut doc = Document::new((2, 0));
635        doc.root
636            .insert("val".to_string(), Item::Scalar(Value::Int(0)));
637
638        let config = ToXmlConfig::default();
639        let xml = to_xml(&doc, &config).unwrap();
640        assert!(xml.contains("<val>0</val>"));
641    }
642
643    #[test]
644    fn test_scalar_float() {
645        let mut doc = Document::new((2, 0));
646        doc.root
647            .insert("val".to_string(), Item::Scalar(Value::Float(3.5)));
648
649        let config = ToXmlConfig::default();
650        let xml = to_xml(&doc, &config).unwrap();
651        assert!(xml.contains("<val>3.5</val>"));
652    }
653
654    #[test]
655    fn test_scalar_string() {
656        let mut doc = Document::new((2, 0));
657        doc.root.insert(
658            "val".to_string(),
659            Item::Scalar(Value::String("hello".to_string().into())),
660        );
661
662        let config = ToXmlConfig::default();
663        let xml = to_xml(&doc, &config).unwrap();
664        assert!(xml.contains("<val>hello</val>"));
665    }
666
667    #[test]
668    fn test_scalar_string_empty() {
669        let mut doc = Document::new((2, 0));
670        doc.root.insert(
671            "val".to_string(),
672            Item::Scalar(Value::String("".to_string().into())),
673        );
674
675        let config = ToXmlConfig::default();
676        let xml = to_xml(&doc, &config).unwrap();
677        assert!(xml.contains("<val></val>") || xml.contains("<val/>"));
678    }
679
680    // ==================== Reference tests ====================
681
682    #[test]
683    fn test_scalar_reference_local() {
684        let mut doc = Document::new((2, 0));
685        doc.root.insert(
686            "ref".to_string(),
687            Item::Scalar(Value::Reference(Reference::local("user123"))),
688        );
689
690        let config = ToXmlConfig::default();
691        let xml = to_xml(&doc, &config).unwrap();
692        assert!(xml.contains("@user123"));
693        assert!(xml.contains("__hedl_type__=\"ref\""));
694    }
695
696    #[test]
697    fn test_scalar_reference_qualified() {
698        let mut doc = Document::new((2, 0));
699        doc.root.insert(
700            "ref".to_string(),
701            Item::Scalar(Value::Reference(Reference::qualified("User", "456"))),
702        );
703
704        let config = ToXmlConfig::default();
705        let xml = to_xml(&doc, &config).unwrap();
706        assert!(xml.contains("@User:456"));
707    }
708
709    // ==================== Expression tests ====================
710
711    #[test]
712    fn test_scalar_expression_identifier() {
713        let mut doc = Document::new((2, 0));
714        doc.root.insert(
715            "expr".to_string(),
716            Item::Scalar(Value::Expression(Box::new(Expression::Identifier {
717                name: "foo".to_string(),
718                span: Span::synthetic(),
719            }))),
720        );
721
722        let config = ToXmlConfig::default();
723        let xml = to_xml(&doc, &config).unwrap();
724        assert!(xml.contains("$(foo)"));
725    }
726
727    #[test]
728    fn test_scalar_expression_call() {
729        let mut doc = Document::new((2, 0));
730        doc.root.insert(
731            "expr".to_string(),
732            Item::Scalar(Value::Expression(Box::new(Expression::Call {
733                name: "add".to_string(),
734                args: vec![
735                    Expression::Identifier {
736                        name: "x".to_string(),
737                        span: Span::synthetic(),
738                    },
739                    Expression::Literal {
740                        value: hedl_core::lex::ExprLiteral::Int(1),
741                        span: Span::synthetic(),
742                    },
743                ],
744                span: Span::synthetic(),
745            }))),
746        );
747
748        let config = ToXmlConfig::default();
749        let xml = to_xml(&doc, &config).unwrap();
750        assert!(xml.contains("$(add(x, 1))"));
751    }
752
753    // ==================== Tensor tests ====================
754
755    #[test]
756    fn test_tensor_1d() {
757        let mut doc = Document::new((2, 0));
758        let tensor = Tensor::Array(vec![
759            Tensor::Scalar(1.0),
760            Tensor::Scalar(2.0),
761            Tensor::Scalar(3.0),
762        ]);
763        doc.root.insert(
764            "tensor".to_string(),
765            Item::Scalar(Value::Tensor(Box::new(tensor))),
766        );
767
768        let config = ToXmlConfig::default();
769        let xml = to_xml(&doc, &config).unwrap();
770        assert!(xml.contains("<tensor>"));
771        assert!(xml.contains("<item>1</item>"));
772        assert!(xml.contains("<item>2</item>"));
773        assert!(xml.contains("<item>3</item>"));
774    }
775
776    #[test]
777    fn test_tensor_scalar() {
778        let mut doc = Document::new((2, 0));
779        let tensor = Tensor::Scalar(42.5);
780        doc.root.insert(
781            "tensor".to_string(),
782            Item::Scalar(Value::Tensor(Box::new(tensor))),
783        );
784
785        let config = ToXmlConfig::default();
786        let xml = to_xml(&doc, &config).unwrap();
787        assert!(xml.contains("<tensor>42.5</tensor>"));
788    }
789
790    // ==================== Object tests ====================
791
792    #[test]
793    fn test_nested_object() {
794        let mut doc = Document::new((2, 0));
795        let mut inner = BTreeMap::new();
796        inner.insert(
797            "name".to_string(),
798            Item::Scalar(Value::String("test".to_string().into())),
799        );
800        inner.insert("value".to_string(), Item::Scalar(Value::Int(100)));
801        doc.root.insert("config".to_string(), Item::Object(inner));
802
803        let config = ToXmlConfig::default();
804        let xml = to_xml(&doc, &config).unwrap();
805
806        assert!(xml.contains("<config>"));
807        assert!(xml.contains("<name>test</name>"));
808        assert!(xml.contains("<value>100</value>"));
809        assert!(xml.contains("</config>"));
810    }
811
812    #[test]
813    fn test_deeply_nested_object() {
814        let mut doc = Document::new((2, 0));
815
816        let mut level3 = BTreeMap::new();
817        level3.insert("deep".to_string(), Item::Scalar(Value::Int(42)));
818
819        let mut level2 = BTreeMap::new();
820        level2.insert("nested".to_string(), Item::Object(level3));
821
822        let mut level1 = BTreeMap::new();
823        level1.insert("inner".to_string(), Item::Object(level2));
824
825        doc.root.insert("outer".to_string(), Item::Object(level1));
826
827        let config = ToXmlConfig::default();
828        let xml = to_xml(&doc, &config).unwrap();
829
830        assert!(xml.contains("<outer>"));
831        assert!(xml.contains("<inner>"));
832        assert!(xml.contains("<nested>"));
833        assert!(xml.contains("<deep>42</deep>"));
834    }
835
836    // ==================== List tests ====================
837
838    #[test]
839    fn test_matrix_list() {
840        let mut doc = Document::new((2, 0));
841        let mut list = MatrixList::new("User", vec!["id".to_string(), "name".to_string()]);
842        list.add_row(Node::new(
843            "User",
844            "u1",
845            vec![
846                Value::String("u1".to_string().into()),
847                Value::String("Alice".to_string().into()),
848            ],
849        ));
850        doc.root.insert("users".to_string(), Item::List(list));
851
852        let config = ToXmlConfig::default();
853        let xml = to_xml(&doc, &config).unwrap();
854
855        assert!(xml.contains("<users>"));
856        assert!(xml.contains("<user>"));
857        assert!(xml.contains("</users>"));
858    }
859
860    #[test]
861    fn test_matrix_list_with_metadata() {
862        let mut doc = Document::new((2, 0));
863        let mut list = MatrixList::new("User", vec!["id".to_string()]);
864        list.add_row(Node::new(
865            "User",
866            "u1",
867            vec![Value::String("u1".to_string().into())],
868        ));
869        doc.root.insert("users".to_string(), Item::List(list));
870
871        let config = ToXmlConfig {
872            include_metadata: true,
873            ..Default::default()
874        };
875        let xml = to_xml(&doc, &config).unwrap();
876        assert!(xml.contains("type=\"User\""));
877    }
878
879    // ==================== Special character tests ====================
880
881    #[test]
882    fn test_special_characters_ampersand() {
883        let mut doc = Document::new((2, 0));
884        doc.root.insert(
885            "text".to_string(),
886            Item::Scalar(Value::String("hello & goodbye".to_string().into())),
887        );
888
889        let config = ToXmlConfig::default();
890        let xml = to_xml(&doc, &config).unwrap();
891        // quick-xml handles escaping automatically
892        assert!(xml.contains("<text>"));
893    }
894
895    #[test]
896    fn test_special_characters_angle_brackets() {
897        let mut doc = Document::new((2, 0));
898        doc.root.insert(
899            "text".to_string(),
900            Item::Scalar(Value::String("hello <tag> goodbye".to_string().into())),
901        );
902
903        let config = ToXmlConfig::default();
904        let xml = to_xml(&doc, &config).unwrap();
905        assert!(xml.contains("<text>"));
906    }
907
908    #[test]
909    fn test_special_characters_quotes() {
910        let mut doc = Document::new((2, 0));
911        doc.root.insert(
912            "text".to_string(),
913            Item::Scalar(Value::String("hello \"quoted\"".to_string().into())),
914        );
915
916        let config = ToXmlConfig::default();
917        let xml = to_xml(&doc, &config).unwrap();
918        assert!(xml.contains("<text>"));
919    }
920
921    // ==================== Helper function tests ====================
922
923    #[test]
924    fn test_is_simple_value() {
925        assert!(is_simple_value(&Value::Null));
926        assert!(is_simple_value(&Value::Bool(true)));
927        assert!(is_simple_value(&Value::Int(42)));
928        assert!(is_simple_value(&Value::Float(3.5)));
929        assert!(is_simple_value(&Value::String("hello".to_string().into())));
930        assert!(!is_simple_value(&Value::Reference(Reference::local("x"))));
931        assert!(!is_simple_value(&Value::Tensor(Box::new(Tensor::Scalar(
932            1.0
933        )))));
934    }
935
936    #[test]
937    fn test_escape_attribute_value_null() {
938        assert_eq!(escape_attribute_value(&Value::Null), "");
939    }
940
941    #[test]
942    fn test_escape_attribute_value_bool() {
943        assert_eq!(escape_attribute_value(&Value::Bool(true)), "true");
944        assert_eq!(escape_attribute_value(&Value::Bool(false)), "false");
945    }
946
947    #[test]
948    fn test_escape_attribute_value_int() {
949        assert_eq!(escape_attribute_value(&Value::Int(42)), "42");
950        assert_eq!(escape_attribute_value(&Value::Int(-100)), "-100");
951    }
952
953    #[test]
954    fn test_escape_attribute_value_float() {
955        assert_eq!(escape_attribute_value(&Value::Float(3.5)), "3.5");
956    }
957
958    #[test]
959    fn test_escape_attribute_value_string() {
960        assert_eq!(
961            escape_attribute_value(&Value::String("hello".to_string().into())),
962            "hello"
963        );
964    }
965
966    #[test]
967    fn test_escape_attribute_value_reference() {
968        let ref_val = Value::Reference(Reference::local("user1"));
969        assert_eq!(escape_attribute_value(&ref_val), "@user1");
970    }
971
972    #[test]
973    fn test_escape_attribute_value_expression() {
974        let expr = Value::Expression(Box::new(Expression::Identifier {
975            name: "foo".to_string(),
976            span: Span::default(),
977        }));
978        assert_eq!(escape_attribute_value(&expr), "$(foo)");
979    }
980
981    #[test]
982    fn test_escape_attribute_value_tensor() {
983        let tensor = Value::Tensor(Box::new(Tensor::Scalar(1.0)));
984        assert_eq!(escape_attribute_value(&tensor), "[tensor]");
985    }
986
987    #[test]
988    fn test_escape_attribute_value_list() {
989        let list = Value::List(Box::new(vec![
990            Value::String("a".to_string().into()),
991            Value::String("b".to_string().into()),
992        ]));
993        assert_eq!(escape_attribute_value(&list), "[list]");
994    }
995
996    // ==================== List value tests ====================
997
998    #[test]
999    fn test_list_string_values() {
1000        let mut doc = Document::new((1, 1));
1001        doc.root.insert(
1002            "roles".to_string(),
1003            Item::Scalar(Value::List(Box::new(vec![
1004                Value::String("admin".to_string().into()),
1005                Value::String("editor".to_string().into()),
1006                Value::String("viewer".to_string().into()),
1007            ]))),
1008        );
1009
1010        let config = ToXmlConfig::default();
1011        let xml = to_xml(&doc, &config).unwrap();
1012        assert!(xml.contains("<roles>"));
1013        assert!(xml.contains("<item>admin</item>"));
1014        assert!(xml.contains("<item>editor</item>"));
1015        assert!(xml.contains("<item>viewer</item>"));
1016        assert!(xml.contains("</roles>"));
1017    }
1018
1019    #[test]
1020    fn test_list_bool_values() {
1021        let mut doc = Document::new((1, 1));
1022        doc.root.insert(
1023            "flags".to_string(),
1024            Item::Scalar(Value::List(Box::new(vec![
1025                Value::Bool(true),
1026                Value::Bool(false),
1027                Value::Bool(true),
1028            ]))),
1029        );
1030
1031        let config = ToXmlConfig::default();
1032        let xml = to_xml(&doc, &config).unwrap();
1033        assert!(xml.contains("<item>true</item>"));
1034        assert!(xml.contains("<item>false</item>"));
1035    }
1036
1037    #[test]
1038    fn test_list_mixed_values() {
1039        let mut doc = Document::new((1, 1));
1040        doc.root.insert(
1041            "mixed".to_string(),
1042            Item::Scalar(Value::List(Box::new(vec![
1043                Value::String("text".to_string().into()),
1044                Value::Int(42),
1045                Value::Bool(true),
1046                Value::Null,
1047            ]))),
1048        );
1049
1050        let config = ToXmlConfig::default();
1051        let xml = to_xml(&doc, &config).unwrap();
1052        assert!(xml.contains("<item>text</item>"));
1053        assert!(xml.contains("<item>42</item>"));
1054        assert!(xml.contains("<item>true</item>"));
1055    }
1056
1057    #[test]
1058    fn test_list_empty() {
1059        let mut doc = Document::new((1, 1));
1060        doc.root.insert(
1061            "empty".to_string(),
1062            Item::Scalar(Value::List(Box::default())),
1063        );
1064
1065        let config = ToXmlConfig::default();
1066        let xml = to_xml(&doc, &config).unwrap();
1067        assert!(xml.contains("<empty>"));
1068        assert!(xml.contains("</empty>"));
1069    }
1070
1071    #[test]
1072    fn test_list_with_references() {
1073        let mut doc = Document::new((1, 1));
1074        doc.root.insert(
1075            "refs".to_string(),
1076            Item::Scalar(Value::List(Box::new(vec![
1077                Value::Reference(Reference::local("user1")),
1078                Value::Reference(Reference::qualified("User", "2")),
1079            ]))),
1080        );
1081
1082        let config = ToXmlConfig::default();
1083        let xml = to_xml(&doc, &config).unwrap();
1084        assert!(xml.contains("<item>@user1</item>"));
1085        assert!(xml.contains("<item>@User:2</item>"));
1086    }
1087
1088    // ==================== Pretty vs compact tests ====================
1089
1090    #[test]
1091    fn test_pretty_vs_compact() {
1092        let mut doc = Document::new((2, 0));
1093        doc.root
1094            .insert("val".to_string(), Item::Scalar(Value::Int(42)));
1095
1096        let config_pretty = ToXmlConfig {
1097            pretty: true,
1098            ..Default::default()
1099        };
1100        let config_compact = ToXmlConfig {
1101            pretty: false,
1102            ..Default::default()
1103        };
1104
1105        let xml_pretty = to_xml(&doc, &config_pretty).unwrap();
1106        let xml_compact = to_xml(&doc, &config_compact).unwrap();
1107
1108        assert!(xml_pretty.len() > xml_compact.len());
1109    }
1110
1111    // ==================== use_attributes mode tests ====================
1112
1113    #[test]
1114    fn test_use_attributes_simple() {
1115        let mut doc = Document::new((2, 0));
1116        doc.root
1117            .insert("val".to_string(), Item::Scalar(Value::Int(42)));
1118
1119        let config = ToXmlConfig {
1120            use_attributes: true,
1121            ..Default::default()
1122        };
1123        let xml = to_xml(&doc, &config).unwrap();
1124        // Simple values get value attribute in empty element
1125        assert!(xml.contains("value=\"42\""));
1126    }
1127}