simple_xml_builder/
lib.rs

1//! `simple_xml_builder` provides basic functionality for building and
2//! outputting XML documents.
3//!
4//! The constructed model is write-only.
5//!
6//! # Usage
7//!
8//! Use [XMLElement](struct.XMLElement.html) to create elements with tags,
9//! attributes, and either text or children.
10//! You can write an XML document by calling
11//! [write](struct.XMLElement.html#method.write) on your root element.
12//!
13//! # Example
14//!
15//! ```rust
16//! # use std::io;
17//! # fn main() -> io::Result<()> {
18//! use std::fs::File;
19//! use simple_xml_builder::XMLElement;
20//!
21//! # /*
22//! let mut file = File::create("sample.xml")?;
23//! # */
24//! # let mut file: Vec<u8> = Vec::new();
25//!
26//! let mut person = XMLElement::new("person");
27//! person.add_attribute("id", "232");
28//! let mut name = XMLElement::new("name");
29//! name.add_text("Joe Schmoe");
30//! person.add_child(name);
31//! let mut age = XMLElement::new("age");
32//! age.add_text("24");
33//! person.add_child(age);
34//! let hobbies = XMLElement::new("hobbies");
35//! person.add_child(hobbies);
36//!
37//! person.write(file)?;
38//! # Ok(())
39//! # }
40//! ```
41//! `sample.xml` will contain:
42//! ```xml
43//! <?xml version = "1.0" encoding = "UTF-8"?>
44//! <person id="232">
45//!     <name>Joe Schmoe</name>
46//!     <age>24</age>
47//!     <hobbies />
48//! </person>
49//! ```
50
51#![doc(html_root_url = "https://docs.rs/simple-xml-builder/1.1.0")]
52
53extern crate indexmap;
54use indexmap::IndexMap;
55use std::fmt;
56use std::io::{self, Write};
57
58/// Represents an XML element
59#[derive(Debug, Clone, Eq, PartialEq)]
60pub struct XMLElement {
61    name: String,
62    attributes: IndexMap<String, String>,
63    content: XMLElementContent,
64}
65
66#[derive(Debug, Clone, Eq, PartialEq)]
67enum XMLElementContent {
68    Empty,
69    Elements(Vec<XMLElement>),
70    Text(String),
71}
72
73impl fmt::Display for XMLElement {
74    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
75        let mut s: Vec<u8> = Vec::new();
76        self.write(&mut s)
77            .expect("Failure writing output to Vec<u8>");
78        write!(f, "{}", unsafe { String::from_utf8_unchecked(s) })
79    }
80}
81
82impl XMLElement {
83    /// Creates a new empty XML element using the given name for the tag.
84    pub fn new(name: impl ToString) -> Self {
85        XMLElement {
86            name: name.to_string(),
87            attributes: IndexMap::new(),
88            content: XMLElementContent::Empty,
89        }
90    }
91
92    /// Adds an attribute to the XML element. The attribute value can take any
93    /// type which implements [`fmt::Display`].
94    pub fn add_attribute(&mut self, name: impl ToString, value: impl ToString) {
95        self.attributes
96            .insert(name.to_string(), escape_str(&value.to_string()));
97    }
98
99    /// Adds a child element to the XML element.
100    /// The new child will be placed after previously added children.
101    ///
102    /// This method may only be called on an element that has children or is
103    /// empty.
104    ///
105    /// # Panics
106    ///
107    /// Panics if the element contains text.
108    pub fn add_child(&mut self, child: XMLElement) {
109        use XMLElementContent::*;
110        match self.content {
111            Empty => {
112                self.content = Elements(vec![child]);
113            }
114            Elements(ref mut list) => {
115                list.push(child);
116            }
117            Text(_) => {
118                panic!("Attempted adding child element to element with text.");
119            }
120        }
121    }
122
123    /// Adds text to the XML element.
124    ///
125    /// This method may only be called on an empty element.
126    ///
127    /// # Panics
128    ///
129    /// Panics if the element is not empty.
130    pub fn add_text(&mut self, text: impl ToString) {
131        use XMLElementContent::*;
132        match self.content {
133            Empty => {
134                self.content = Text(escape_str(&text.to_string()));
135            }
136            _ => {
137                panic!("Attempted adding text to non-empty element.");
138            }
139        }
140    }
141
142    /// Outputs a UTF-8 XML document, where this element is the root element.
143    ///
144    /// Output is properly indented.
145    ///
146    /// # Errors
147    ///
148    /// Returns Errors from writing to the Write object.
149    pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
150        writeln!(writer, r#"<?xml version = "1.0" encoding = "UTF-8"?>"#)?;
151        self.write_level(&mut writer, 0)
152    }
153
154    fn write_level<W: Write>(&self, writer: &mut W, level: usize) -> io::Result<()> {
155        use XMLElementContent::*;
156        let prefix = "\t".repeat(level);
157        match &self.content {
158            Empty => {
159                writeln!(
160                    writer,
161                    "{}<{}{} />",
162                    prefix,
163                    self.name,
164                    self.attribute_string()
165                )?;
166            }
167            Elements(list) => {
168                writeln!(
169                    writer,
170                    "{}<{}{}>",
171                    prefix,
172                    self.name,
173                    self.attribute_string()
174                )?;
175                for elem in list {
176                    elem.write_level(writer, level + 1)?;
177                }
178                writeln!(writer, "{}</{}>", prefix, self.name)?;
179            }
180            Text(text) => {
181                writeln!(
182                    writer,
183                    "{}<{}{}>{}</{1}>",
184                    prefix,
185                    self.name,
186                    self.attribute_string(),
187                    text
188                )?;
189            }
190        }
191        Ok(())
192    }
193
194    fn attribute_string(&self) -> String {
195        if self.attributes.is_empty() {
196            "".to_owned()
197        } else {
198            let mut result = "".to_owned();
199            for (k, v) in &self.attributes {
200                result = result + &format!(r#" {}="{}""#, k, v);
201            }
202            result
203        }
204    }
205}
206
207fn escape_str(input: &str) -> String {
208    input
209        .replace('&', "&amp;")
210        .replace('"', "&quot;")
211        .replace('\'', "&apos;")
212        .replace('<', "&lt;")
213        .replace('>', "&gt;")
214}
215
216#[cfg(test)]
217mod tests {
218    use XMLElement;
219
220    #[test]
221    fn write_xml() {
222        let mut root = XMLElement::new("root");
223        let mut child1 = XMLElement::new("child1");
224        let inner1 = XMLElement::new("inner");
225        child1.add_child(inner1);
226        let mut inner2 = XMLElement::new("inner");
227        inner2.add_text("Example Text\nNew line");
228        child1.add_child(inner2);
229        root.add_child(child1);
230        let mut child2 = XMLElement::new("child2");
231        child2.add_attribute("at1", "test &");
232        child2.add_attribute("at2", "test <");
233        child2.add_attribute("at3", "test \"");
234        let mut inner3 = XMLElement::new("inner");
235        inner3.add_attribute("test", "example");
236        child2.add_child(inner3);
237        root.add_child(child2);
238        let mut child3 = XMLElement::new("child3");
239        child3.add_text("&< &");
240        root.add_child(child3);
241        let mut child4 = XMLElement::new("child4");
242        child4.add_attribute("non-str-attribute", 5);
243        child4.add_text(6);
244        root.add_child(child4);
245
246        let expected = r#"<?xml version = "1.0" encoding = "UTF-8"?>
247<root>
248	<child1>
249		<inner />
250		<inner>Example Text
251New line</inner>
252	</child1>
253	<child2 at1="test &amp;" at2="test &lt;" at3="test &quot;">
254		<inner test="example" />
255	</child2>
256	<child3>&amp;&lt; &amp;</child3>
257	<child4 non-str-attribute="5">6</child4>
258</root>
259"#;
260        assert_eq!(
261            format!("{}", root),
262            expected,
263            "Attempt to write XML did not give expected results."
264        );
265    }
266
267    #[test]
268    #[should_panic]
269    fn add_text_to_parent_element() {
270        let mut e = XMLElement::new("test");
271        e.add_child(XMLElement::new("test"));
272        e.add_text("example text");
273    }
274
275    #[test]
276    #[should_panic]
277    fn add_child_to_text_element() {
278        let mut e = XMLElement::new("test");
279        e.add_text("example text");
280        e.add_child(XMLElement::new("test"));
281    }
282}