1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

//! Utilities for writing Smithy values into a query string.
//!
//! Formatting values into the query string as specified in
//! [httpQuery](https://smithy.io/2.0/spec/http-bindings.html#httpquery-trait)

use crate::urlencode::BASE_SET;
use aws_smithy_types::date_time::{DateTimeFormatError, Format};
use aws_smithy_types::DateTime;
use percent_encoding::utf8_percent_encode;

/// Format a given string as a query string.
pub fn fmt_string<T: AsRef<str>>(t: T) -> String {
    utf8_percent_encode(t.as_ref(), BASE_SET).to_string()
}

/// Format a given [`DateTime`] as a query string.
pub fn fmt_timestamp(t: &DateTime, format: Format) -> Result<String, DateTimeFormatError> {
    Ok(fmt_string(t.fmt(format)?))
}

/// Simple abstraction to enable appending params to a string as query params.
///
/// ```rust
/// use aws_smithy_http::query::Writer;
/// let mut s = String::from("www.example.com");
/// let mut q = Writer::new(&mut s);
/// q.push_kv("key", "value");
/// q.push_v("another_value");
/// assert_eq!(s, "www.example.com?key=value&another_value");
/// ```
#[allow(missing_debug_implementations)]
pub struct Writer<'a> {
    out: &'a mut String,
    prefix: char,
}

impl<'a> Writer<'a> {
    /// Create a new query string writer.
    pub fn new(out: &'a mut String) -> Self {
        Writer { out, prefix: '?' }
    }

    /// Add a new key and value pair to this writer.
    pub fn push_kv(&mut self, k: &str, v: &str) {
        self.out.push(self.prefix);
        self.out.push_str(k);
        self.out.push('=');
        self.out.push_str(v);
        self.prefix = '&';
    }

    /// Add a new value (which is its own key) to this writer.
    pub fn push_v(&mut self, v: &str) {
        self.out.push(self.prefix);
        self.out.push_str(v);
        self.prefix = '&';
    }
}

#[cfg(test)]
mod test {
    use crate::query::{fmt_string, Writer};
    use http::Uri;
    use proptest::proptest;

    #[test]
    fn url_encode() {
        assert_eq!(fmt_string("y̆").as_str(), "y%CC%86");
        assert_eq!(fmt_string(" ").as_str(), "%20");
        assert_eq!(fmt_string("foo/baz%20").as_str(), "foo%2Fbaz%2520");
        assert_eq!(fmt_string("&=").as_str(), "%26%3D");
        assert_eq!(fmt_string("🐱").as_str(), "%F0%9F%90%B1");
        // `:` needs to be encoded, but only for AWS services
        assert_eq!(fmt_string("a:b"), "a%3Ab")
    }

    #[test]
    fn writer_sets_prefix_properly() {
        let mut out = String::new();
        let mut writer = Writer::new(&mut out);
        writer.push_v("a");
        writer.push_kv("b", "c");
        assert_eq!(out, "?a&b=c");
    }

    proptest! {
        #[test]
        fn test_encode_request(s: String) {
            let _: Uri = format!("http://host.example.com/?{}", fmt_string(s)).parse().expect("all strings should be encoded properly");
        }
    }
}