1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
8#![allow(clippy::derive_partial_eq_without_eq)]
10#![warn(
11 rustdoc::missing_crate_level_docs,
13 unreachable_pub,
14 rust_2018_idioms
15)]
16
17use aws_smithy_types::date_time::{DateTimeFormatError, Format};
20use aws_smithy_types::primitive::Encoder;
21use aws_smithy_types::{DateTime, Number};
22use std::borrow::Cow;
23use std::fmt::Write;
24use urlencoding::encode;
25
26pub struct QueryWriter<'a> {
27 output: &'a mut String,
28}
29
30impl<'a> QueryWriter<'a> {
31 pub fn new(output: &'a mut String, action: &str, version: &str) -> Self {
32 output.push_str("Action=");
33 output.push_str(&encode(action));
34 output.push_str("&Version=");
35 output.push_str(&encode(version));
36 QueryWriter { output }
37 }
38
39 pub fn prefix(&mut self, prefix: &'a str) -> QueryValueWriter<'_> {
40 QueryValueWriter::new(self.output, Cow::Borrowed(prefix))
41 }
42
43 pub fn finish(self) {
44 }
46}
47
48#[must_use]
49pub struct QueryMapWriter<'a> {
50 output: &'a mut String,
51 prefix: Cow<'a, str>,
52 flatten: bool,
53 key_name: &'static str,
54 value_name: &'static str,
55 next_index: usize,
56}
57
58impl<'a> QueryMapWriter<'a> {
59 fn new(
60 output: &'a mut String,
61 prefix: Cow<'a, str>,
62 flatten: bool,
63 key_name: &'static str,
64 value_name: &'static str,
65 ) -> QueryMapWriter<'a> {
66 QueryMapWriter {
67 prefix,
68 output,
69 flatten,
70 key_name,
71 value_name,
72 next_index: 1,
73 }
74 }
75
76 pub fn entry(&mut self, key: &str) -> QueryValueWriter<'_> {
77 let entry = if self.flatten { "" } else { ".entry" };
78 write!(
79 &mut self.output,
80 "&{}{}.{}.{}={}",
81 self.prefix,
82 entry,
83 self.next_index,
84 self.key_name,
85 encode(key)
86 )
87 .unwrap();
90 let value_name = format!(
91 "{}{}.{}.{}",
92 self.prefix, entry, self.next_index, self.value_name
93 );
94
95 self.next_index += 1;
96 QueryValueWriter::new(self.output, Cow::Owned(value_name))
97 }
98
99 pub fn finish(self) {
100 }
102}
103
104#[must_use]
105pub struct QueryListWriter<'a> {
106 output: &'a mut String,
107 prefix: Cow<'a, str>,
108 flatten: bool,
109 member_override: Option<&'a str>,
110 next_index: usize,
111}
112
113impl<'a> QueryListWriter<'a> {
114 fn new(
115 output: &'a mut String,
116 prefix: Cow<'a, str>,
117 flatten: bool,
118 member_override: Option<&'a str>,
119 ) -> QueryListWriter<'a> {
120 QueryListWriter {
121 prefix,
122 output,
123 flatten,
124 member_override,
125 next_index: 1,
126 }
127 }
128
129 pub fn entry(&mut self) -> QueryValueWriter<'_> {
130 let value_name = if self.flatten {
131 format!("{}.{}", self.prefix, self.next_index)
132 } else if self.member_override.is_some() {
133 format!(
134 "{}.{}.{}",
135 self.prefix,
136 self.member_override.unwrap(),
137 self.next_index
138 )
139 } else {
140 format!("{}.member.{}", self.prefix, self.next_index)
141 };
142
143 self.next_index += 1;
144 QueryValueWriter::new(self.output, Cow::Owned(value_name))
145 }
146
147 pub fn finish(self) {
148 if self.next_index == 1 {
151 QueryValueWriter::new(self.output, self.prefix).write_param_name();
152 }
153 }
154}
155
156#[must_use]
157pub struct QueryValueWriter<'a> {
158 output: &'a mut String,
159 prefix: Cow<'a, str>,
160}
161
162impl<'a> QueryValueWriter<'a> {
163 pub fn new(output: &'a mut String, prefix: Cow<'a, str>) -> QueryValueWriter<'a> {
164 QueryValueWriter { output, prefix }
165 }
166
167 pub fn prefix(&mut self, prefix: &'a str) -> QueryValueWriter<'_> {
169 QueryValueWriter::new(
170 self.output,
171 Cow::Owned(format!("{}.{}", self.prefix, prefix)),
172 )
173 }
174
175 pub fn boolean(mut self, value: bool) {
177 self.write_param_name();
178 self.output.push_str(match value {
179 true => "true",
180 _ => "false",
181 });
182 }
183
184 pub fn string(mut self, value: &str) {
186 self.write_param_name();
187 self.output.push_str(&encode(value));
188 }
189
190 pub fn number(self, value: Number) {
192 match value {
193 Number::PosInt(value) => {
194 self.string(Encoder::from(value).encode());
196 }
197 Number::NegInt(value) => {
198 self.string(Encoder::from(value).encode());
199 }
200 Number::Float(value) => self.string(Encoder::from(value).encode()),
201 }
202 }
203
204 pub fn date_time(
206 self,
207 date_time: &DateTime,
208 format: Format,
209 ) -> Result<(), DateTimeFormatError> {
210 self.string(&date_time.fmt(format)?);
211 Ok(())
212 }
213
214 pub fn start_map(
216 self,
217 flat: bool,
218 key_name: &'static str,
219 value_name: &'static str,
220 ) -> QueryMapWriter<'a> {
221 QueryMapWriter::new(self.output, self.prefix, flat, key_name, value_name)
222 }
223
224 pub fn start_list(self, flat: bool, member_override: Option<&'a str>) -> QueryListWriter<'a> {
226 QueryListWriter::new(self.output, self.prefix, flat, member_override)
227 }
228
229 fn write_param_name(&mut self) {
230 self.output.push('&');
231 self.output.push_str(&self.prefix);
232 self.output.push('=');
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use crate::QueryWriter;
239 use aws_smithy_types::date_time::Format;
240 use aws_smithy_types::{DateTime, Number};
241
242 #[test]
243 fn no_params() {
244 let mut out = String::new();
245 let writer = QueryWriter::new(&mut out, "SomeAction", "1.0");
246 writer.finish();
247 assert_eq!("Action=SomeAction&Version=1.0", out);
248 }
249
250 #[test]
251 fn query_list_writer_empty_list() {
252 let mut out = String::new();
253 let mut writer = QueryWriter::new(&mut out, "SomeAction", "1.0");
254 writer.prefix("myList").start_list(false, None).finish();
255 writer.finish();
256 assert_eq!("Action=SomeAction&Version=1.0&myList=", out);
257 }
258
259 #[test]
260 fn maps() {
261 let mut out = String::new();
262 let mut writer = QueryWriter::new(&mut out, "SomeAction", "1.0");
263
264 let mut map = writer.prefix("MapArg").start_map(false, "key", "value");
265 map.entry("bar").string("Bar");
266 map.entry("foo").string("Foo");
267 map.finish();
268
269 let mut map = writer
270 .prefix("Some.Flattened")
271 .start_map(true, "key", "value");
272 map.entry("bar").string("Bar");
273 map.entry("foo").string("Foo");
274 map.finish();
275
276 let mut map = writer.prefix("RenamedKVs").start_map(false, "K", "V");
277 map.entry("bar").string("Bar");
278 map.finish();
279
280 writer.finish();
281
282 assert_eq!(
283 "Action=SomeAction\
284 &Version=1.0\
285 &MapArg.entry.1.key=bar\
286 &MapArg.entry.1.value=Bar\
287 &MapArg.entry.2.key=foo\
288 &MapArg.entry.2.value=Foo\
289 &Some.Flattened.1.key=bar\
290 &Some.Flattened.1.value=Bar\
291 &Some.Flattened.2.key=foo\
292 &Some.Flattened.2.value=Foo\
293 &RenamedKVs.entry.1.K=bar\
294 &RenamedKVs.entry.1.V=Bar\
295 ",
296 out
297 );
298 }
299
300 #[test]
301 fn lists() {
302 let mut out = String::new();
303 let mut writer = QueryWriter::new(&mut out, "SomeAction", "1.0");
304
305 let mut list = writer.prefix("ListArg").start_list(false, None);
306 list.entry().string("foo");
307 list.entry().string("bar");
308 list.entry().string("baz");
309 list.finish();
310
311 let mut list = writer.prefix("FlattenedListArg").start_list(true, None);
312 list.entry().string("A");
313 list.entry().string("B");
314 list.finish();
315
316 let mut list = writer.prefix("ItemList").start_list(false, Some("item"));
317 list.entry().string("foo");
318 list.entry().string("bar");
319 list.finish();
320
321 writer.finish();
322
323 assert_eq!(
324 "Action=SomeAction\
325 &Version=1.0\
326 &ListArg.member.1=foo\
327 &ListArg.member.2=bar\
328 &ListArg.member.3=baz\
329 &FlattenedListArg.1=A\
330 &FlattenedListArg.2=B\
331 &ItemList.item.1=foo\
332 &ItemList.item.2=bar\
333 ",
334 out
335 );
336 }
337
338 #[test]
339 fn prefixes() {
340 let mut out = String::new();
341 let mut writer = QueryWriter::new(&mut out, "SomeAction", "1.0");
342
343 let mut first = writer.prefix("first");
344 let second = first.prefix("second");
345 second.string("second_val");
346 first.string("first_val");
347
348 writer.finish();
349
350 assert_eq!(
351 "Action=SomeAction\
352 &Version=1.0\
353 &first.second=second_val\
354 &first=first_val\
355 ",
356 out
357 );
358 }
359
360 #[test]
361 fn timestamps() {
362 let mut out = String::new();
363 let mut writer = QueryWriter::new(&mut out, "SomeAction", "1.0");
364
365 writer
366 .prefix("epoch_seconds")
367 .date_time(&DateTime::from_secs_f64(5.2), Format::EpochSeconds)
368 .unwrap();
369 writer
370 .prefix("date_time")
371 .date_time(
372 &DateTime::from_str("2021-05-24T15:34:50.123Z", Format::DateTime).unwrap(),
373 Format::DateTime,
374 )
375 .unwrap();
376 writer
377 .prefix("http_date")
378 .date_time(
379 &DateTime::from_str("Wed, 21 Oct 2015 07:28:00 GMT", Format::HttpDate).unwrap(),
380 Format::HttpDate,
381 )
382 .unwrap();
383 writer.finish();
384
385 assert_eq!(
386 "Action=SomeAction\
387 &Version=1.0\
388 &epoch_seconds=5.2\
389 &date_time=2021-05-24T15%3A34%3A50.123Z\
390 &http_date=Wed%2C%2021%20Oct%202015%2007%3A28%3A00%20GMT\
391 ",
392 out
393 );
394 }
395
396 #[test]
397 fn numbers() {
398 let mut out = String::new();
399 let mut writer = QueryWriter::new(&mut out, "SomeAction", "1.0");
400
401 writer.prefix("PosInt").number(Number::PosInt(5));
402 writer.prefix("NegInt").number(Number::NegInt(-5));
403 writer
404 .prefix("Infinity")
405 .number(Number::Float(f64::INFINITY));
406 writer
407 .prefix("NegInfinity")
408 .number(Number::Float(f64::NEG_INFINITY));
409 writer.prefix("NaN").number(Number::Float(f64::NAN));
410 writer.prefix("Floating").number(Number::Float(5.2));
411 writer.finish();
412
413 assert_eq!(
414 "Action=SomeAction\
415 &Version=1.0\
416 &PosInt=5\
417 &NegInt=-5\
418 &Infinity=Infinity\
419 &NegInfinity=-Infinity\
420 &NaN=NaN\
421 &Floating=5.2\
422 ",
423 out
424 );
425 }
426
427 #[test]
428 fn booleans() {
429 let mut out = String::new();
430 let mut writer = QueryWriter::new(&mut out, "SomeAction", "1.0");
431
432 writer.prefix("IsTrue").boolean(true);
433 writer.prefix("IsFalse").boolean(false);
434 writer.finish();
435
436 assert_eq!(
437 "Action=SomeAction\
438 &Version=1.0\
439 &IsTrue=true\
440 &IsFalse=false\
441 ",
442 out
443 );
444 }
445
446 #[test]
447 fn action_version_escaping() {
448 let mut out = String::new();
449 QueryWriter::new(&mut out, "Some Action", "1 2").finish();
450 assert_eq!("Action=Some%20Action&Version=1%202", out);
451 }
452}