Skip to content

Commit 9966ee3

Browse files
softpropsdavidbarsky
authored andcommitted
impl http multi value query str parameters (#59)
* impl multi_value_headers * remove redundant closure * impl multi_value_headers in response * fmt * unprefix * missing import in test * rustfmt * impl multi_value_query_string_parameters * improve test assertion msg * rustfmt
1 parent e881b72 commit 9966ee3

File tree

3 files changed

+89
-18
lines changed

3 files changed

+89
-18
lines changed

lambda-http/src/ext.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,13 @@ pub enum PayloadError {
7878
pub trait RequestExt {
7979
/// Return pre-parsed http query string parameters, parameters
8080
/// provided after the `?` portion of a url,
81-
/// associated with the API gateway request. No query parameters
81+
/// associated with the API gateway request.
82+
///
83+
/// The yielded value represents both single and multi-valued
84+
/// parameters alike. When multiple query string parameters with the same
85+
/// name are expected, `query_string_parameters().get_all("many")` to retrieve them all.
86+
///
87+
/// No query parameters
8288
/// will yield an empty `StrMap`.
8389
fn query_string_parameters(&self) -> StrMap;
8490
/// Return pre-extracted path parameters, parameter provided in url placeholders
@@ -163,7 +169,7 @@ mod tests {
163169
let mut headers = HeaderMap::new();
164170
headers.insert("Host", "www.rust-lang.org".parse().unwrap());
165171
let mut query = HashMap::new();
166-
query.insert("foo".to_owned(), "bar".to_owned());
172+
query.insert("foo".to_owned(), vec!["bar".to_owned()]);
167173
let gwr: GatewayRequest<'_> = GatewayRequest {
168174
path: "/foo".into(),
169175
headers,

lambda-http/src/request.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ pub(crate) struct GatewayRequest<'a> {
3535
pub(crate) multi_value_headers: HeaderMap<HeaderValue>,
3636
#[serde(deserialize_with = "nullable_default")]
3737
pub(crate) query_string_parameters: StrMap,
38+
#[serde(default, deserialize_with = "nullable_default")]
39+
pub(crate) multi_value_query_string_parameters: StrMap,
3840
#[serde(deserialize_with = "nullable_default")]
3941
pub(crate) path_parameters: StrMap,
4042
#[serde(deserialize_with = "nullable_default")]
@@ -200,6 +202,7 @@ impl<'a> From<GatewayRequest<'a>> for HttpRequest<Body> {
200202
headers,
201203
mut multi_value_headers,
202204
query_string_parameters,
205+
multi_value_query_string_parameters,
203206
path_parameters,
204207
stage_variables,
205208
body,
@@ -220,8 +223,16 @@ impl<'a> From<GatewayRequest<'a>> for HttpRequest<Body> {
220223
path
221224
)
222225
});
223-
224-
builder.extension(QueryStringParameters(query_string_parameters));
226+
// multi valued query string parameters are always a super
227+
// set of singly valued query string parameters,
228+
// when present, multi-valued query string parameters are preferred
229+
builder.extension(QueryStringParameters(
230+
if multi_value_query_string_parameters.is_empty() {
231+
query_string_parameters
232+
} else {
233+
multi_value_query_string_parameters
234+
},
235+
));
225236
builder.extension(PathParameters(path_parameters));
226237
builder.extension(StageVariables(stage_variables));
227238
builder.extension(request_context);
@@ -262,6 +273,7 @@ impl<'a> From<GatewayRequest<'a>> for HttpRequest<Body> {
262273
#[cfg(test)]
263274
mod tests {
264275
use super::*;
276+
use crate::RequestExt;
265277
use serde_json;
266278
use std::collections::HashMap;
267279

@@ -296,7 +308,20 @@ mod tests {
296308
// https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
297309
let input = include_str!("../tests/data/apigw_multi_value_proxy_request.json");
298310
let result = serde_json::from_str::<GatewayRequest<'_>>(&input);
299-
assert!(result.is_ok(), format!("event was not parsed as expected {:?}", result));
311+
assert!(
312+
result.is_ok(),
313+
format!("event is was not parsed as expected {:?}", result)
314+
);
315+
let apigw = result.unwrap();
316+
assert!(!apigw.query_string_parameters.is_empty());
317+
assert!(!apigw.multi_value_query_string_parameters.is_empty());
318+
let actual = HttpRequest::from(apigw);
319+
320+
// test RequestExt#query_string_parameters does the right thing
321+
assert_eq!(
322+
actual.query_string_parameters().get_all("multivalueName"),
323+
Some(vec!["you", "me"])
324+
);
300325
}
301326

302327
#[test]

lambda-http/src/strmap.rs

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,27 @@ use serde::{
99
Deserialize, Deserializer,
1010
};
1111

12-
/// A read-only view into a map of string data
12+
/// A read-only view into a map of string data which may contain multiple values
13+
///
14+
/// Internally data is always represented as many valued
1315
#[derive(Default, Debug, PartialEq)]
14-
pub struct StrMap(pub(crate) Arc<HashMap<String, String>>);
16+
pub struct StrMap(pub(crate) Arc<HashMap<String, Vec<String>>>);
1517

1618
impl StrMap {
17-
/// Return a named value where available
19+
/// Return a named value where available.
20+
/// If there is more than one value associated with this name,
21+
/// the first one will be returned
1822
pub fn get(&self, key: &str) -> Option<&str> {
19-
self.0.get(key).map(|value| value.as_ref())
23+
self.0
24+
.get(key)
25+
.and_then(|values| values.first().map(|owned| owned.as_str()))
26+
}
27+
28+
/// Return all values associated with name where available
29+
pub fn get_all(&self, key: &str) -> Option<Vec<&str>> {
30+
self.0
31+
.get(key)
32+
.map(|values| values.iter().map(|owned| owned.as_str()).collect::<Vec<_>>())
2033
}
2134

2235
/// Return true if the underlying map is empty
@@ -39,16 +52,17 @@ impl Clone for StrMap {
3952
StrMap(self.0.clone())
4053
}
4154
}
42-
impl From<HashMap<String, String>> for StrMap {
43-
fn from(inner: HashMap<String, String>) -> Self {
55+
56+
impl From<HashMap<String, Vec<String>>> for StrMap {
57+
fn from(inner: HashMap<String, Vec<String>>) -> Self {
4458
StrMap(Arc::new(inner))
4559
}
4660
}
4761

4862
/// A read only reference to `StrMap` key and value slice pairings
4963
pub struct StrMapIter<'a> {
5064
data: &'a StrMap,
51-
keys: Keys<'a, String, String>,
65+
keys: Keys<'a, String, Vec<String>>,
5266
}
5367

5468
impl<'a> Iterator for StrMapIter<'a> {
@@ -60,6 +74,15 @@ impl<'a> Iterator for StrMapIter<'a> {
6074
}
6175
}
6276

77+
/// internal type used when deserializing StrMaps from
78+
/// potentially one or many valued maps
79+
#[derive(serde_derive::Deserialize)]
80+
#[serde(untagged)]
81+
enum OneOrMany {
82+
One(String),
83+
Many(Vec<String>),
84+
}
85+
6386
impl<'de> Deserialize<'de> for StrMap {
6487
fn deserialize<D>(deserializer: D) -> Result<StrMap, D::Error>
6588
where
@@ -78,9 +101,17 @@ impl<'de> Deserialize<'de> for StrMap {
78101
where
79102
A: MapAccess<'de>,
80103
{
81-
let mut inner = HashMap::new();
82-
while let Some((key, value)) = map.next_entry()? {
83-
inner.insert(key, value);
104+
let mut inner = map.size_hint().map(HashMap::with_capacity).unwrap_or_else(HashMap::new);
105+
// values may either be String or Vec<String>
106+
// to handle both single and multi value data
107+
while let Some((key, value)) = map.next_entry::<_, OneOrMany>()? {
108+
inner.insert(
109+
key,
110+
match value {
111+
OneOrMany::One(one) => vec![one],
112+
OneOrMany::Many(many) => many,
113+
},
114+
);
84115
}
85116
Ok(StrMap(Arc::new(inner)))
86117
}
@@ -103,17 +134,26 @@ mod tests {
103134
#[test]
104135
fn str_map_get() {
105136
let mut data = HashMap::new();
106-
data.insert("foo".into(), "bar".into());
137+
data.insert("foo".into(), vec!["bar".into()]);
107138
let strmap = StrMap(data.into());
108139
assert_eq!(strmap.get("foo"), Some("bar"));
109140
assert_eq!(strmap.get("bar"), None);
110141
}
111142

143+
#[test]
144+
fn str_map_get_all() {
145+
let mut data = HashMap::new();
146+
data.insert("foo".into(), vec!["bar".into(), "baz".into()]);
147+
let strmap = StrMap(data.into());
148+
assert_eq!(strmap.get_all("foo"), Some(vec!["bar", "baz"]));
149+
assert_eq!(strmap.get_all("bar"), None);
150+
}
151+
112152
#[test]
113153
fn str_map_iter() {
114154
let mut data = HashMap::new();
115-
data.insert("foo".into(), "bar".into());
116-
data.insert("baz".into(), "boom".into());
155+
data.insert("foo".into(), vec!["bar".into()]);
156+
data.insert("baz".into(), vec!["boom".into()]);
117157
let strmap = StrMap(data.into());
118158
let mut values = strmap.iter().map(|(_, v)| v).collect::<Vec<_>>();
119159
values.sort();

0 commit comments

Comments
 (0)