#![cfg_attr(not(test), warn(missing_docs))]
mod from_xml;
pub mod schema;
pub mod security;
pub mod streaming;
mod to_xml;
#[cfg(feature = "async")]
pub mod async_api;
pub use from_xml::{from_xml, EntityPolicy, FromXmlConfig};
pub use schema::{SchemaCache, SchemaValidator, ValidationError};
pub use security::{SecurityViolation, XmlSecurityValidator};
pub use streaming::{from_xml_stream, StreamConfig, StreamItem, XmlStreamingParser};
pub use to_xml::{to_xml, ToXmlConfig};
use hedl_core::Document;
pub fn hedl_to_xml(doc: &Document) -> Result<String, String> {
to_xml(doc, &ToXmlConfig::default())
}
pub fn xml_to_hedl(xml: &str) -> Result<Document, String> {
from_xml(xml, &FromXmlConfig::default())
}
#[cfg(test)]
mod tests {
use super::*;
use hedl_core::{Document, Item, MatrixList, Node, Reference, Value};
use std::collections::BTreeMap;
#[test]
fn test_round_trip_scalars() {
let mut doc = Document::new((2, 0));
doc.root
.insert("null_val".to_string(), Item::Scalar(Value::Null));
doc.root
.insert("bool_val".to_string(), Item::Scalar(Value::Bool(true)));
doc.root
.insert("int_val".to_string(), Item::Scalar(Value::Int(42)));
doc.root
.insert("float_val".to_string(), Item::Scalar(Value::Float(3.25)));
doc.root.insert(
"string_val".to_string(),
Item::Scalar(Value::String("hello".to_string().into())),
);
let xml = hedl_to_xml(&doc).unwrap();
let doc2 = xml_to_hedl(&xml).unwrap();
assert_eq!(
doc2.root.get("bool_val").and_then(|i| i.as_scalar()),
Some(&Value::Bool(true))
);
assert_eq!(
doc2.root.get("int_val").and_then(|i| i.as_scalar()),
Some(&Value::Int(42))
);
assert_eq!(
doc2.root.get("string_val").and_then(|i| i.as_scalar()),
Some(&Value::String("hello".to_string().into()))
);
}
#[test]
fn test_round_trip_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 xml = hedl_to_xml(&doc).unwrap();
let doc2 = xml_to_hedl(&xml).unwrap();
let config_obj = doc2.root.get("config").and_then(|i| i.as_object()).unwrap();
assert_eq!(
config_obj.get("name").and_then(|i| i.as_scalar()),
Some(&Value::String("test".to_string().into()))
);
assert_eq!(
config_obj.get("value").and_then(|i| i.as_scalar()),
Some(&Value::Int(100))
);
}
#[test]
fn test_round_trip_reference() {
let mut doc = Document::new((2, 0));
doc.root.insert(
"ref1".to_string(),
Item::Scalar(Value::Reference(Reference::local("user123"))),
);
doc.root.insert(
"ref2".to_string(),
Item::Scalar(Value::Reference(Reference::qualified("User", "456"))),
);
let xml = hedl_to_xml(&doc).unwrap();
let doc2 = xml_to_hedl(&xml).unwrap();
assert_eq!(
doc2.root.get("ref1").and_then(|i| i.as_scalar()),
Some(&Value::Reference(Reference::local("user123")))
);
assert_eq!(
doc2.root.get("ref2").and_then(|i| i.as_scalar()),
Some(&Value::Reference(Reference::qualified("User", "456")))
);
}
#[test]
fn test_round_trip_expression() {
use hedl_core::lex::{ExprLiteral, Expression, Span};
let mut doc = Document::new((2, 0));
let expr = Expression::Call {
name: "add".to_string(),
args: vec![
Expression::Identifier {
name: "x".to_string(),
span: Span::synthetic(),
},
Expression::Literal {
value: ExprLiteral::Int(1),
span: Span::synthetic(),
},
],
span: Span::synthetic(),
};
doc.root.insert(
"expr".to_string(),
Item::Scalar(Value::Expression(Box::new(expr.clone()))),
);
let xml = hedl_to_xml(&doc).unwrap();
let doc2 = xml_to_hedl(&xml).unwrap();
if let Some(Item::Scalar(Value::Expression(e))) = doc2.root.get("expr") {
assert_eq!(e.to_string(), expr.to_string());
} else {
panic!("Expected expression value");
}
}
#[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()]);
let node1 = Node::new(
"User",
"user1",
vec![
Value::String("user1".to_string().into()),
Value::String("Alice".to_string().into()),
],
);
let node2 = Node::new(
"User",
"user2",
vec![
Value::String("user2".to_string().into()),
Value::String("Bob".to_string().into()),
],
);
list.add_row(node1);
list.add_row(node2);
doc.root.insert("users".to_string(), Item::List(list));
let xml = hedl_to_xml(&doc).unwrap();
assert!(xml.contains("<users"));
assert!(xml.contains("user1"));
assert!(xml.contains("user2"));
}
#[test]
fn test_special_characters_escaping() {
let mut doc = Document::new((2, 0));
doc.root.insert(
"text".to_string(),
Item::Scalar(Value::String(
"hello & goodbye <tag> \"quoted\"".to_string().into(),
)),
);
let xml = hedl_to_xml(&doc).unwrap();
let doc2 = xml_to_hedl(&xml).unwrap();
let original = doc.root.get("text").and_then(|i| i.as_scalar());
let parsed = doc2.root.get("text").and_then(|i| i.as_scalar());
assert_eq!(original, parsed);
}
#[test]
fn test_nested_objects() {
let mut doc = Document::new((2, 0));
let mut level2 = BTreeMap::new();
level2.insert(
"deep".to_string(),
Item::Scalar(Value::String("value".to_string().into())),
);
let mut level1 = BTreeMap::new();
level1.insert("nested".to_string(), Item::Object(level2));
doc.root.insert("outer".to_string(), Item::Object(level1));
let xml = hedl_to_xml(&doc).unwrap();
let doc2 = xml_to_hedl(&xml).unwrap();
assert!(doc2.root.contains_key("outer"));
}
#[test]
fn test_config_pretty_print() {
let mut doc = Document::new((2, 0));
doc.root.insert(
"test".to_string(),
Item::Scalar(Value::String("value".to_string().into())),
);
let config_pretty = ToXmlConfig {
pretty: true,
indent: " ".to_string(),
..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_config_custom_root() {
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_config_metadata() {
let doc = Document::new((2, 1));
let config = ToXmlConfig {
include_metadata: true,
..Default::default()
};
let xml = to_xml(&doc, &config).unwrap();
assert!(xml.contains("version=\"2.1\""));
}
#[test]
fn test_empty_values() {
let mut doc = Document::new((2, 0));
doc.root
.insert("empty".to_string(), Item::Scalar(Value::Null));
let xml = hedl_to_xml(&doc).unwrap();
let doc2 = xml_to_hedl(&xml).unwrap();
assert!(doc2.root.contains_key("empty"));
}
#[test]
fn test_tensor_values() {
use hedl_core::lex::Tensor;
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 xml = hedl_to_xml(&doc).unwrap();
assert!(xml.contains("<tensor>"));
assert!(xml.contains("<item>"));
}
#[test]
fn test_infer_lists_config() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<hedl>
<user id="1"><name>Alice</name></user>
<user id="2"><name>Bob</name></user>
</hedl>"#;
let config = FromXmlConfig {
infer_lists: true,
..Default::default()
};
let doc = from_xml(xml, &config).unwrap();
assert!(doc.root.contains_key("user"));
if let Some(Item::List(list)) = doc.root.get("user") {
assert_eq!(list.rows.len(), 2);
}
}
#[test]
fn test_attributes_as_values() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<hedl>
<item id="123" name="test" active="true"/>
</hedl>"#;
let config = FromXmlConfig::default();
let doc = from_xml(xml, &config).unwrap();
assert!(doc.root.contains_key("item"));
if let Some(Item::Object(obj)) = doc.root.get("item") {
assert_eq!(
obj.get("id").and_then(|i| i.as_scalar()),
Some(&Value::Int(123))
);
assert_eq!(
obj.get("name").and_then(|i| i.as_scalar()),
Some(&Value::String("test".to_string().into()))
);
assert_eq!(
obj.get("active").and_then(|i| i.as_scalar()),
Some(&Value::Bool(true))
);
}
}
}