use crate::core::{
Attribute, Document, ErrorKind, NamespaceDeclaration, NodeId, QName, XmlError, XmlResult,
};
pub fn element(name: impl Into<String>) -> XmlResult<ElementBuilder> {
ElementBuilder::new(name)
}
pub fn text(value: impl Into<String>) -> XmlNode {
XmlNode::Text(value.into())
}
pub fn fragment() -> FragmentBuilder {
FragmentBuilder::new()
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DocumentBuilder {
root: Option<ElementBuilder>,
}
impl DocumentBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn root(mut self, root: ElementBuilder) -> XmlResult<Self> {
if self.root.is_some() {
return Err(XmlError::new(
crate::core::ErrorKind::InvalidOperation,
"document builder already has a root element",
));
}
self.root = Some(root);
Ok(self)
}
pub fn build(self) -> XmlResult<Document> {
let root = self.root.ok_or_else(|| {
XmlError::new(
crate::core::ErrorKind::InvalidOperation,
"document builder requires a root element",
)
})?;
let mut document = Document::new();
let root_id = document.add_root_element(root.name.clone())?;
apply_element(&mut document, root_id, root)?;
Ok(document)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ElementBuilder {
name: QName,
attributes: Vec<Attribute>,
namespaces: Vec<NamespaceDeclaration>,
children: FragmentBuilder,
}
impl ElementBuilder {
pub fn new(local_name: impl Into<String>) -> XmlResult<Self> {
Self::from_qname(QName::new(local_name)?)
}
pub fn qualified(
prefix: impl Into<String>,
local_name: impl Into<String>,
namespace_uri: impl Into<String>,
) -> XmlResult<Self> {
Self::from_qname(QName::qualified(prefix, local_name, namespace_uri)?)
}
pub fn namespaced(
local_name: impl Into<String>,
namespace_uri: impl Into<String>,
) -> XmlResult<Self> {
Self::from_qname(QName::namespaced(local_name, namespace_uri)?)
}
pub fn from_qname(name: QName) -> XmlResult<Self> {
Ok(Self {
name,
attributes: Vec::new(),
namespaces: Vec::new(),
children: FragmentBuilder::new(),
})
}
pub fn attr(mut self, name: impl Into<String>, value: impl ToString) -> XmlResult<Self> {
self.attributes
.push(Attribute::new(QName::new(name)?, value.to_string()));
Ok(self)
}
pub fn qualified_attr(
mut self,
prefix: impl Into<String>,
local_name: impl Into<String>,
namespace_uri: impl Into<String>,
value: impl ToString,
) -> XmlResult<Self> {
self.attributes.push(Attribute::new(
QName::qualified(prefix, local_name, namespace_uri)?,
value.to_string(),
));
Ok(self)
}
pub fn attr_if(self, name: impl Into<String>, value: Option<impl ToString>) -> XmlResult<Self> {
match value {
Some(value) => self.attr(name, value),
None => Ok(self),
}
}
pub fn default_namespace(mut self, uri: impl Into<String>) -> XmlResult<Self> {
self.namespaces.push(NamespaceDeclaration::default(uri)?);
Ok(self)
}
pub fn namespace(
mut self,
prefix: impl Into<String>,
uri: impl Into<String>,
) -> XmlResult<Self> {
self.namespaces
.push(NamespaceDeclaration::prefixed(prefix, uri)?);
Ok(self)
}
pub fn child(mut self, child: impl IntoXmlFragment) -> XmlResult<Self> {
self.children = self.children.child(child)?;
Ok(self)
}
pub fn children<I, T>(mut self, children: I) -> XmlResult<Self>
where
I: IntoIterator<Item = T>,
T: IntoXmlFragment,
{
self.children = self.children.children(children)?;
Ok(self)
}
pub fn text(self, value: impl Into<String>) -> XmlResult<Self> {
self.child(XmlNode::Text(value.into()))
}
pub fn comment(self, value: impl Into<String>) -> XmlResult<Self> {
self.child(XmlNode::Comment(value.into()))
}
pub fn cdata(self, value: impl Into<String>) -> XmlResult<Self> {
self.child(XmlNode::CData(value.into()))
}
pub fn processing_instruction(
self,
target: impl Into<String>,
data: Option<impl Into<String>>,
) -> XmlResult<Self> {
self.child(XmlNode::ProcessingInstruction {
target: target.into(),
data: data.map(Into::into),
})
}
pub fn name(&self) -> &QName {
&self.name
}
pub fn child_nodes(&self) -> &[XmlNode] {
self.children.nodes()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FragmentBuilder {
nodes: Vec<XmlNode>,
}
impl FragmentBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn child(mut self, child: impl IntoXmlFragment) -> XmlResult<Self> {
self.nodes.extend(child.into_xml_fragment()?.nodes);
Ok(self)
}
pub fn children<I, T>(mut self, children: I) -> XmlResult<Self>
where
I: IntoIterator<Item = T>,
T: IntoXmlFragment,
{
for child in children {
self = self.child(child)?;
}
Ok(self)
}
pub fn text(self, value: impl Into<String>) -> XmlResult<Self> {
self.child(XmlNode::Text(value.into()))
}
pub fn nodes(&self) -> &[XmlNode] {
&self.nodes
}
pub fn into_single_element(self) -> XmlResult<ElementBuilder> {
let mut nodes = self.nodes.into_iter();
let Some(first) = nodes.next() else {
return Err(XmlError::new(
ErrorKind::InvalidOperation,
"fragment requires one root element to build a document",
));
};
if nodes.next().is_some() {
return Err(XmlError::new(
ErrorKind::InvalidOperation,
"fragment has multiple top-level nodes; document requires one root element",
));
}
match first {
XmlNode::Element(element) => Ok(element),
_ => Err(XmlError::new(
ErrorKind::InvalidOperation,
"document root must be an element",
)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum XmlNode {
Element(ElementBuilder),
Text(String),
Comment(String),
CData(String),
ProcessingInstruction {
target: String,
data: Option<String>,
},
}
pub trait IntoXmlFragment {
fn into_xml_fragment(self) -> XmlResult<FragmentBuilder>;
}
impl IntoXmlFragment for FragmentBuilder {
fn into_xml_fragment(self) -> XmlResult<FragmentBuilder> {
Ok(self)
}
}
impl IntoXmlFragment for ElementBuilder {
fn into_xml_fragment(self) -> XmlResult<FragmentBuilder> {
Ok(FragmentBuilder {
nodes: vec![XmlNode::Element(self)],
})
}
}
impl IntoXmlFragment for XmlNode {
fn into_xml_fragment(self) -> XmlResult<FragmentBuilder> {
Ok(FragmentBuilder { nodes: vec![self] })
}
}
impl<T> IntoXmlFragment for Option<T>
where
T: IntoXmlFragment,
{
fn into_xml_fragment(self) -> XmlResult<FragmentBuilder> {
match self {
Some(value) => value.into_xml_fragment(),
None => Ok(FragmentBuilder::new()),
}
}
}
impl<T> IntoXmlFragment for XmlResult<T>
where
T: IntoXmlFragment,
{
fn into_xml_fragment(self) -> XmlResult<FragmentBuilder> {
self?.into_xml_fragment()
}
}
impl<T> IntoXmlFragment for Vec<T>
where
T: IntoXmlFragment,
{
fn into_xml_fragment(self) -> XmlResult<FragmentBuilder> {
FragmentBuilder::new().children(self)
}
}
impl IntoXmlFragment for String {
fn into_xml_fragment(self) -> XmlResult<FragmentBuilder> {
XmlNode::Text(self).into_xml_fragment()
}
}
impl IntoXmlFragment for &str {
fn into_xml_fragment(self) -> XmlResult<FragmentBuilder> {
XmlNode::Text(self.to_owned()).into_xml_fragment()
}
}
fn apply_element(
document: &mut Document,
element_id: NodeId,
element: ElementBuilder,
) -> XmlResult<()> {
for namespace in element.namespaces {
document.add_namespace_declaration(element_id, namespace)?;
}
for attribute in element.attributes {
document.add_attribute(element_id, attribute)?;
}
apply_fragment(document, element_id, element.children)
}
fn apply_fragment(
document: &mut Document,
parent: NodeId,
fragment: FragmentBuilder,
) -> XmlResult<()> {
for node in fragment.nodes {
match node {
XmlNode::Element(element) => {
let child_id = document.add_element(parent, element.name.clone())?;
apply_element(document, child_id, element)?;
}
XmlNode::Text(text) => {
document.add_text(parent, text)?;
}
XmlNode::Comment(comment) => {
document.add_comment(parent, comment)?;
}
XmlNode::CData(cdata) => {
document.add_cdata(parent, cdata)?;
}
XmlNode::ProcessingInstruction { target, data } => {
document.add_processing_instruction(parent, target, data)?;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::writer::to_string_compact;
fn item(code: &str, name: &str) -> XmlResult<ElementBuilder> {
element("Item")?.attr("code", code)?.text(name)
}
fn component_simple(id: &str) -> XmlResult<ElementBuilder> {
element("ID")?.text(id)
}
fn component_with_children(children: impl IntoXmlFragment) -> XmlResult<ElementBuilder> {
element("Wrapper")?.child(children)
}
#[test]
fn document_builder_creates_xml_with_three_levels() -> XmlResult<()> {
let document = DocumentBuilder::new()
.root(
element("Root")?
.child(element("Child")?.child(element("Grandchild")?.text("v")?)?)?,
)?
.build()?;
assert_eq!(
to_string_compact(&document)?,
"<Root><Child><Grandchild>v</Grandchild></Child></Root>"
);
Ok::<(), XmlError>(())
}
#[test]
fn builder_serializes_result_with_writer() -> XmlResult<()> {
let document = DocumentBuilder::new()
.root(element("Root")?.text("value")?)?
.build()?;
assert_eq!(to_string_compact(&document)?, "<Root>value</Root>");
Ok::<(), XmlError>(())
}
#[test]
fn document_builder_supports_static_and_dynamic_attributes() -> XmlResult<()> {
let dynamic_value = Some(42);
let document = DocumentBuilder::new()
.root(
element("Root")?
.attr("static", "yes")?
.attr_if("dynamic", dynamic_value)?,
)?
.build()?;
assert_eq!(
to_string_compact(&document)?,
"<Root static=\"yes\" dynamic=\"42\"/>"
);
Ok::<(), XmlError>(())
}
#[test]
fn namespaces_can_be_declared_with_default_and_prefix() -> XmlResult<()> {
let document = DocumentBuilder::new()
.root(
ElementBuilder::qualified("doc", "Root", "urn:doc")?
.default_namespace("urn:default")?
.namespace("doc", "urn:doc")?
.qualified_attr("doc", "id", "urn:doc", "123")?,
)?
.build()?;
assert_eq!(
to_string_compact(&document)?,
"<doc:Root xmlns=\"urn:default\" xmlns:doc=\"urn:doc\" doc:id=\"123\"/>"
);
Ok::<(), XmlError>(())
}
#[test]
fn fragment_builder_collects_children_from_vec() -> XmlResult<()> {
let children = vec![item("a", "A")?, item("b", "B")?];
let document = DocumentBuilder::new()
.root(element("Items")?.child(children)?)?
.build()?;
assert_eq!(
to_string_compact(&document)?,
"<Items><Item code=\"a\">A</Item><Item code=\"b\">B</Item></Items>"
);
Ok::<(), XmlError>(())
}
#[test]
fn collections_can_be_added_from_iterators() -> XmlResult<()> {
let document = DocumentBuilder::new()
.root(
element("Items")?.children(
["a", "b"]
.into_iter()
.map(|code| item(code, code.to_uppercase().as_str())),
)?,
)?
.build()?;
assert_eq!(
to_string_compact(&document)?,
"<Items><Item code=\"a\">A</Item><Item code=\"b\">B</Item></Items>"
);
Ok::<(), XmlError>(())
}
#[test]
fn option_represents_conditional_content() -> XmlResult<()> {
let include_child = true;
let optional = include_child.then(|| element("Optional").and_then(|node| node.text("yes")));
let document = DocumentBuilder::new()
.root(element("Root")?.child(optional.transpose()?)?)?
.build()?;
assert_eq!(
to_string_compact(&document)?,
"<Root><Optional>yes</Optional></Root>"
);
Ok::<(), XmlError>(())
}
#[test]
fn rust_functions_can_act_as_components() -> XmlResult<()> {
let document = DocumentBuilder::new()
.root(element("Root")?.child(component_simple("123")?)?)?
.build()?;
assert_eq!(to_string_compact(&document)?, "<Root><ID>123</ID></Root>");
Ok::<(), XmlError>(())
}
#[test]
fn children_can_be_passed_to_component_functions() -> XmlResult<()> {
let children = fragment()
.child(element("A")?.text("one")?)?
.child(element("B")?.text("two")?)?;
let document = DocumentBuilder::new()
.root(component_with_children(children)?)?
.build()?;
assert_eq!(
to_string_compact(&document)?,
"<Wrapper><A>one</A><B>two</B></Wrapper>"
);
Ok::<(), XmlError>(())
}
#[test]
fn invalid_names_return_xml_error() {
let error = element("1invalid").expect_err("invalid name must fail");
assert_eq!(error.kind(), &crate::core::ErrorKind::InvalidName);
}
}