Skip to main content

xdoc/builder/
mod.rs

1//! Ergonomic builders over the core XML model.
2
3use crate::core::{
4    Attribute, Document, ErrorKind, NamespaceDeclaration, NodeId, QName, XmlError, XmlResult,
5};
6
7/// Creates an element builder from a local XML name.
8pub fn element(name: impl Into<String>) -> XmlResult<ElementBuilder> {
9    ElementBuilder::new(name)
10}
11
12/// Creates a text fragment.
13pub fn text(value: impl Into<String>) -> XmlNode {
14    XmlNode::Text(value.into())
15}
16
17/// Creates an empty fragment builder.
18pub fn fragment() -> FragmentBuilder {
19    FragmentBuilder::new()
20}
21
22/// Builder for complete XML documents.
23#[derive(Debug, Clone, Default, PartialEq, Eq)]
24pub struct DocumentBuilder {
25    root: Option<ElementBuilder>,
26}
27
28impl DocumentBuilder {
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    pub fn root(mut self, root: ElementBuilder) -> XmlResult<Self> {
34        if self.root.is_some() {
35            return Err(XmlError::new(
36                crate::core::ErrorKind::InvalidOperation,
37                "document builder already has a root element",
38            ));
39        }
40
41        self.root = Some(root);
42        Ok(self)
43    }
44
45    pub fn build(self) -> XmlResult<Document> {
46        let root = self.root.ok_or_else(|| {
47            XmlError::new(
48                crate::core::ErrorKind::InvalidOperation,
49                "document builder requires a root element",
50            )
51        })?;
52
53        let mut document = Document::new();
54        let root_id = document.add_root_element(root.name.clone())?;
55        apply_element(&mut document, root_id, root)?;
56        Ok(document)
57    }
58}
59
60/// Builder for XML elements.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct ElementBuilder {
63    name: QName,
64    attributes: Vec<Attribute>,
65    namespaces: Vec<NamespaceDeclaration>,
66    children: FragmentBuilder,
67}
68
69impl ElementBuilder {
70    pub fn new(local_name: impl Into<String>) -> XmlResult<Self> {
71        Self::from_qname(QName::new(local_name)?)
72    }
73
74    pub fn qualified(
75        prefix: impl Into<String>,
76        local_name: impl Into<String>,
77        namespace_uri: impl Into<String>,
78    ) -> XmlResult<Self> {
79        Self::from_qname(QName::qualified(prefix, local_name, namespace_uri)?)
80    }
81
82    pub fn namespaced(
83        local_name: impl Into<String>,
84        namespace_uri: impl Into<String>,
85    ) -> XmlResult<Self> {
86        Self::from_qname(QName::namespaced(local_name, namespace_uri)?)
87    }
88
89    pub fn from_qname(name: QName) -> XmlResult<Self> {
90        Ok(Self {
91            name,
92            attributes: Vec::new(),
93            namespaces: Vec::new(),
94            children: FragmentBuilder::new(),
95        })
96    }
97
98    pub fn attr(mut self, name: impl Into<String>, value: impl ToString) -> XmlResult<Self> {
99        self.attributes
100            .push(Attribute::new(QName::new(name)?, value.to_string()));
101        Ok(self)
102    }
103
104    pub fn qualified_attr(
105        mut self,
106        prefix: impl Into<String>,
107        local_name: impl Into<String>,
108        namespace_uri: impl Into<String>,
109        value: impl ToString,
110    ) -> XmlResult<Self> {
111        self.attributes.push(Attribute::new(
112            QName::qualified(prefix, local_name, namespace_uri)?,
113            value.to_string(),
114        ));
115        Ok(self)
116    }
117
118    pub fn attr_if(self, name: impl Into<String>, value: Option<impl ToString>) -> XmlResult<Self> {
119        match value {
120            Some(value) => self.attr(name, value),
121            None => Ok(self),
122        }
123    }
124
125    pub fn default_namespace(mut self, uri: impl Into<String>) -> XmlResult<Self> {
126        self.namespaces.push(NamespaceDeclaration::default(uri)?);
127        Ok(self)
128    }
129
130    pub fn namespace(
131        mut self,
132        prefix: impl Into<String>,
133        uri: impl Into<String>,
134    ) -> XmlResult<Self> {
135        self.namespaces
136            .push(NamespaceDeclaration::prefixed(prefix, uri)?);
137        Ok(self)
138    }
139
140    pub fn child(mut self, child: impl IntoXmlFragment) -> XmlResult<Self> {
141        self.children = self.children.child(child)?;
142        Ok(self)
143    }
144
145    pub fn children<I, T>(mut self, children: I) -> XmlResult<Self>
146    where
147        I: IntoIterator<Item = T>,
148        T: IntoXmlFragment,
149    {
150        self.children = self.children.children(children)?;
151        Ok(self)
152    }
153
154    pub fn text(self, value: impl Into<String>) -> XmlResult<Self> {
155        self.child(XmlNode::Text(value.into()))
156    }
157
158    pub fn comment(self, value: impl Into<String>) -> XmlResult<Self> {
159        self.child(XmlNode::Comment(value.into()))
160    }
161
162    pub fn cdata(self, value: impl Into<String>) -> XmlResult<Self> {
163        self.child(XmlNode::CData(value.into()))
164    }
165
166    pub fn processing_instruction(
167        self,
168        target: impl Into<String>,
169        data: Option<impl Into<String>>,
170    ) -> XmlResult<Self> {
171        self.child(XmlNode::ProcessingInstruction {
172            target: target.into(),
173            data: data.map(Into::into),
174        })
175    }
176
177    pub fn name(&self) -> &QName {
178        &self.name
179    }
180
181    pub fn child_nodes(&self) -> &[XmlNode] {
182        self.children.nodes()
183    }
184}
185
186/// Builder for reusable XML fragments.
187#[derive(Debug, Clone, Default, PartialEq, Eq)]
188pub struct FragmentBuilder {
189    nodes: Vec<XmlNode>,
190}
191
192impl FragmentBuilder {
193    pub fn new() -> Self {
194        Self::default()
195    }
196
197    pub fn child(mut self, child: impl IntoXmlFragment) -> XmlResult<Self> {
198        self.nodes.extend(child.into_xml_fragment()?.nodes);
199        Ok(self)
200    }
201
202    pub fn children<I, T>(mut self, children: I) -> XmlResult<Self>
203    where
204        I: IntoIterator<Item = T>,
205        T: IntoXmlFragment,
206    {
207        for child in children {
208            self = self.child(child)?;
209        }
210        Ok(self)
211    }
212
213    pub fn text(self, value: impl Into<String>) -> XmlResult<Self> {
214        self.child(XmlNode::Text(value.into()))
215    }
216
217    pub fn nodes(&self) -> &[XmlNode] {
218        &self.nodes
219    }
220
221    pub fn into_single_element(self) -> XmlResult<ElementBuilder> {
222        let mut nodes = self.nodes.into_iter();
223        let Some(first) = nodes.next() else {
224            return Err(XmlError::new(
225                ErrorKind::InvalidOperation,
226                "fragment requires one root element to build a document",
227            ));
228        };
229
230        if nodes.next().is_some() {
231            return Err(XmlError::new(
232                ErrorKind::InvalidOperation,
233                "fragment has multiple top-level nodes; document requires one root element",
234            ));
235        }
236
237        match first {
238            XmlNode::Element(element) => Ok(element),
239            _ => Err(XmlError::new(
240                ErrorKind::InvalidOperation,
241                "document root must be an element",
242            )),
243        }
244    }
245}
246
247/// Fragment node accepted by the builder layer.
248#[derive(Debug, Clone, PartialEq, Eq)]
249pub enum XmlNode {
250    Element(ElementBuilder),
251    Text(String),
252    Comment(String),
253    CData(String),
254    ProcessingInstruction {
255        target: String,
256        data: Option<String>,
257    },
258}
259
260/// Converts values into XML fragments for builder composition.
261pub trait IntoXmlFragment {
262    fn into_xml_fragment(self) -> XmlResult<FragmentBuilder>;
263}
264
265impl IntoXmlFragment for FragmentBuilder {
266    fn into_xml_fragment(self) -> XmlResult<FragmentBuilder> {
267        Ok(self)
268    }
269}
270
271impl IntoXmlFragment for ElementBuilder {
272    fn into_xml_fragment(self) -> XmlResult<FragmentBuilder> {
273        Ok(FragmentBuilder {
274            nodes: vec![XmlNode::Element(self)],
275        })
276    }
277}
278
279impl IntoXmlFragment for XmlNode {
280    fn into_xml_fragment(self) -> XmlResult<FragmentBuilder> {
281        Ok(FragmentBuilder { nodes: vec![self] })
282    }
283}
284
285impl<T> IntoXmlFragment for Option<T>
286where
287    T: IntoXmlFragment,
288{
289    fn into_xml_fragment(self) -> XmlResult<FragmentBuilder> {
290        match self {
291            Some(value) => value.into_xml_fragment(),
292            None => Ok(FragmentBuilder::new()),
293        }
294    }
295}
296
297impl<T> IntoXmlFragment for XmlResult<T>
298where
299    T: IntoXmlFragment,
300{
301    fn into_xml_fragment(self) -> XmlResult<FragmentBuilder> {
302        self?.into_xml_fragment()
303    }
304}
305
306impl<T> IntoXmlFragment for Vec<T>
307where
308    T: IntoXmlFragment,
309{
310    fn into_xml_fragment(self) -> XmlResult<FragmentBuilder> {
311        FragmentBuilder::new().children(self)
312    }
313}
314
315impl IntoXmlFragment for String {
316    fn into_xml_fragment(self) -> XmlResult<FragmentBuilder> {
317        XmlNode::Text(self).into_xml_fragment()
318    }
319}
320
321impl IntoXmlFragment for &str {
322    fn into_xml_fragment(self) -> XmlResult<FragmentBuilder> {
323        XmlNode::Text(self.to_owned()).into_xml_fragment()
324    }
325}
326
327fn apply_element(
328    document: &mut Document,
329    element_id: NodeId,
330    element: ElementBuilder,
331) -> XmlResult<()> {
332    for namespace in element.namespaces {
333        document.add_namespace_declaration(element_id, namespace)?;
334    }
335
336    for attribute in element.attributes {
337        document.add_attribute(element_id, attribute)?;
338    }
339
340    apply_fragment(document, element_id, element.children)
341}
342
343fn apply_fragment(
344    document: &mut Document,
345    parent: NodeId,
346    fragment: FragmentBuilder,
347) -> XmlResult<()> {
348    for node in fragment.nodes {
349        match node {
350            XmlNode::Element(element) => {
351                let child_id = document.add_element(parent, element.name.clone())?;
352                apply_element(document, child_id, element)?;
353            }
354            XmlNode::Text(text) => {
355                document.add_text(parent, text)?;
356            }
357            XmlNode::Comment(comment) => {
358                document.add_comment(parent, comment)?;
359            }
360            XmlNode::CData(cdata) => {
361                document.add_cdata(parent, cdata)?;
362            }
363            XmlNode::ProcessingInstruction { target, data } => {
364                document.add_processing_instruction(parent, target, data)?;
365            }
366        }
367    }
368
369    Ok(())
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375    use crate::writer::to_string_compact;
376
377    fn item(code: &str, name: &str) -> XmlResult<ElementBuilder> {
378        element("Item")?.attr("code", code)?.text(name)
379    }
380
381    fn component_simple(id: &str) -> XmlResult<ElementBuilder> {
382        element("ID")?.text(id)
383    }
384
385    fn component_with_children(children: impl IntoXmlFragment) -> XmlResult<ElementBuilder> {
386        element("Wrapper")?.child(children)
387    }
388
389    #[test]
390    fn document_builder_creates_xml_with_three_levels() -> XmlResult<()> {
391        let document = DocumentBuilder::new()
392            .root(
393                element("Root")?
394                    .child(element("Child")?.child(element("Grandchild")?.text("v")?)?)?,
395            )?
396            .build()?;
397
398        assert_eq!(
399            to_string_compact(&document)?,
400            "<Root><Child><Grandchild>v</Grandchild></Child></Root>"
401        );
402        Ok::<(), XmlError>(())
403    }
404
405    #[test]
406    fn builder_serializes_result_with_writer() -> XmlResult<()> {
407        let document = DocumentBuilder::new()
408            .root(element("Root")?.text("value")?)?
409            .build()?;
410
411        assert_eq!(to_string_compact(&document)?, "<Root>value</Root>");
412        Ok::<(), XmlError>(())
413    }
414
415    #[test]
416    fn document_builder_supports_static_and_dynamic_attributes() -> XmlResult<()> {
417        let dynamic_value = Some(42);
418        let document = DocumentBuilder::new()
419            .root(
420                element("Root")?
421                    .attr("static", "yes")?
422                    .attr_if("dynamic", dynamic_value)?,
423            )?
424            .build()?;
425
426        assert_eq!(
427            to_string_compact(&document)?,
428            "<Root static=\"yes\" dynamic=\"42\"/>"
429        );
430        Ok::<(), XmlError>(())
431    }
432
433    #[test]
434    fn namespaces_can_be_declared_with_default_and_prefix() -> XmlResult<()> {
435        let document = DocumentBuilder::new()
436            .root(
437                ElementBuilder::qualified("doc", "Root", "urn:doc")?
438                    .default_namespace("urn:default")?
439                    .namespace("doc", "urn:doc")?
440                    .qualified_attr("doc", "id", "urn:doc", "123")?,
441            )?
442            .build()?;
443
444        assert_eq!(
445            to_string_compact(&document)?,
446            "<doc:Root xmlns=\"urn:default\" xmlns:doc=\"urn:doc\" doc:id=\"123\"/>"
447        );
448        Ok::<(), XmlError>(())
449    }
450
451    #[test]
452    fn fragment_builder_collects_children_from_vec() -> XmlResult<()> {
453        let children = vec![item("a", "A")?, item("b", "B")?];
454        let document = DocumentBuilder::new()
455            .root(element("Items")?.child(children)?)?
456            .build()?;
457
458        assert_eq!(
459            to_string_compact(&document)?,
460            "<Items><Item code=\"a\">A</Item><Item code=\"b\">B</Item></Items>"
461        );
462        Ok::<(), XmlError>(())
463    }
464
465    #[test]
466    fn collections_can_be_added_from_iterators() -> XmlResult<()> {
467        let document = DocumentBuilder::new()
468            .root(
469                element("Items")?.children(
470                    ["a", "b"]
471                        .into_iter()
472                        .map(|code| item(code, code.to_uppercase().as_str())),
473                )?,
474            )?
475            .build()?;
476
477        assert_eq!(
478            to_string_compact(&document)?,
479            "<Items><Item code=\"a\">A</Item><Item code=\"b\">B</Item></Items>"
480        );
481        Ok::<(), XmlError>(())
482    }
483
484    #[test]
485    fn option_represents_conditional_content() -> XmlResult<()> {
486        let include_child = true;
487        let optional = include_child.then(|| element("Optional").and_then(|node| node.text("yes")));
488        let document = DocumentBuilder::new()
489            .root(element("Root")?.child(optional.transpose()?)?)?
490            .build()?;
491
492        assert_eq!(
493            to_string_compact(&document)?,
494            "<Root><Optional>yes</Optional></Root>"
495        );
496        Ok::<(), XmlError>(())
497    }
498
499    #[test]
500    fn rust_functions_can_act_as_components() -> XmlResult<()> {
501        let document = DocumentBuilder::new()
502            .root(element("Root")?.child(component_simple("123")?)?)?
503            .build()?;
504
505        assert_eq!(to_string_compact(&document)?, "<Root><ID>123</ID></Root>");
506        Ok::<(), XmlError>(())
507    }
508
509    #[test]
510    fn children_can_be_passed_to_component_functions() -> XmlResult<()> {
511        let children = fragment()
512            .child(element("A")?.text("one")?)?
513            .child(element("B")?.text("two")?)?;
514        let document = DocumentBuilder::new()
515            .root(component_with_children(children)?)?
516            .build()?;
517
518        assert_eq!(
519            to_string_compact(&document)?,
520            "<Wrapper><A>one</A><B>two</B></Wrapper>"
521        );
522        Ok::<(), XmlError>(())
523    }
524
525    #[test]
526    fn invalid_names_return_xml_error() {
527        let error = element("1invalid").expect_err("invalid name must fail");
528
529        assert_eq!(error.kind(), &crate::core::ErrorKind::InvalidName);
530    }
531}