use hedl_core::lex::Tensor;
use hedl_core::{Document, Item, MatrixList, Node, Value};
use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
use quick_xml::Writer;
use std::collections::BTreeMap;
use std::io::Cursor;
#[derive(Debug, Clone)]
pub struct ToXmlConfig {
pub pretty: bool,
pub indent: String,
pub root_element: String,
pub include_metadata: bool,
pub use_attributes: bool,
}
impl Default for ToXmlConfig {
fn default() -> Self {
Self {
pretty: true,
indent: " ".to_string(),
root_element: "hedl".to_string(),
include_metadata: false,
use_attributes: false,
}
}
}
impl hedl_core::convert::ExportConfig for ToXmlConfig {
fn include_metadata(&self) -> bool {
self.include_metadata
}
fn pretty(&self) -> bool {
self.pretty
}
}
pub fn to_xml(doc: &Document, config: &ToXmlConfig) -> Result<String, String> {
let mut writer = if config.pretty {
Writer::new_with_indent(Cursor::new(Vec::new()), b' ', config.indent.len())
} else {
Writer::new(Cursor::new(Vec::new()))
};
writer
.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))
.map_err(|e| format!("Failed to write XML declaration: {}", e))?;
let mut root = BytesStart::new(&config.root_element);
if config.include_metadata {
root.push_attribute((
"version",
format!("{}.{}", doc.version.0, doc.version.1).as_str(),
));
}
writer
.write_event(Event::Start(root))
.map_err(|e| format!("Failed to write root element: {}", e))?;
write_root(&mut writer, &doc.root, config, &doc.structs)?;
writer
.write_event(Event::End(BytesEnd::new(&config.root_element)))
.map_err(|e| format!("Failed to close root element: {}", e))?;
let result = writer.into_inner().into_inner();
String::from_utf8(result).map_err(|e| format!("Invalid UTF-8 in XML output: {}", e))
}
fn write_root<W: std::io::Write>(
writer: &mut Writer<W>,
root: &BTreeMap<String, Item>,
config: &ToXmlConfig,
structs: &BTreeMap<String, Vec<String>>,
) -> Result<(), String> {
for (key, item) in root {
write_item(writer, key, item, config, structs)?;
}
Ok(())
}
fn write_item<W: std::io::Write>(
writer: &mut Writer<W>,
key: &str,
item: &Item,
config: &ToXmlConfig,
structs: &BTreeMap<String, Vec<String>>,
) -> Result<(), String> {
match item {
Item::Scalar(value) => write_scalar_element(writer, key, value, config)?,
Item::Object(obj) => write_object(writer, key, obj, config, structs)?,
Item::List(list) => write_matrix_list(writer, key, list, config, structs)?,
}
Ok(())
}
fn write_scalar_element<W: std::io::Write>(
writer: &mut Writer<W>,
key: &str,
value: &Value,
config: &ToXmlConfig,
) -> Result<(), String> {
let mut elem = BytesStart::new(key);
if matches!(value, Value::Reference(_)) {
elem.push_attribute(("__hedl_type__", "ref"));
}
if config.use_attributes && is_simple_value(value) {
elem.push_attribute(("value", escape_attribute_value(value).as_str()));
writer
.write_event(Event::Empty(elem))
.map_err(|e| format!("Failed to write empty element: {}", e))?;
} else {
writer
.write_event(Event::Start(elem.clone()))
.map_err(|e| format!("Failed to write start element: {}", e))?;
write_value_content(writer, value, config)?;
writer
.write_event(Event::End(BytesEnd::new(key)))
.map_err(|e| format!("Failed to write end element: {}", e))?;
}
Ok(())
}
fn write_value_content<W: std::io::Write>(
writer: &mut Writer<W>,
value: &Value,
config: &ToXmlConfig,
) -> Result<(), String> {
match value {
Value::Null => {
}
Value::Bool(b) => write_text(writer, &b.to_string())?,
Value::Int(n) => write_text(writer, &n.to_string())?,
Value::Float(f) => write_text(writer, &f.to_string())?,
Value::String(s) => write_text(writer, s)?,
Value::Tensor(t) => write_tensor(writer, t, config)?,
Value::Reference(r) => write_text(writer, &r.to_ref_string())?,
Value::Expression(e) => write_text(writer, &format!("$({})", e))?,
Value::List(items) => write_list(writer, items, config)?,
}
Ok(())
}
fn write_object<W: std::io::Write>(
writer: &mut Writer<W>,
key: &str,
obj: &BTreeMap<String, Item>,
config: &ToXmlConfig,
structs: &BTreeMap<String, Vec<String>>,
) -> Result<(), String> {
let elem = BytesStart::new(key);
writer
.write_event(Event::Start(elem))
.map_err(|e| format!("Failed to write object start: {}", e))?;
for (child_key, child_item) in obj {
write_item(writer, child_key, child_item, config, structs)?;
}
writer
.write_event(Event::End(BytesEnd::new(key)))
.map_err(|e| format!("Failed to write object end: {}", e))?;
Ok(())
}
fn write_matrix_list<W: std::io::Write>(
writer: &mut Writer<W>,
key: &str,
list: &MatrixList,
config: &ToXmlConfig,
structs: &BTreeMap<String, Vec<String>>,
) -> Result<(), String> {
let mut list_elem = BytesStart::new(key);
if config.include_metadata {
list_elem.push_attribute(("type", list.type_name.as_str()));
}
writer
.write_event(Event::Start(list_elem))
.map_err(|e| format!("Failed to write list start: {}", e))?;
let item_name = list.type_name.to_lowercase();
for row in &list.rows {
write_node(writer, &item_name, row, &list.schema, config, structs)?;
}
writer
.write_event(Event::End(BytesEnd::new(key)))
.map_err(|e| format!("Failed to write list end: {}", e))?;
Ok(())
}
fn write_node<W: std::io::Write>(
writer: &mut Writer<W>,
elem_name: &str,
node: &Node,
schema: &[String],
config: &ToXmlConfig,
structs: &BTreeMap<String, Vec<String>>,
) -> Result<(), String> {
let mut elem = BytesStart::new(elem_name);
if config.use_attributes {
for (i, field) in node.fields.iter().enumerate() {
if is_simple_value(field) && i < schema.len() {
let attr_value = escape_attribute_value(field);
elem.push_attribute((schema[i].as_str(), attr_value.as_str()));
}
}
}
let has_complex_values = node.fields.iter().any(|v| !is_simple_value(v));
let has_children = node.children().map(|c| !c.is_empty()).unwrap_or(false);
if !config.use_attributes || has_complex_values || has_children {
writer
.write_event(Event::Start(elem))
.map_err(|e| format!("Failed to write node start: {}", e))?;
if !config.use_attributes {
for (i, field) in node.fields.iter().enumerate() {
if i < schema.len() {
write_scalar_element(writer, &schema[i], field, config)?;
}
}
} else if has_complex_values {
for (i, field) in node.fields.iter().enumerate() {
if i < schema.len() && !is_simple_value(field) {
write_scalar_element(writer, &schema[i], field, config)?;
}
}
}
if let Some(children) = node.children() {
for (child_type, child_nodes) in children {
for child in child_nodes {
let default_schema = vec!["id".to_string()];
let child_schema = structs
.get(child_type)
.map(|s| s.as_slice())
.unwrap_or(&default_schema);
write_child_node(writer, child_type, child, child_schema, config, structs)?;
}
}
}
writer
.write_event(Event::End(BytesEnd::new(elem_name)))
.map_err(|e| format!("Failed to write node end: {}", e))?;
} else {
writer
.write_event(Event::Empty(elem))
.map_err(|e| format!("Failed to write empty node: {}", e))?;
}
Ok(())
}
fn write_child_node<W: std::io::Write>(
writer: &mut Writer<W>,
elem_name: &str,
node: &Node,
schema: &[String],
config: &ToXmlConfig,
structs: &BTreeMap<String, Vec<String>>,
) -> Result<(), String> {
let mut elem = BytesStart::new(elem_name);
elem.push_attribute(("__hedl_child__", "true"));
if config.use_attributes {
for (i, field) in node.fields.iter().enumerate() {
if is_simple_value(field) && i < schema.len() {
let attr_value = escape_attribute_value(field);
elem.push_attribute((schema[i].as_str(), attr_value.as_str()));
}
}
}
let has_complex_values = node.fields.iter().any(|v| !is_simple_value(v));
let has_children = node.children().map(|c| !c.is_empty()).unwrap_or(false);
if !config.use_attributes || has_complex_values || has_children {
writer
.write_event(Event::Start(elem))
.map_err(|e| format!("Failed to write child node start: {}", e))?;
if !config.use_attributes {
for (i, field) in node.fields.iter().enumerate() {
if i < schema.len() {
write_scalar_element(writer, &schema[i], field, config)?;
}
}
} else if has_complex_values {
for (i, field) in node.fields.iter().enumerate() {
if i < schema.len() && !is_simple_value(field) {
write_scalar_element(writer, &schema[i], field, config)?;
}
}
}
if let Some(children) = node.children() {
for (child_type, child_nodes) in children {
for child in child_nodes {
let default_schema = vec!["id".to_string()];
let child_schema = structs
.get(child_type)
.map(|s| s.as_slice())
.unwrap_or(&default_schema);
write_child_node(writer, child_type, child, child_schema, config, structs)?;
}
}
}
writer
.write_event(Event::End(BytesEnd::new(elem_name)))
.map_err(|e| format!("Failed to write child node end: {}", e))?;
} else {
writer
.write_event(Event::Empty(elem))
.map_err(|e| format!("Failed to write empty child node: {}", e))?;
}
Ok(())
}
fn write_tensor<W: std::io::Write>(
writer: &mut Writer<W>,
tensor: &Tensor,
_config: &ToXmlConfig,
) -> Result<(), String> {
match tensor {
Tensor::Scalar(n) => write_text(writer, &n.to_string())?,
Tensor::Array(items) => {
for item in items {
let elem = BytesStart::new("item");
writer
.write_event(Event::Start(elem))
.map_err(|e| format!("Failed to write tensor item start: {}", e))?;
write_tensor(writer, item, _config)?;
writer
.write_event(Event::End(BytesEnd::new("item")))
.map_err(|e| format!("Failed to write tensor item end: {}", e))?;
}
}
}
Ok(())
}
fn write_list<W: std::io::Write>(
writer: &mut Writer<W>,
items: &[Value],
config: &ToXmlConfig,
) -> Result<(), String> {
for item in items {
let elem = BytesStart::new("item");
writer
.write_event(Event::Start(elem))
.map_err(|e| format!("Failed to write list item start: {}", e))?;
write_value_content(writer, item, config)?;
writer
.write_event(Event::End(BytesEnd::new("item")))
.map_err(|e| format!("Failed to write list item end: {}", e))?;
}
Ok(())
}
fn write_text<W: std::io::Write>(writer: &mut Writer<W>, text: &str) -> Result<(), String> {
writer
.write_event(Event::Text(BytesText::new(text)))
.map_err(|e| format!("Failed to write text: {}", e))
}
fn is_simple_value(value: &Value) -> bool {
matches!(
value,
Value::Null | Value::Bool(_) | Value::Int(_) | Value::Float(_) | Value::String(_)
)
}
fn escape_attribute_value(value: &Value) -> String {
match value {
Value::Null => String::new(),
Value::Bool(b) => b.to_string(),
Value::Int(n) => n.to_string(),
Value::Float(f) => f.to_string(),
Value::String(s) => s.to_string(),
Value::Reference(r) => r.to_ref_string(),
Value::Expression(e) => format!("$({})", e),
Value::Tensor(_) => "[tensor]".to_string(),
Value::List(_) => "[list]".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use hedl_core::lex::{Expression, Span};
use hedl_core::{Document, Reference};
#[test]
fn test_to_xml_config_default() {
let config = ToXmlConfig::default();
assert!(config.pretty);
assert_eq!(config.indent, " ");
assert_eq!(config.root_element, "hedl");
assert!(!config.include_metadata);
assert!(!config.use_attributes);
}
#[test]
fn test_to_xml_config_debug() {
let config = ToXmlConfig::default();
let debug = format!("{:?}", config);
assert!(debug.contains("ToXmlConfig"));
assert!(debug.contains("pretty"));
assert!(debug.contains("indent"));
assert!(debug.contains("root_element"));
}
#[test]
fn test_to_xml_config_clone() {
let config = ToXmlConfig {
pretty: false,
indent: "\t".to_string(),
root_element: "custom".to_string(),
include_metadata: true,
use_attributes: true,
};
let cloned = config.clone();
assert!(!cloned.pretty);
assert_eq!(cloned.indent, "\t");
assert_eq!(cloned.root_element, "custom");
assert!(cloned.include_metadata);
assert!(cloned.use_attributes);
}
#[test]
fn test_to_xml_config_all_options() {
let config = ToXmlConfig {
pretty: true,
indent: " ".to_string(),
root_element: "document".to_string(),
include_metadata: true,
use_attributes: true,
};
assert!(config.pretty);
assert_eq!(config.indent.len(), 4);
}
#[test]
fn test_empty_document() {
let doc = Document::new((2, 0));
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<?xml"));
assert!(xml.contains("<hedl"));
assert!(xml.contains("</hedl>"));
}
#[test]
fn test_empty_document_compact() {
let doc = Document::new((2, 0));
let config = ToXmlConfig {
pretty: false,
..Default::default()
};
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<?xml"));
assert!(xml.contains("<hedl></hedl>"));
}
#[test]
fn test_custom_root_element() {
let doc = Document::new((2, 0));
let config = ToXmlConfig {
root_element: "custom_root".to_string(),
..Default::default()
};
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<custom_root"));
assert!(xml.contains("</custom_root>"));
}
#[test]
fn test_with_metadata() {
let doc = Document::new((2, 5));
let config = ToXmlConfig {
include_metadata: true,
..Default::default()
};
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("version=\"2.5\""));
}
#[test]
fn test_scalar_null() {
let mut doc = Document::new((2, 0));
doc.root
.insert("null_val".to_string(), Item::Scalar(Value::Null));
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<null_val>") && xml.contains("</null_val>"));
}
#[test]
fn test_scalar_bool_true() {
let mut doc = Document::new((2, 0));
doc.root
.insert("val".to_string(), Item::Scalar(Value::Bool(true)));
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<val>true</val>"));
}
#[test]
fn test_scalar_bool_false() {
let mut doc = Document::new((2, 0));
doc.root
.insert("val".to_string(), Item::Scalar(Value::Bool(false)));
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<val>false</val>"));
}
#[test]
fn test_scalar_int_positive() {
let mut doc = Document::new((2, 0));
doc.root
.insert("val".to_string(), Item::Scalar(Value::Int(42)));
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<val>42</val>"));
}
#[test]
fn test_scalar_int_negative() {
let mut doc = Document::new((2, 0));
doc.root
.insert("val".to_string(), Item::Scalar(Value::Int(-100)));
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<val>-100</val>"));
}
#[test]
fn test_scalar_int_zero() {
let mut doc = Document::new((2, 0));
doc.root
.insert("val".to_string(), Item::Scalar(Value::Int(0)));
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<val>0</val>"));
}
#[test]
fn test_scalar_float() {
let mut doc = Document::new((2, 0));
doc.root
.insert("val".to_string(), Item::Scalar(Value::Float(3.5)));
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<val>3.5</val>"));
}
#[test]
fn test_scalar_string() {
let mut doc = Document::new((2, 0));
doc.root.insert(
"val".to_string(),
Item::Scalar(Value::String("hello".to_string().into())),
);
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<val>hello</val>"));
}
#[test]
fn test_scalar_string_empty() {
let mut doc = Document::new((2, 0));
doc.root.insert(
"val".to_string(),
Item::Scalar(Value::String("".to_string().into())),
);
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<val></val>") || xml.contains("<val/>"));
}
#[test]
fn test_scalar_reference_local() {
let mut doc = Document::new((2, 0));
doc.root.insert(
"ref".to_string(),
Item::Scalar(Value::Reference(Reference::local("user123"))),
);
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("@user123"));
assert!(xml.contains("__hedl_type__=\"ref\""));
}
#[test]
fn test_scalar_reference_qualified() {
let mut doc = Document::new((2, 0));
doc.root.insert(
"ref".to_string(),
Item::Scalar(Value::Reference(Reference::qualified("User", "456"))),
);
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("@User:456"));
}
#[test]
fn test_scalar_expression_identifier() {
let mut doc = Document::new((2, 0));
doc.root.insert(
"expr".to_string(),
Item::Scalar(Value::Expression(Box::new(Expression::Identifier {
name: "foo".to_string(),
span: Span::synthetic(),
}))),
);
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("$(foo)"));
}
#[test]
fn test_scalar_expression_call() {
let mut doc = Document::new((2, 0));
doc.root.insert(
"expr".to_string(),
Item::Scalar(Value::Expression(Box::new(Expression::Call {
name: "add".to_string(),
args: vec![
Expression::Identifier {
name: "x".to_string(),
span: Span::synthetic(),
},
Expression::Literal {
value: hedl_core::lex::ExprLiteral::Int(1),
span: Span::synthetic(),
},
],
span: Span::synthetic(),
}))),
);
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("$(add(x, 1))"));
}
#[test]
fn test_tensor_1d() {
let mut doc = Document::new((2, 0));
let tensor = Tensor::Array(vec![
Tensor::Scalar(1.0),
Tensor::Scalar(2.0),
Tensor::Scalar(3.0),
]);
doc.root.insert(
"tensor".to_string(),
Item::Scalar(Value::Tensor(Box::new(tensor))),
);
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<tensor>"));
assert!(xml.contains("<item>1</item>"));
assert!(xml.contains("<item>2</item>"));
assert!(xml.contains("<item>3</item>"));
}
#[test]
fn test_tensor_scalar() {
let mut doc = Document::new((2, 0));
let tensor = Tensor::Scalar(42.5);
doc.root.insert(
"tensor".to_string(),
Item::Scalar(Value::Tensor(Box::new(tensor))),
);
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<tensor>42.5</tensor>"));
}
#[test]
fn test_nested_object() {
let mut doc = Document::new((2, 0));
let mut inner = BTreeMap::new();
inner.insert(
"name".to_string(),
Item::Scalar(Value::String("test".to_string().into())),
);
inner.insert("value".to_string(), Item::Scalar(Value::Int(100)));
doc.root.insert("config".to_string(), Item::Object(inner));
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<config>"));
assert!(xml.contains("<name>test</name>"));
assert!(xml.contains("<value>100</value>"));
assert!(xml.contains("</config>"));
}
#[test]
fn test_deeply_nested_object() {
let mut doc = Document::new((2, 0));
let mut level3 = BTreeMap::new();
level3.insert("deep".to_string(), Item::Scalar(Value::Int(42)));
let mut level2 = BTreeMap::new();
level2.insert("nested".to_string(), Item::Object(level3));
let mut level1 = BTreeMap::new();
level1.insert("inner".to_string(), Item::Object(level2));
doc.root.insert("outer".to_string(), Item::Object(level1));
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<outer>"));
assert!(xml.contains("<inner>"));
assert!(xml.contains("<nested>"));
assert!(xml.contains("<deep>42</deep>"));
}
#[test]
fn test_matrix_list() {
let mut doc = Document::new((2, 0));
let mut list = MatrixList::new("User", vec!["id".to_string(), "name".to_string()]);
list.add_row(Node::new(
"User",
"u1",
vec![
Value::String("u1".to_string().into()),
Value::String("Alice".to_string().into()),
],
));
doc.root.insert("users".to_string(), Item::List(list));
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<users>"));
assert!(xml.contains("<user>"));
assert!(xml.contains("</users>"));
}
#[test]
fn test_matrix_list_with_metadata() {
let mut doc = Document::new((2, 0));
let mut list = MatrixList::new("User", vec!["id".to_string()]);
list.add_row(Node::new(
"User",
"u1",
vec![Value::String("u1".to_string().into())],
));
doc.root.insert("users".to_string(), Item::List(list));
let config = ToXmlConfig {
include_metadata: true,
..Default::default()
};
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("type=\"User\""));
}
#[test]
fn test_special_characters_ampersand() {
let mut doc = Document::new((2, 0));
doc.root.insert(
"text".to_string(),
Item::Scalar(Value::String("hello & goodbye".to_string().into())),
);
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<text>"));
}
#[test]
fn test_special_characters_angle_brackets() {
let mut doc = Document::new((2, 0));
doc.root.insert(
"text".to_string(),
Item::Scalar(Value::String("hello <tag> goodbye".to_string().into())),
);
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<text>"));
}
#[test]
fn test_special_characters_quotes() {
let mut doc = Document::new((2, 0));
doc.root.insert(
"text".to_string(),
Item::Scalar(Value::String("hello \"quoted\"".to_string().into())),
);
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<text>"));
}
#[test]
fn test_is_simple_value() {
assert!(is_simple_value(&Value::Null));
assert!(is_simple_value(&Value::Bool(true)));
assert!(is_simple_value(&Value::Int(42)));
assert!(is_simple_value(&Value::Float(3.5)));
assert!(is_simple_value(&Value::String("hello".to_string().into())));
assert!(!is_simple_value(&Value::Reference(Reference::local("x"))));
assert!(!is_simple_value(&Value::Tensor(Box::new(Tensor::Scalar(
1.0
)))));
}
#[test]
fn test_escape_attribute_value_null() {
assert_eq!(escape_attribute_value(&Value::Null), "");
}
#[test]
fn test_escape_attribute_value_bool() {
assert_eq!(escape_attribute_value(&Value::Bool(true)), "true");
assert_eq!(escape_attribute_value(&Value::Bool(false)), "false");
}
#[test]
fn test_escape_attribute_value_int() {
assert_eq!(escape_attribute_value(&Value::Int(42)), "42");
assert_eq!(escape_attribute_value(&Value::Int(-100)), "-100");
}
#[test]
fn test_escape_attribute_value_float() {
assert_eq!(escape_attribute_value(&Value::Float(3.5)), "3.5");
}
#[test]
fn test_escape_attribute_value_string() {
assert_eq!(
escape_attribute_value(&Value::String("hello".to_string().into())),
"hello"
);
}
#[test]
fn test_escape_attribute_value_reference() {
let ref_val = Value::Reference(Reference::local("user1"));
assert_eq!(escape_attribute_value(&ref_val), "@user1");
}
#[test]
fn test_escape_attribute_value_expression() {
let expr = Value::Expression(Box::new(Expression::Identifier {
name: "foo".to_string(),
span: Span::default(),
}));
assert_eq!(escape_attribute_value(&expr), "$(foo)");
}
#[test]
fn test_escape_attribute_value_tensor() {
let tensor = Value::Tensor(Box::new(Tensor::Scalar(1.0)));
assert_eq!(escape_attribute_value(&tensor), "[tensor]");
}
#[test]
fn test_escape_attribute_value_list() {
let list = Value::List(Box::new(vec![
Value::String("a".to_string().into()),
Value::String("b".to_string().into()),
]));
assert_eq!(escape_attribute_value(&list), "[list]");
}
#[test]
fn test_list_string_values() {
let mut doc = Document::new((1, 1));
doc.root.insert(
"roles".to_string(),
Item::Scalar(Value::List(Box::new(vec![
Value::String("admin".to_string().into()),
Value::String("editor".to_string().into()),
Value::String("viewer".to_string().into()),
]))),
);
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<roles>"));
assert!(xml.contains("<item>admin</item>"));
assert!(xml.contains("<item>editor</item>"));
assert!(xml.contains("<item>viewer</item>"));
assert!(xml.contains("</roles>"));
}
#[test]
fn test_list_bool_values() {
let mut doc = Document::new((1, 1));
doc.root.insert(
"flags".to_string(),
Item::Scalar(Value::List(Box::new(vec![
Value::Bool(true),
Value::Bool(false),
Value::Bool(true),
]))),
);
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<item>true</item>"));
assert!(xml.contains("<item>false</item>"));
}
#[test]
fn test_list_mixed_values() {
let mut doc = Document::new((1, 1));
doc.root.insert(
"mixed".to_string(),
Item::Scalar(Value::List(Box::new(vec![
Value::String("text".to_string().into()),
Value::Int(42),
Value::Bool(true),
Value::Null,
]))),
);
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<item>text</item>"));
assert!(xml.contains("<item>42</item>"));
assert!(xml.contains("<item>true</item>"));
}
#[test]
fn test_list_empty() {
let mut doc = Document::new((1, 1));
doc.root.insert(
"empty".to_string(),
Item::Scalar(Value::List(Box::default())),
);
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<empty>"));
assert!(xml.contains("</empty>"));
}
#[test]
fn test_list_with_references() {
let mut doc = Document::new((1, 1));
doc.root.insert(
"refs".to_string(),
Item::Scalar(Value::List(Box::new(vec![
Value::Reference(Reference::local("user1")),
Value::Reference(Reference::qualified("User", "2")),
]))),
);
let config = ToXmlConfig::default();
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("<item>@user1</item>"));
assert!(xml.contains("<item>@User:2</item>"));
}
#[test]
fn test_pretty_vs_compact() {
let mut doc = Document::new((2, 0));
doc.root
.insert("val".to_string(), Item::Scalar(Value::Int(42)));
let config_pretty = ToXmlConfig {
pretty: true,
..Default::default()
};
let config_compact = ToXmlConfig {
pretty: false,
..Default::default()
};
let xml_pretty = to_xml(&doc, &config_pretty).unwrap();
let xml_compact = to_xml(&doc, &config_compact).unwrap();
assert!(xml_pretty.len() > xml_compact.len());
}
#[test]
fn test_use_attributes_simple() {
let mut doc = Document::new((2, 0));
doc.root
.insert("val".to_string(), Item::Scalar(Value::Int(42)));
let config = ToXmlConfig {
use_attributes: true,
..Default::default()
};
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("value=\"42\""));
}
}