Skip to main content

timvw_rust_runtime/
request_options.rs

1use http::header::{HeaderName, HeaderValue};
2use http::HeaderMap;
3use serde::Serialize;
4
5use crate::Error;
6
7/// Extra headers and query parameters for one call, applied after everything the generated code sets.
8///
9/// A generated operation takes them with `.options(&options)`; [`RequestBuilder::options`](crate::RequestBuilder::options)
10/// applies them to a request of your own. One value can be reused for many calls.
11///
12/// - A header replaces every value the request has under the same name: generated headers, authentication and
13///   `Content-Type` included. A name set twice on the options keeps both values (see [`RequestOptions::headers`]).
14/// - Query parameters are appended after the generated ones; a name the request already has is sent twice.
15///
16/// ```
17/// use timvw_rust_runtime::RequestOptions;
18///
19/// let options = RequestOptions::new()
20///     .header("x-request-source", "batch")?
21///     .query(&[("trace", "1")])?;
22/// assert_eq!(options.header_map()["x-request-source"], "batch");
23/// assert_eq!(options.query_pairs(), [("trace".to_owned(), "1".to_owned())]);
24/// # Ok::<(), timvw_rust_runtime::Error>(())
25/// ```
26#[derive(Clone, Debug, Default)]
27pub struct RequestOptions {
28    headers: HeaderMap,
29    query: Vec<(String, String)>,
30}
31
32impl RequestOptions {
33    /// No extra headers or query parameters.
34    pub fn new() -> Self {
35        Self::default()
36    }
37
38    /// Sets a header, replacing a value set before under the same name. Fails, with an error whose
39    /// [`is_builder()`](Error::is_builder) is true, on an invalid name or value.
40    pub fn header<K, V>(mut self, name: K, value: V) -> Result<Self, Error>
41    where
42        HeaderName: TryFrom<K>,
43        <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
44        HeaderValue: TryFrom<V>,
45        <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
46    {
47        let name = HeaderName::try_from(name).map_err(|e| Error::builder(e.into()))?;
48        let value = HeaderValue::try_from(value).map_err(|e| Error::builder(e.into()))?;
49        self.headers.insert(name, value);
50        Ok(self)
51    }
52
53    /// Adds every header of a map, replacing the values set before under its names. A name with several values in
54    /// `headers` keeps them all, and so does the request they are applied to.
55    pub fn headers(mut self, headers: HeaderMap) -> Self {
56        self.headers.extend(headers);
57        self
58    }
59
60    /// Appends query parameters, serialized as `application/x-www-form-urlencoded`: a sequence of pairs, a map or a
61    /// struct, as [`RequestBuilder::query`](crate::RequestBuilder::query) takes them. Fails, with an error whose
62    /// [`is_builder()`](Error::is_builder) is true, on a value that is not a flat list of pairs.
63    pub fn query<T: Serialize + ?Sized>(mut self, query: &T) -> Result<Self, Error> {
64        let encoded = serde_urlencoded::to_string(query).map_err(Error::builder)?;
65        self.query
66            .extend(url::form_urlencoded::parse(encoded.as_bytes()).into_owned());
67        Ok(self)
68    }
69
70    /// The headers.
71    pub fn header_map(&self) -> &HeaderMap {
72        &self.headers
73    }
74
75    /// The query parameters, decoded, in order.
76    pub fn query_pairs(&self) -> &[(String, String)] {
77        &self.query
78    }
79
80    /// Whether there are no headers and no query parameters.
81    pub fn is_empty(&self) -> bool {
82        self.headers.is_empty() && self.query.is_empty()
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use http::header::{AUTHORIZATION, CONTENT_TYPE};
90
91    #[test]
92    fn header_replaces_and_headers_keeps_every_value() {
93        let mut extra = HeaderMap::new();
94        extra.append("x-tag", HeaderValue::from_static("a"));
95        extra.append("x-tag", HeaderValue::from_static("b"));
96        let options = RequestOptions::new()
97            .header("x-one", "1")
98            .unwrap()
99            .header("x-one", "2")
100            .unwrap()
101            .header(CONTENT_TYPE, HeaderValue::from_static("text/plain"))
102            .unwrap()
103            .headers(extra);
104        let headers = options.header_map();
105        assert_eq!(headers.get_all("x-one").iter().collect::<Vec<_>>(), ["2"]);
106        assert_eq!(
107            headers.get_all("x-tag").iter().collect::<Vec<_>>(),
108            ["a", "b"]
109        );
110        assert_eq!(headers[CONTENT_TYPE], "text/plain");
111    }
112
113    #[test]
114    fn query_keeps_order_and_decodes() {
115        let options = RequestOptions::new()
116            .query(&[("a", "x y"), ("b", "&")])
117            .unwrap()
118            .query(&[("a", 2)])
119            .unwrap();
120        let pairs: Vec<(&str, &str)> = options
121            .query_pairs()
122            .iter()
123            .map(|(k, v)| (k.as_str(), v.as_str()))
124            .collect();
125        assert_eq!(pairs, [("a", "x y"), ("b", "&"), ("a", "2")]);
126    }
127
128    #[test]
129    fn invalid_input_is_a_builder_error() {
130        assert!(RequestOptions::new()
131            .header("x bad", "v")
132            .unwrap_err()
133            .is_builder());
134        assert!(RequestOptions::new()
135            .header("x-bad", "line\nbreak")
136            .unwrap_err()
137            .is_builder());
138        // a nested value is not a pair
139        assert!(RequestOptions::new()
140            .query(&[("a", [1, 2])])
141            .unwrap_err()
142            .is_builder());
143    }
144
145    #[test]
146    fn empty() {
147        assert!(RequestOptions::new().is_empty());
148        assert!(RequestOptions::default().is_empty());
149        assert!(!RequestOptions::new()
150            .header(AUTHORIZATION, "Bearer x")
151            .unwrap()
152            .is_empty());
153        assert!(!RequestOptions::new()
154            .query(&[("a", "")])
155            .unwrap()
156            .is_empty());
157    }
158}