1use crate::builder::{DocumentBuilder, FragmentBuilder, IntoXmlFragment};
16use crate::core::{Document, XmlResult};
17
18pub use xdoc_macros::xml;
19
20#[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}