xml-creator 0.1.1

Create simple xml files
Documentation
//! `xml_creator`
//! building a simple xml (with childs, attributes, CDATA...)
//!
//! # Usage
//!
//! Use [XMLElement](struct.XMLElement.html) to create elements with everything you need.
//!
//! You can write an XML document by calling
//! [write](struct.XMLElement.html#method.write) on your root element.
//!
//! # Example
//!
//! ```rust
//! # use std::io;
//! # fn main() -> io::Result<()> {
//! use std::fs::File;
//! use xml_creator::XMLElement;
//!
//! # /*
//! let mut file = File::create("example.xml").unwrap();
//! # */
//! # let mut file: Vec<u8> = Vec::new();
//!
//! let mut company = XMLElement::new("company");
//! company.add_attribute("name", "val");
//!
//! let mut employee = XMLElement::new("employee");
//! // no escaping and CDATA needed
//! employee.add_text("Max Mustermann".to_string(), false, false);
//!
//! let mut cdata = XMLElement::new("cdata");
//! // will get wrapped into CDATA
//! cdata.add_text("<p>Some Html</p>".to_string(), false, true);
//! company.add_child(cdata);
//!
//! let mut escape = XMLElement::new("escape");
//! // < will get escaped
//! escape.add_text("<".to_string(), true, false);
//! company.add_child(escape);
//!
//! // add employee to company
//! company.add_child(employee);
//!
//! company.write(file).unwrap();
//! # Ok(())
//! # }
//! ```
//! `sample.xml` will contain:
//! ```xml
//! <?xml version = "1.0" encoding = "UTF-8"?>
//! <company name="val">
//!     <employee>Max Mustermann</employee>
//!     <cdata><![CDATA[<p>Some Html</p>]]></cdata>
//!     <escape>&lt;</escape>
//! </company>
//! ```

#![doc(html_root_url = "https://docs.rs/xml-creator/0.0.1")]

extern crate indexmap;
use indexmap::IndexMap;
use std::fmt;
use std::io::{self, Write};

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct XMLElement {
    name: String,
    attributes: IndexMap<String, String>,
    content: XMLElementContent,
}

#[derive(Debug, Clone, Eq, PartialEq)]
enum XMLElementContent {
    Empty,
    Elements(Vec<XMLElement>),
    Text(String),
}

impl fmt::Display for XMLElement {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let mut s: Vec<u8> = Vec::new();
        self.write(&mut s)
            .expect("Failure writing output to Vec<u8>");
        write!(f, "{}",  { String::from_utf8(s) }.unwrap())
    }
}

impl XMLElement {
    pub fn new(name: impl ToString) -> Self {
        XMLElement {
            name: name.to_string(),
            attributes: IndexMap::new(),
            content: XMLElementContent::Empty,
        }
    }

    pub fn add_attribute(&mut self, name: impl ToString, value: impl ToString) {
        self.attributes
            .insert(name.to_string(), escape_str(&value.to_string()));
    }

    pub fn add_child(&mut self, child: XMLElement) {
        use XMLElementContent::*;
        match self.content {
            Empty => {
                self.content = Elements(vec![child]);
            }
            Elements(ref mut list) => {
                list.push(child);
            }
            Text(_) => {
                panic!("Attempted adding child element to element with text.");
            }
        }
    }

    pub fn add_text(&mut self, mut text: String, escape: bool, cdata: bool) {
        use XMLElementContent::*;

        if cdata {
            text = wrap_in_cdata(text);
        }

        match self.content {
            Empty => {
                if escape {
                    self.content = Text(escape_str(&text));
                } else {
                    self.content = Text(text);
                }
            }
            _ => {
                panic!("Attempted adding text to non-empty element.");
            }
        }
    }

    pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
        writeln!(writer, r#"<?xml version = "1.0" encoding = "UTF-8"?>"#)?;
        self.write_level(&mut writer, 0)
    }

    fn write_level<W: Write>(&self, writer: &mut W, level: usize) -> io::Result<()> {
        use XMLElementContent::*;
        let prefix = "\t".repeat(level);
        match &self.content {
            Empty => {
                writeln!(
                    writer,
                    "{}<{}{} />",
                    prefix,
                    self.name,
                    self.attribute_string()
                )?;
            }
            Elements(list) => {
                writeln!(
                    writer,
                    "{}<{}{}>",
                    prefix,
                    self.name,
                    self.attribute_string()
                )?;
                for elem in list {
                    elem.write_level(writer, level + 1)?;
                }
                writeln!(writer, "{}</{}>", prefix, self.name)?;
            }
            Text(text) => {
                writeln!(
                    writer,
                    "{}<{}{}>{}</{1}>",
                    prefix,
                    self.name,
                    self.attribute_string(),
                    text
                )?;
            }
        }
        Ok(())
    }

    fn attribute_string(&self) -> String {
        if self.attributes.is_empty() {
            "".to_owned()
        } else {
            let mut result = "".to_owned();
            for (k, v) in &self.attributes {
                result = result + &format!(r#" {}="{}""#, k, v);
            }
            result
        }
    }
}

fn escape_str(input: &str) -> String {
    input
        .replace('&', "&amp;")
        .replace('"', "&quot;")
        .replace('\'', "&apos;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
}

fn wrap_in_cdata(string: String) -> String {
    format!("{}{}{}", r#"<![CDATA["#, string, "]]>")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn write_xml() {
        let mut root = XMLElement::new("root");
        let mut child1 = XMLElement::new("child1");
        let inner1 = XMLElement::new("inner");
        child1.add_child(inner1);
        let mut inner2 = XMLElement::new("inner");
        inner2.add_text("Example Text\nNew line".to_string(), true, false);
        child1.add_child(inner2);
        root.add_child(child1);
        let mut child2 = XMLElement::new("child2");
        child2.add_attribute("at1", "test &");
        child2.add_attribute("at2", "test <");
        child2.add_attribute("at3", "test \"");
        let mut inner3 = XMLElement::new("inner");
        inner3.add_attribute("test", "example");
        child2.add_child(inner3);
        root.add_child(child2);
        let mut child3 = XMLElement::new("child3");
        child3.add_text("&< &".to_string(), true, false);
        root.add_child(child3);
        let mut child4 = XMLElement::new("child4");
        child4.add_attribute("non-str-attribute", 5);
        child4.add_text("6".to_string(), true, false);
        root.add_child(child4);

        let expected = r#"<?xml version = "1.0" encoding = "UTF-8"?>
<root>
	<child1>
		<inner />
		<inner>Example Text
New line</inner>
	</child1>
	<child2 at1="test &amp;" at2="test &lt;" at3="test &quot;">
		<inner test="example" />
	</child2>
	<child3>&amp;&lt; &amp;</child3>
	<child4 non-str-attribute="5">6</child4>
</root>
"#;
        assert_eq!(
            format!("{}", root),
            expected,
            "Attempt to write XML did not give expected results."
        );
    }

    #[test]
    #[should_panic]
    fn add_text_to_parent_element() {
        let mut e = XMLElement::new("test");
        e.add_child(XMLElement::new("test"));
        e.add_text("example text".to_string(), false, false);
    }

    #[test]
    #[should_panic]
    fn add_child_to_text_element() {
        let mut e = XMLElement::new("test");
        e.add_text("example text".to_string(), false, false);
        e.add_child(XMLElement::new("test"));
    }
}