Skip to main content

nextcloud_dav/xml/
writer.rs

1//! A minimal XML writer: open, close, empty, text.
2
3use super::escape::escape_text;
4
5/// Builds XML by appending element markup to a string.
6///
7/// The writer treats names as opaque: the caller owns namespace prefixes and
8/// their declarations, and closing an element is the caller's promise that it
9/// was opened. That is all a fixed request body needs, and it keeps the
10/// output byte-for-byte predictable in tests.
11#[derive(Debug, Default)]
12pub struct XmlWriter {
13    output: String,
14}
15
16impl XmlWriter {
17    /// An empty document.
18    pub fn new() -> Self {
19        Self::default()
20    }
21
22    /// Writes the XML declaration.
23    pub fn prolog(&mut self) {
24        self.output
25            .push_str("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
26    }
27
28    /// Opens `<name>`.
29    pub fn open(&mut self, name: &str) {
30        self.open_with(name, &[]);
31    }
32
33    /// Opens `<name>` with attributes in the order given.
34    pub fn open_with(&mut self, name: &str, attributes: &[(&str, &str)]) {
35        self.output.push('<');
36        self.output.push_str(name);
37        for (key, value) in attributes {
38            self.output.push(' ');
39            self.output.push_str(key);
40            self.output.push_str("=\"");
41            self.output.push_str(&escape_text(value));
42            self.output.push('"');
43        }
44        self.output.push('>');
45    }
46
47    /// Closes `</name>`.
48    pub fn close(&mut self, name: &str) {
49        self.output.push_str("</");
50        self.output.push_str(name);
51        self.output.push('>');
52    }
53
54    /// Writes a self-closing `<name/>`.
55    pub fn empty(&mut self, name: &str) {
56        self.output.push('<');
57        self.output.push_str(name);
58        self.output.push_str("/>");
59    }
60
61    /// Writes escaped character data.
62    pub fn text(&mut self, value: &str) {
63        self.output.push_str(&escape_text(value));
64    }
65
66    /// The document built so far.
67    pub fn as_str(&self) -> &str {
68        &self.output
69    }
70
71    /// Consumes the writer, yielding the document.
72    pub fn into_string(self) -> String {
73        self.output
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::XmlWriter;
80
81    #[test]
82    fn nests_elements() {
83        let mut writer = XmlWriter::new();
84        writer.open("d:prop");
85        writer.empty("d:getetag");
86        writer.close("d:prop");
87        assert_eq!(writer.as_str(), "<d:prop><d:getetag/></d:prop>");
88    }
89
90    #[test]
91    fn escapes_attribute_values_and_text() {
92        let mut writer = XmlWriter::new();
93        writer.open_with("e", &[("name", "a&b")]);
94        writer.text("1 < 2");
95        writer.close("e");
96        assert_eq!(writer.as_str(), "<e name=\"a&amp;b\">1 &lt; 2</e>");
97    }
98}