Skip to main content

rustack_ses_http/
request.rs

1//! SES `awsQuery` request parameter parsing utilities.
2//!
3//! SES v1 uses `application/x-www-form-urlencoded` request bodies with
4//! dot-notation for nested parameters (e.g., `Destination.ToAddresses.member.1`).
5
6use std::collections::HashMap;
7
8use rustack_ses_model::error::{SesError, SesErrorCode};
9
10/// Parse a URL-encoded body into a list of key-value pairs.
11#[must_use]
12pub fn parse_form_params(body: &[u8]) -> Vec<(String, String)> {
13    form_urlencoded::parse(body)
14        .map(|(k, v)| (k.into_owned(), v.into_owned()))
15        .collect()
16}
17
18/// Get a required parameter value.
19///
20/// Returns an error if the parameter is not present.
21pub fn get_required_param<'a>(
22    params: &'a [(String, String)],
23    key: &str,
24) -> Result<&'a str, SesError> {
25    params
26        .iter()
27        .find(|(k, _)| k == key)
28        .map(|(_, v)| v.as_str())
29        .ok_or_else(|| {
30            SesError::with_message(
31                SesErrorCode::InvalidParameterValue,
32                format!("Missing required parameter: {key}"),
33            )
34        })
35}
36
37/// Get an optional parameter value.
38#[must_use]
39pub fn get_optional_param<'a>(params: &'a [(String, String)], key: &str) -> Option<&'a str> {
40    params
41        .iter()
42        .find(|(k, _)| k == key)
43        .map(|(_, v)| v.as_str())
44}
45
46/// Get an optional boolean parameter.
47///
48/// Parses `"true"` / `"false"` (case-insensitive). Returns `None` if
49/// the parameter is absent.
50#[must_use]
51pub fn get_optional_bool(params: &[(String, String)], key: &str) -> Option<bool> {
52    get_optional_param(params, key).map(|v| v.eq_ignore_ascii_case("true"))
53}
54
55/// Get an optional i32 parameter.
56#[must_use]
57pub fn get_optional_i32(params: &[(String, String)], key: &str) -> Option<i32> {
58    get_optional_param(params, key).and_then(|v| v.parse().ok())
59}
60
61/// Parse a `member.N` list from form parameters.
62///
63/// Given parameters like `Prefix.member.1=value1`, `Prefix.member.2=value2`,
64/// collects them into a `Vec<String>`.
65#[must_use]
66pub fn parse_member_list(params: &[(String, String)], prefix: &str) -> Vec<String> {
67    let member_prefix = format!("{prefix}.member.");
68    let mut items: Vec<(u32, String)> = Vec::new();
69    for (k, v) in params {
70        if let Some(rest) = k.strip_prefix(&member_prefix) {
71            if let Ok(idx) = rest.parse::<u32>() {
72                items.push((idx, v.clone()));
73            }
74        }
75    }
76    items.sort_by_key(|(idx, _)| *idx);
77    items.into_iter().map(|(_, v)| v).collect()
78}
79
80/// Parse message tags from form parameters.
81///
82/// Tags follow the pattern:
83/// `Tags.member.N.Name=key`, `Tags.member.N.Value=value`
84#[must_use]
85pub fn parse_tag_list(params: &[(String, String)], prefix: &str) -> Vec<(String, String)> {
86    let member_prefix = format!("{prefix}.member.");
87    let indices = collect_indices(params, &member_prefix);
88    let mut tags = Vec::new();
89    for idx in indices {
90        let name_key = format!("{member_prefix}{idx}.Name");
91        let value_key = format!("{member_prefix}{idx}.Value");
92        if let (Some(name), Some(value)) = (
93            get_optional_param(params, &name_key),
94            get_optional_param(params, &value_key),
95        ) {
96            tags.push((name.to_owned(), value.to_owned()));
97        }
98    }
99    tags
100}
101
102/// Parse an attributes map from `Prefix.entry.N.key` / `Prefix.entry.N.value`.
103pub fn parse_attributes_map(
104    params: &[(String, String)],
105    prefix: &str,
106) -> Result<HashMap<String, String>, SesError> {
107    let mut result = HashMap::new();
108    let entry_prefix = format!("{prefix}.entry.");
109    let indices = collect_indices(params, &entry_prefix);
110
111    for idx in indices {
112        let key_param = format!("{entry_prefix}{idx}.key");
113        let value_param = format!("{entry_prefix}{idx}.value");
114
115        let key = get_required_param(params, &key_param)?;
116        let value = get_optional_param(params, &value_param).unwrap_or("");
117        result.insert(key.to_owned(), value.to_owned());
118    }
119    Ok(result)
120}
121
122/// Parse query parameters from a URI query string.
123#[must_use]
124pub fn parse_query_params(query: Option<&str>) -> HashMap<String, String> {
125    let mut params = HashMap::new();
126    if let Some(q) = query {
127        for (k, v) in form_urlencoded::parse(q.as_bytes()) {
128            params.insert(k.into_owned(), v.into_owned());
129        }
130    }
131    params
132}
133
134/// Collect unique numeric indices from params matching a prefix pattern.
135fn collect_indices(params: &[(String, String)], prefix: &str) -> Vec<u32> {
136    let mut indices: Vec<u32> = Vec::new();
137    for (k, _) in params {
138        if let Some(rest) = k.strip_prefix(prefix) {
139            if let Some(idx_str) = rest.split('.').next() {
140                if let Ok(idx) = idx_str.parse::<u32>() {
141                    if !indices.contains(&idx) {
142                        indices.push(idx);
143                    }
144                }
145            }
146        }
147    }
148    indices
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_should_parse_form_params() {
157        let body = b"Action=SendEmail&Source=sender%40example.com&Version=2010-12-01";
158        let params = parse_form_params(body);
159        assert_eq!(params.len(), 3);
160        assert_eq!(params[0], ("Action".to_owned(), "SendEmail".to_owned()));
161        assert_eq!(
162            params[1],
163            ("Source".to_owned(), "sender@example.com".to_owned())
164        );
165    }
166
167    #[test]
168    fn test_should_get_required_param() {
169        let params = vec![("Source".to_owned(), "test@example.com".to_owned())];
170        assert_eq!(
171            get_required_param(&params, "Source").unwrap(),
172            "test@example.com"
173        );
174    }
175
176    #[test]
177    fn test_should_error_on_missing_required_param() {
178        let params: Vec<(String, String)> = vec![];
179        let err = get_required_param(&params, "Source").unwrap_err();
180        assert!(err.message.contains("Source"));
181    }
182
183    #[test]
184    fn test_should_parse_member_list() {
185        let params = vec![
186            (
187                "Destination.ToAddresses.member.1".to_owned(),
188                "a@example.com".to_owned(),
189            ),
190            (
191                "Destination.ToAddresses.member.2".to_owned(),
192                "b@example.com".to_owned(),
193            ),
194        ];
195        let list = parse_member_list(&params, "Destination.ToAddresses");
196        assert_eq!(list, vec!["a@example.com", "b@example.com"]);
197    }
198
199    #[test]
200    fn test_should_parse_tag_list() {
201        let params = vec![
202            ("Tags.member.1.Name".to_owned(), "campaign".to_owned()),
203            ("Tags.member.1.Value".to_owned(), "welcome".to_owned()),
204            ("Tags.member.2.Name".to_owned(), "env".to_owned()),
205            ("Tags.member.2.Value".to_owned(), "test".to_owned()),
206        ];
207        let tags = parse_tag_list(&params, "Tags");
208        assert_eq!(tags.len(), 2);
209        assert_eq!(tags[0], ("campaign".to_owned(), "welcome".to_owned()));
210        assert_eq!(tags[1], ("env".to_owned(), "test".to_owned()));
211    }
212
213    #[test]
214    fn test_should_parse_query_params() {
215        let params = parse_query_params(Some("id=abc&email=test@example.com"));
216        assert_eq!(params.get("id").unwrap(), "abc");
217        assert_eq!(params.get("email").unwrap(), "test@example.com");
218    }
219
220    #[test]
221    fn test_should_parse_empty_query_params() {
222        let params = parse_query_params(None);
223        assert!(params.is_empty());
224    }
225}