aws_smithy_query/
lib.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6/* Automatically managed default lints */
7#![cfg_attr(docsrs, feature(doc_auto_cfg))]
8/* End of automatically managed default lints */
9#![allow(clippy::derive_partial_eq_without_eq)]
10#![warn(
11    // missing_docs,
12    rustdoc::missing_crate_level_docs,
13    unreachable_pub,
14    rust_2018_idioms
15)]
16
17//! Abstractions for the Smithy AWS Query protocol
18
19use 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        // Calling this drops self
45    }
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        // The `Write` implementation for `String` is infallible,
88        // see https://doc.rust-lang.org/src/alloc/string.rs.html#2815
89        .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        // Calling this drops self
101    }
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        // https://github.com/awslabs/smithy/commit/715b1d94ab14764ad43496b016b0c2e85bcf1d1f
149        // If the list was empty, just serialize the parameter name
150        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    /// Starts a new prefix.
168    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    /// Writes the boolean `value`.
176    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    /// Writes a string `value`.
185    pub fn string(mut self, value: &str) {
186        self.write_param_name();
187        self.output.push_str(&encode(value));
188    }
189
190    /// Writes a number `value`.
191    pub fn number(self, value: Number) {
192        match value {
193            Number::PosInt(value) => {
194                // itoa::Buffer is a fixed-size stack allocation, so this is cheap
195                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    /// Writes a date-time `value` with the given `format`.
205    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    /// Starts a map.
215    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    /// Starts a list.
225    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}