use crate::builder::{DocumentBuilder, FragmentBuilder, IntoXmlFragment};
use crate::core::{Document, XmlResult};
pub use xdoc_macros::xml;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct XmlTemplate {
fragment: FragmentBuilder,
}
impl XmlTemplate {
pub fn from_fragment(fragment: FragmentBuilder) -> Self {
Self { fragment }
}
pub fn into_fragment(self) -> FragmentBuilder {
self.fragment
}
pub fn into_document(self) -> XmlResult<Document> {
let root = self.fragment.into_single_element()?;
DocumentBuilder::new().root(root)?.build()
}
}
impl IntoXmlFragment for XmlTemplate {
fn into_xml_fragment(self) -> XmlResult<FragmentBuilder> {
Ok(self.fragment)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::builder::{element, fragment};
use crate::component::{Children, Fragment};
use crate::core::{ErrorKind, XmlResult};
use crate::writer::to_string_compact;
#[derive(Debug, Clone)]
struct InvoiceHeaderProps {
title: &'static str,
issued_at: String,
}
#[allow(non_snake_case)]
fn InvoiceHeader(props: InvoiceHeaderProps) -> XmlResult<crate::builder::ElementBuilder> {
element("Header")?
.attr("title", props.title)?
.attr("issued_at", props.issued_at)
}
#[derive(Debug, Clone)]
struct PanelProps {
title: &'static str,
}
#[allow(non_snake_case)]
fn Panel(props: PanelProps, children: Children) -> XmlResult<crate::builder::ElementBuilder> {
element("Panel")?
.attr("title", props.title)?
.child(children)
}
#[derive(Debug, Clone)]
struct PairProps {
first: &'static str,
second: &'static str,
}
#[allow(non_snake_case)]
fn Pair(props: PairProps) -> XmlResult<Fragment> {
fragment()
.child(element("First")?.text(props.first)?)?
.child(element("Second")?.text(props.second)?)
}
#[test]
fn xml_macro_basic_serializes_self_closing_element() -> XmlResult<()> {
let document = xml! { <Root/> }?.into_document()?;
assert_eq!(to_string_compact(&document)?, "<Root/>");
Ok(())
}
#[test]
fn xml_macro_basic_serializes_nested_text() -> XmlResult<()> {
let document = xml! { <Root><Child>text</Child></Root> }?.into_document()?;
assert_eq!(
to_string_compact(&document)?,
"<Root><Child>text</Child></Root>"
);
Ok(())
}
#[test]
fn xml_macro_basic_serializes_text_expression() -> XmlResult<()> {
let value = "DOC-1";
let document = xml! { <ID>{ value }</ID> }?.into_document()?;
assert_eq!(to_string_compact(&document)?, "<ID>DOC-1</ID>");
Ok(())
}
#[test]
fn xml_macro_basic_serializes_literal_and_dynamic_attributes() -> XmlResult<()> {
let quantity = 3;
let document = xml! { <Item code="A001" quantity={ quantity }/> }?.into_document()?;
assert_eq!(
to_string_compact(&document)?,
"<Item code=\"A001\" quantity=\"3\"/>"
);
Ok(())
}
#[test]
fn xml_macro_basic_composes_interpolated_fragment() -> XmlResult<()> {
let child = xml! { <Child>value</Child> }?;
let document = xml! { <Root>{ child }</Root> }?.into_document()?;
assert_eq!(
to_string_compact(&document)?,
"<Root><Child>value</Child></Root>"
);
Ok(())
}
#[test]
fn xml_macro_namespaces_serializes_default_namespace() -> XmlResult<()> {
let document =
xml! { <Root xmlns="urn:default"><Child>value</Child></Root> }?.into_document()?;
assert_eq!(
to_string_compact(&document)?,
"<Root xmlns=\"urn:default\"><Child>value</Child></Root>"
);
Ok(())
}
#[test]
fn xml_macro_namespaces_serializes_prefixed_element() -> XmlResult<()> {
let document =
xml! { <doc:Root xmlns:doc="urn:doc"><doc:Child>value</doc:Child></doc:Root> }?
.into_document()?;
assert_eq!(
to_string_compact(&document)?,
"<doc:Root xmlns:doc=\"urn:doc\"><doc:Child>value</doc:Child></doc:Root>"
);
Ok(())
}
#[test]
fn xml_macro_namespaces_serializes_prefixed_attribute() -> XmlResult<()> {
let id = "A001";
let document = xml! { <Root xmlns:doc="urn:doc" doc:id={ id }/> }?.into_document()?;
assert_eq!(
to_string_compact(&document)?,
"<Root xmlns:doc=\"urn:doc\" doc:id=\"A001\"/>"
);
Ok(())
}
#[test]
fn xml_macro_comments_serializes_comment() -> XmlResult<()> {
let document = xml! { <Root><!-- hello --><Child>value</Child></Root> }?.into_document()?;
assert_eq!(
to_string_compact(&document)?,
"<Root><!--hello--><Child>value</Child></Root>"
);
Ok(())
}
#[test]
fn xml_macro_components_serializes_typed_props_component() -> XmlResult<()> {
let issued_at = "2026-06-11".to_owned();
let document =
xml! { <{InvoiceHeader} title="Demo" issued_at={ issued_at }/> }?.into_document()?;
assert_eq!(
to_string_compact(&document)?,
"<Header title=\"Demo\" issued_at=\"2026-06-11\"/>"
);
Ok(())
}
#[test]
fn xml_macro_components_serializes_children_component() -> XmlResult<()> {
let document = xml! { <{Panel} title="Main"><ID>DOC1</ID><Name>Report</Name></{Panel}> }?
.into_document()?;
assert_eq!(
to_string_compact(&document)?,
"<Panel title=\"Main\"><ID>DOC1</ID><Name>Report</Name></Panel>"
);
Ok(())
}
#[test]
fn xml_macro_components_accepts_interpolated_children() -> XmlResult<()> {
let child = xml! { <ID>DOC1</ID> }?;
let document = xml! { <{Panel} title="Main">{ child }<Name>Report</Name></{Panel}> }?
.into_document()?;
assert_eq!(
to_string_compact(&document)?,
"<Panel title=\"Main\"><ID>DOC1</ID><Name>Report</Name></Panel>"
);
Ok(())
}
#[test]
fn xml_macro_components_accepts_fragment_component() -> XmlResult<()> {
let document = xml! { <Root><{Pair} first="one" second="two"/></Root> }?.into_document()?;
assert_eq!(
to_string_compact(&document)?,
"<Root><First>one</First><Second>two</Second></Root>"
);
Ok(())
}
#[test]
fn macro_output_template_materializes_document() -> XmlResult<()> {
let template = XmlTemplate::from_fragment(fragment().child(element("Root")?.text("ok")?)?);
let document = template.into_document()?;
assert_eq!(to_string_compact(&document)?, "<Root>ok</Root>");
Ok(())
}
#[test]
fn macro_output_template_can_be_used_as_child_fragment() -> XmlResult<()> {
let template =
XmlTemplate::from_fragment(fragment().child(element("Child")?.text("value")?)?);
let document = DocumentBuilder::new()
.root(element("Root")?.child(template)?)?
.build()?;
assert_eq!(
to_string_compact(&document)?,
"<Root><Child>value</Child></Root>"
);
Ok(())
}
#[test]
fn macro_output_template_rejects_empty_document() {
let error = XmlTemplate::default()
.into_document()
.expect_err("empty template must not build a document");
assert_eq!(error.kind(), &ErrorKind::InvalidOperation);
assert_eq!(
error.message(),
"fragment requires one root element to build a document"
);
}
#[test]
fn macro_output_template_rejects_multiple_document_roots() -> XmlResult<()> {
let template = XmlTemplate::from_fragment(
fragment()
.child(element("First")?)?
.child(element("Second")?)?,
);
let error = template
.into_document()
.expect_err("multiple roots must not build a document");
assert_eq!(error.kind(), &ErrorKind::InvalidOperation);
assert_eq!(
error.message(),
"fragment has multiple top-level nodes; document requires one root element"
);
Ok(())
}
}