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