Skip to main content

rustack_cloudfront_http/xml/
writer.rs

1//! Minimal XML writer — linear, allocation-aware, no external schema.
2
3/// Sequential XML writer used to emit CloudFront response bodies.
4#[derive(Debug, Default)]
5pub struct XmlWriter {
6    buf: String,
7}
8
9impl XmlWriter {
10    /// Create an empty writer with a default capacity.
11    #[must_use]
12    pub fn new() -> Self {
13        Self::with_capacity(512)
14    }
15
16    /// Create a writer pre-reserving `cap` bytes.
17    #[must_use]
18    pub fn with_capacity(cap: usize) -> Self {
19        Self {
20            buf: String::with_capacity(cap),
21        }
22    }
23
24    /// Emit the XML declaration.
25    pub fn declaration(&mut self) {
26        self.buf
27            .push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
28    }
29
30    /// Open element with an optional `xmlns` attribute.
31    pub fn open_root(&mut self, name: &str, namespace: Option<&str>) {
32        self.buf.push('<');
33        self.buf.push_str(name);
34        if let Some(ns) = namespace {
35            self.buf.push_str(" xmlns=\"");
36            self.buf.push_str(ns);
37            self.buf.push('\"');
38        }
39        self.buf.push('>');
40    }
41
42    /// Open element.
43    pub fn open(&mut self, name: &str) {
44        self.buf.push('<');
45        self.buf.push_str(name);
46        self.buf.push('>');
47    }
48
49    /// Close element.
50    pub fn close(&mut self, name: &str) {
51        self.buf.push_str("</");
52        self.buf.push_str(name);
53        self.buf.push('>');
54    }
55
56    /// Emit `<name/>`.
57    pub fn empty(&mut self, name: &str) {
58        self.buf.push('<');
59        self.buf.push_str(name);
60        self.buf.push_str("/>");
61    }
62
63    /// Emit `<name>text</name>` with escaping.
64    pub fn element(&mut self, name: &str, text: &str) {
65        self.open(name);
66        self.buf.push_str(&escape(text));
67        self.close(name);
68    }
69
70    /// Emit `<name>value</name>` using `Display`.
71    pub fn element_display<D: std::fmt::Display>(&mut self, name: &str, v: D) {
72        let s = v.to_string();
73        self.element(name, &s);
74    }
75
76    /// Emit `<name>true</name>` / `<name>false</name>`.
77    pub fn bool(&mut self, name: &str, v: bool) {
78        self.element(name, if v { "true" } else { "false" });
79    }
80
81    /// Emit an optional string element only when `v` is non-empty.
82    pub fn optional_str(&mut self, name: &str, v: &str) {
83        if !v.is_empty() {
84            self.element(name, v);
85        }
86    }
87
88    /// Emit a `<Quantity>n</Quantity><Items>…</Items>` wrapped list.
89    pub fn items<T, F: FnMut(&mut Self, &T)>(
90        &mut self,
91        items: &[T],
92        wrapper: &str,
93        item_name: &str,
94        mut emit: F,
95    ) {
96        self.open(wrapper);
97        self.element_display("Quantity", items.len());
98        if items.is_empty() {
99            // CloudFront omits Items entirely when the list is empty.
100        } else {
101            self.open("Items");
102            for it in items {
103                self.open(item_name);
104                emit(self, it);
105                self.close(item_name);
106            }
107            self.close("Items");
108        }
109        self.close(wrapper);
110    }
111
112    /// Consume the writer and return the XML body.
113    #[must_use]
114    pub fn finish(self) -> String {
115        self.buf
116    }
117
118    /// Append raw, pre-escaped XML. Caller is responsible for correctness.
119    pub fn raw(&mut self, s: &str) {
120        self.buf.push_str(s);
121    }
122}
123
124fn escape(s: &str) -> String {
125    let mut out = String::with_capacity(s.len());
126    for c in s.chars() {
127        match c {
128            '&' => out.push_str("&amp;"),
129            '<' => out.push_str("&lt;"),
130            '>' => out.push_str("&gt;"),
131            '"' => out.push_str("&quot;"),
132            '\'' => out.push_str("&apos;"),
133            _ => out.push(c),
134        }
135    }
136    out
137}