Skip to main content

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_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
19pub 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        // Calling this drops self
48    }
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        // The `Write` implementation for `String` is infallible,
91        // see https://doc.rust-lang.org/src/alloc/string.rs.html#2815
92        .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        // Calling this drops self
104    }
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        // https://github.com/awslabs/smithy/commit/715b1d94ab14764ad43496b016b0c2e85bcf1d1f
152        // If the list was empty, just serialize the parameter name
153        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    /// Starts a new prefix.
171    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    /// Writes the boolean `value`.
179    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    /// Writes a string `value`.
188    pub fn string(mut self, value: &str) {
189        self.write_param_name();
190        self.output.push_str(&encode(value));
191    }
192
193    /// Writes a number `value`.
194    pub fn number(self, value: Number) {
195        match value {
196            Number::PosInt(value) => {
197                // itoa::Buffer is a fixed-size stack allocation, so this is cheap
198                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    /// Writes a date-time `value` with the given `format`.
208    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    /// Starts a map.
218    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    /// Starts a list.
228    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}