fakecloud_aws/
ec2query.rs1use crate::xml::xml_escape;
23
24pub const EC2_NAMESPACE: &str = "http://ec2.amazonaws.com/doc/2016-11-15/";
27
28pub fn ec2_response(action: &str, request_id: &str, body: &str) -> String {
41 format!(
42 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
43 <{action}Response xmlns=\"{EC2_NAMESPACE}\">\
44 <requestId>{rid}</requestId>{body}</{action}Response>",
45 rid = xml_escape(request_id),
46 )
47}
48
49pub fn ec2_elem(name: &str, value: &str) -> String {
51 format!("<{name}>{}</{name}>", xml_escape(value))
52}
53
54pub fn ec2_elem_opt(name: &str, value: Option<&str>) -> String {
57 match value {
58 Some(v) => ec2_elem(name, v),
59 None => String::new(),
60 }
61}
62
63pub fn ec2_bool(name: &str, value: bool) -> String {
65 format!("<{name}>{value}</{name}>")
66}
67
68pub fn ec2_return(value: bool) -> String {
72 format!("<return>{value}</return>")
73}
74
75pub fn ec2_list(wrapper: &str, items: &[String]) -> String {
82 if items.is_empty() {
83 return format!("<{wrapper}/>");
84 }
85 let mut out = String::with_capacity(items.len() * 16 + wrapper.len() * 2 + 5);
86 out.push('<');
87 out.push_str(wrapper);
88 out.push('>');
89 for item in items {
90 out.push_str("<item>");
91 out.push_str(item);
92 out.push_str("</item>");
93 }
94 out.push_str("</");
95 out.push_str(wrapper);
96 out.push('>');
97 out
98}
99
100pub fn ec2_scalar_list(wrapper: &str, values: &[String]) -> String {
103 let items: Vec<String> = values.iter().map(|v| xml_escape(v)).collect();
104 ec2_list(wrapper, &items)
105}
106
107pub fn ec2_tag_set(tags: &[(String, String)]) -> String {
110 let items: Vec<String> = tags
111 .iter()
112 .map(|(k, v)| format!("{}{}", ec2_elem("key", k), ec2_elem("value", v)))
113 .collect();
114 ec2_list("tagSet", &items)
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 fn contains_balanced(haystack: &str, tag: &str) -> bool {
125 haystack.contains(&format!("<{tag}>")) && haystack.contains(&format!("</{tag}>"))
126 }
127
128 #[test]
129 fn response_envelope_has_no_result_wrapper_and_lowercase_request_id() {
130 let xml = ec2_response("DeleteVpc", "req-123", &ec2_return(true));
131 assert!(xml.starts_with("<?xml"));
132 assert!(
133 xml.contains("<DeleteVpcResponse xmlns=\"http://ec2.amazonaws.com/doc/2016-11-15/\">")
134 );
135 assert!(xml.contains("<requestId>req-123</requestId>"));
137 assert!(!xml.contains("ResponseMetadata"));
138 assert!(!xml.contains("Result>"));
139 assert!(xml.contains("<return>true</return>"));
140 assert!(xml.ends_with("</DeleteVpcResponse>"));
141 }
142
143 #[test]
144 fn list_flattens_with_item_children_no_member_wrapper() {
145 let items = vec![
146 "<instanceId>i-1</instanceId>".to_string(),
147 "<instanceId>i-2</instanceId>".to_string(),
148 ];
149 let xml = ec2_list("instancesSet", &items);
150 assert_eq!(
151 xml,
152 "<instancesSet><item><instanceId>i-1</instanceId></item>\
153 <item><instanceId>i-2</instanceId></item></instancesSet>"
154 );
155 assert!(!xml.contains("<member>"));
156 }
157
158 #[test]
159 fn empty_list_emits_self_closing_wrapper() {
160 assert_eq!(ec2_list("instancesSet", &[]), "<instancesSet/>");
161 assert_eq!(ec2_scalar_list("groupSet", &[]), "<groupSet/>");
162 assert_eq!(ec2_tag_set(&[]), "<tagSet/>");
163 }
164
165 #[test]
166 fn scalar_list_escapes_and_wraps_each_value() {
167 let xml = ec2_scalar_list("groupSet", &["sg-1".to_string(), "a&b".to_string()]);
168 assert_eq!(
169 xml,
170 "<groupSet><item>sg-1</item><item>a&b</item></groupSet>"
171 );
172 }
173
174 #[test]
175 fn tag_set_renders_key_value_items() {
176 let tags = vec![
177 ("Name".to_string(), "web".to_string()),
178 ("env".to_string(), "prod & test".to_string()),
179 ];
180 let xml = ec2_tag_set(&tags);
181 assert!(contains_balanced(&xml, "tagSet"));
182 assert!(xml.contains("<item><key>Name</key><value>web</value></item>"));
183 assert!(xml.contains("<value>prod & test</value>"));
185 }
186
187 #[test]
188 fn elem_helpers_escape_and_omit() {
189 assert_eq!(
190 ec2_elem("imageId", "ami-<1>"),
191 "<imageId>ami-<1></imageId>"
192 );
193 assert_eq!(ec2_elem_opt("publicIp", None), "");
194 assert_eq!(
195 ec2_elem_opt("publicIp", Some("1.2.3.4")),
196 "<publicIp>1.2.3.4</publicIp>"
197 );
198 assert_eq!(
199 ec2_bool("ebsOptimized", false),
200 "<ebsOptimized>false</ebsOptimized>"
201 );
202 }
203
204 #[test]
205 fn nested_reservation_shape_round_trips_structurally() {
206 let instance = format!(
208 "{}{}{}",
209 ec2_elem("instanceId", "i-abc"),
210 ec2_elem("imageId", "ami-1"),
211 ec2_tag_set(&[("Name".to_string(), "x".to_string())]),
212 );
213 let instances = ec2_list("instancesSet", &[instance]);
214 let reservation = format!("{}{}", ec2_elem("reservationId", "r-1"), instances);
215 let body = ec2_list("reservationSet", &[reservation]);
216 let xml = ec2_response("DescribeInstances", "r-9", &body);
217 assert!(contains_balanced(&xml, "reservationSet"));
218 assert!(contains_balanced(&xml, "instancesSet"));
219 assert!(xml.contains("<reservationId>r-1</reservationId>"));
220 assert!(xml.contains("<instanceId>i-abc</instanceId>"));
221 assert_eq!(xml.matches("<item>").count(), 3);
223 }
224}