Skip to main content

xdoc/macros/
mod.rs

1//! Declarative XML macro surface.
2//!
3//! The `xml!` MVP is syntax sugar over `builder` and `component`; it does not
4//! parse or serialize XML by itself. It supports XML elements, literal and
5//! expression attributes, text and expression children, comments, namespace
6//! declarations, prefixed names, and explicit Rust components written as
7//! `<{ComponentPath}>`.
8//!
9//! Current limits are intentional: component props must be simple Rust field
10//! identifiers, namespace declarations must be string literals, text token
11//! spacing follows Rust tokenization for raw text, and compile-fail coverage is
12//! limited to the MVP parser/codegen errors. Use normal Rust interpolation for
13//! custom component signatures or more complex values.
14
15use crate::builder::{DocumentBuilder, FragmentBuilder, IntoXmlFragment};
16use crate::core::{Document, XmlResult};
17
18pub use xdoc_macros::xml;
19
20/// Runtime output produced by `xml!`.
21///
22/// A template can be materialized as a full `Document` when it contains exactly
23/// one root element, or used as a fragment when embedded as children.
24#[derive(Debug, Clone, Default, PartialEq, Eq)]
25pub struct XmlTemplate {
26    fragment: FragmentBuilder,
27}
28
29impl XmlTemplate {
30    pub fn from_fragment(fragment: FragmentBuilder) -> Self {
31        Self { fragment }
32    }
33
34    pub fn into_fragment(self) -> FragmentBuilder {
35        self.fragment
36    }
37
38    pub fn into_document(self) -> XmlResult<Document> {
39        let root = self.fragment.into_single_element()?;
40        DocumentBuilder::new().root(root)?.build()
41    }
42}
43
44impl IntoXmlFragment for XmlTemplate {
45    fn into_xml_fragment(self) -> XmlResult<FragmentBuilder> {
46        Ok(self.fragment)
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use crate::builder::{element, fragment};
54    use crate::component::{Children, Fragment};
55    use crate::core::{ErrorKind, XmlResult};
56    use crate::writer::to_string_compact;
57
58    #[derive(Debug, Clone)]
59    struct InvoiceHeaderProps {
60        title: &'static str,
61        issued_at: String,
62    }
63
64    #[allow(non_snake_case)]
65    fn InvoiceHeader(props: InvoiceHeaderProps) -> XmlResult<crate::builder::ElementBuilder> {
66        element("Header")?
67            .attr("title", props.title)?
68            .attr("issued_at", props.issued_at)
69    }
70
71    #[derive(Debug, Clone)]
72    struct PanelProps {
73        title: &'static str,
74    }
75
76    #[allow(non_snake_case)]
77    fn Panel(props: PanelProps, children: Children) -> XmlResult<crate::builder::ElementBuilder> {
78        element("Panel")?
79            .attr("title", props.title)?
80            .child(children)
81    }
82
83    #[derive(Debug, Clone)]
84    struct PairProps {
85        first: &'static str,
86        second: &'static str,
87    }
88
89    #[allow(non_snake_case)]
90    fn Pair(props: PairProps) -> XmlResult<Fragment> {
91        fragment()
92            .child(element("First")?.text(props.first)?)?
93            .child(element("Second")?.text(props.second)?)
94    }
95
96    #[test]
97    fn xml_macro_basic_serializes_self_closing_element() -> XmlResult<()> {
98        let document = xml! { <Root/> }?.into_document()?;
99
100        assert_eq!(to_string_compact(&document)?, "<Root/>");
101        Ok(())
102    }
103
104    #[test]
105    fn xml_macro_basic_serializes_nested_text() -> XmlResult<()> {
106        let document = xml! { <Root><Child>text</Child></Root> }?.into_document()?;
107
108        assert_eq!(
109            to_string_compact(&document)?,
110            "<Root><Child>text</Child></Root>"
111        );
112        Ok(())
113    }
114
115    #[test]
116    fn xml_macro_basic_serializes_text_expression() -> XmlResult<()> {
117        let value = "DOC-1";
118        let document = xml! { <ID>{ value }</ID> }?.into_document()?;
119
120        assert_eq!(to_string_compact(&document)?, "<ID>DOC-1</ID>");
121        Ok(())
122    }
123
124    #[test]
125    fn xml_macro_basic_serializes_literal_and_dynamic_attributes() -> XmlResult<()> {
126        let quantity = 3;
127        let document = xml! { <Item code="A001" quantity={ quantity }/> }?.into_document()?;
128
129        assert_eq!(
130            to_string_compact(&document)?,
131            "<Item code=\"A001\" quantity=\"3\"/>"
132        );
133        Ok(())
134    }
135
136    #[test]
137    fn xml_macro_basic_composes_interpolated_fragment() -> XmlResult<()> {
138        let child = xml! { <Child>value</Child> }?;
139        let document = xml! { <Root>{ child }</Root> }?.into_document()?;
140
141        assert_eq!(
142            to_string_compact(&document)?,
143            "<Root><Child>value</Child></Root>"
144        );
145        Ok(())
146    }
147
148    #[test]
149    fn xml_macro_namespaces_serializes_default_namespace() -> XmlResult<()> {
150        let document =
151            xml! { <Root xmlns="urn:default"><Child>value</Child></Root> }?.into_document()?;
152
153        assert_eq!(
154            to_string_compact(&document)?,
155            "<Root xmlns=\"urn:default\"><Child>value</Child></Root>"
156        );
157        Ok(())
158    }
159
160    #[test]
161    fn xml_macro_namespaces_serializes_prefixed_element() -> XmlResult<()> {
162        let document =
163            xml! { <doc:Root xmlns:doc="urn:doc"><doc:Child>value</doc:Child></doc:Root> }?
164                .into_document()?;
165
166        assert_eq!(
167            to_string_compact(&document)?,
168            "<doc:Root xmlns:doc=\"urn:doc\"><doc:Child>value</doc:Child></doc:Root>"
169        );
170        Ok(())
171    }
172
173    #[test]
174    fn xml_macro_namespaces_serializes_prefixed_attribute() -> XmlResult<()> {
175        let id = "A001";
176        let document = xml! { <Root xmlns:doc="urn:doc" doc:id={ id }/> }?.into_document()?;
177
178        assert_eq!(
179            to_string_compact(&document)?,
180            "<Root xmlns:doc=\"urn:doc\" doc:id=\"A001\"/>"
181        );
182        Ok(())
183    }
184
185    #[test]
186    fn xml_macro_comments_serializes_comment() -> XmlResult<()> {
187        let document = xml! { <Root><!-- hello --><Child>value</Child></Root> }?.into_document()?;
188
189        assert_eq!(
190            to_string_compact(&document)?,
191            "<Root><!--hello--><Child>value</Child></Root>"
192        );
193        Ok(())
194    }
195
196    #[test]
197    fn xml_macro_components_serializes_typed_props_component() -> XmlResult<()> {
198        let issued_at = "2026-06-11".to_owned();
199        let document =
200            xml! { <{InvoiceHeader} title="Demo" issued_at={ issued_at }/> }?.into_document()?;
201
202        assert_eq!(
203            to_string_compact(&document)?,
204            "<Header title=\"Demo\" issued_at=\"2026-06-11\"/>"
205        );
206        Ok(())
207    }
208
209    #[test]
210    fn xml_macro_components_serializes_children_component() -> XmlResult<()> {
211        let document = xml! { <{Panel} title="Main"><ID>DOC1</ID><Name>Report</Name></{Panel}> }?
212            .into_document()?;
213
214        assert_eq!(
215            to_string_compact(&document)?,
216            "<Panel title=\"Main\"><ID>DOC1</ID><Name>Report</Name></Panel>"
217        );
218        Ok(())
219    }
220
221    #[test]
222    fn xml_macro_components_accepts_interpolated_children() -> XmlResult<()> {
223        let child = xml! { <ID>DOC1</ID> }?;
224        let document = xml! { <{Panel} title="Main">{ child }<Name>Report</Name></{Panel}> }?
225            .into_document()?;
226
227        assert_eq!(
228            to_string_compact(&document)?,
229            "<Panel title=\"Main\"><ID>DOC1</ID><Name>Report</Name></Panel>"
230        );
231        Ok(())
232    }
233
234    #[test]
235    fn xml_macro_components_accepts_fragment_component() -> XmlResult<()> {
236        let document = xml! { <Root><{Pair} first="one" second="two"/></Root> }?.into_document()?;
237
238        assert_eq!(
239            to_string_compact(&document)?,
240            "<Root><First>one</First><Second>two</Second></Root>"
241        );
242        Ok(())
243    }
244
245    #[test]
246    fn macro_output_template_materializes_document() -> XmlResult<()> {
247        let template = XmlTemplate::from_fragment(fragment().child(element("Root")?.text("ok")?)?);
248
249        let document = template.into_document()?;
250
251        assert_eq!(to_string_compact(&document)?, "<Root>ok</Root>");
252        Ok(())
253    }
254
255    #[test]
256    fn macro_output_template_can_be_used_as_child_fragment() -> XmlResult<()> {
257        let template =
258            XmlTemplate::from_fragment(fragment().child(element("Child")?.text("value")?)?);
259        let document = DocumentBuilder::new()
260            .root(element("Root")?.child(template)?)?
261            .build()?;
262
263        assert_eq!(
264            to_string_compact(&document)?,
265            "<Root><Child>value</Child></Root>"
266        );
267        Ok(())
268    }
269
270    #[test]
271    fn macro_output_template_rejects_empty_document() {
272        let error = XmlTemplate::default()
273            .into_document()
274            .expect_err("empty template must not build a document");
275
276        assert_eq!(error.kind(), &ErrorKind::InvalidOperation);
277        assert_eq!(
278            error.message(),
279            "fragment requires one root element to build a document"
280        );
281    }
282
283    #[test]
284    fn macro_output_template_rejects_multiple_document_roots() -> XmlResult<()> {
285        let template = XmlTemplate::from_fragment(
286            fragment()
287                .child(element("First")?)?
288                .child(element("Second")?)?,
289        );
290
291        let error = template
292            .into_document()
293            .expect_err("multiple roots must not build a document");
294
295        assert_eq!(error.kind(), &ErrorKind::InvalidOperation);
296        assert_eq!(
297            error.message(),
298            "fragment has multiple top-level nodes; document requires one root element"
299        );
300        Ok(())
301    }
302}