Skip to main content

fakecloud_cloudfront/
xml_io.rs

1//! XML serialization helpers.
2//!
3//! CloudFront's wire format is REST-XML with `xmlns` carried on every
4//! top-level response element. `quick-xml` doesn't emit a namespace from
5//! a serde derive, so we wrap the serialized body to inject it.
6
7use quick_xml::de::from_str;
8use quick_xml::se::to_string_with_root;
9use serde::de::DeserializeOwned;
10use serde::Serialize;
11
12use crate::NAMESPACE;
13
14const XML_DECL: &str = r#"<?xml version="1.0" encoding="UTF-8"?>"#;
15
16/// Serialize a serde value as `<Root xmlns="...">...</Root>` with a
17/// leading XML declaration.
18pub fn to_xml_root<T: Serialize>(root: &str, value: &T) -> Result<String, quick_xml::DeError> {
19    let inner =
20        to_string_with_root(root, value).map_err(|e| quick_xml::DeError::Custom(e.to_string()))?;
21    Ok(inject_namespace(&inner, root))
22}
23
24fn inject_namespace(inner: &str, root: &str) -> String {
25    let open_tag = format!("<{root}>");
26    let open_with_ns = format!("<{root} xmlns=\"{NAMESPACE}\">");
27    let stamped = if let Some(rest) = inner.strip_prefix(&open_tag) {
28        format!("{open_with_ns}{rest}")
29    } else {
30        let self_close = format!("<{root}/>");
31        if let Some(rest) = inner.strip_prefix(&self_close) {
32            format!("<{root} xmlns=\"{NAMESPACE}\"/>{rest}")
33        } else {
34            inner.to_string()
35        }
36    };
37    format!("{XML_DECL}{stamped}")
38}
39
40/// Parse an XML body into a serde-deserializable type.
41pub fn from_xml_root<T: DeserializeOwned>(body: &[u8]) -> Result<T, quick_xml::DeError> {
42    let s = std::str::from_utf8(body)
43        .map_err(|e| quick_xml::DeError::Custom(format!("invalid utf-8 in body: {e}")))?;
44    from_str(s)
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use serde::{Deserialize, Serialize};
51
52    #[derive(Serialize, Deserialize, PartialEq, Debug)]
53    #[serde(rename_all = "PascalCase")]
54    struct Wrap {
55        inner: String,
56    }
57
58    #[test]
59    fn roundtrip_root_with_namespace() {
60        let value = Wrap {
61            inner: "hello".into(),
62        };
63        let xml = to_xml_root("Wrap", &value).unwrap();
64        assert!(xml.contains("xmlns=\"http://cloudfront.amazonaws.com/doc/2020-05-31/\""));
65        assert!(xml.contains("<Inner>hello</Inner>"));
66        let parsed: Wrap = from_xml_root(xml.as_bytes()).unwrap();
67        assert_eq!(parsed, value);
68    }
69
70    #[test]
71    fn empty_struct_renders_self_closing() {
72        #[derive(Serialize, Deserialize)]
73        #[serde(rename_all = "PascalCase")]
74        struct Empty {}
75        let xml = to_xml_root("Empty", &Empty {}).unwrap();
76        assert!(xml.contains("<Empty xmlns=\"http://cloudfront.amazonaws.com/doc/2020-05-31/\"/>"));
77    }
78}