Skip to main content

timvw_rust_runtime/
request_options.rs

1use http::header::{HeaderName, HeaderValue};
2use http::HeaderMap;
3use serde::Serialize;
4
5use url::Url;
6
7use crate::provider::parse_base;
8use crate::Error;
9
10/// Extra headers and query parameters, and another base URL, for one call, applied after everything the generated
11/// code sets.
12///
13/// A generated operation takes them with `.options(&options)` (or its last argument without builders);
14/// [`RequestBuilder::options`](crate::RequestBuilder::options) applies them to a request of your own. One value can be
15/// reused for many calls.
16///
17/// - A header replaces every value the request has under the same name: generated headers, authentication and
18///   `Content-Type` included. A name set twice on the options keeps both values (see [`RequestOptions::headers`]).
19/// - Query parameters are appended after the generated ones; a name the request already has is sent twice.
20/// - A [base URL](RequestOptions::base_url) sends the call to another host or path prefix: it replaces the
21///   `Configuration.base_path` the request was built on, and the operation's own path and query are kept.
22///
23/// They win over a [`ProviderLayer`](crate::ProviderLayer): it does not replace a header or a query parameter the
24/// options set, and it does not move a call with a base URL of its own to the provider (see its docs).
25///
26/// ```
27/// use timvw_rust_runtime::RequestOptions;
28///
29/// let options = RequestOptions::new()
30///     .header("x-request-source", "batch")?
31///     .query(&[("trace", "1")])?;
32/// assert_eq!(options.header_map()["x-request-source"], "batch");
33/// assert_eq!(options.query_pairs(), [("trace".to_owned(), "1".to_owned())]);
34/// # Ok::<(), timvw_rust_runtime::Error>(())
35/// ```
36#[derive(Clone, Debug, Default)]
37pub struct RequestOptions {
38    headers: HeaderMap,
39    query: Vec<(String, String)>,
40    base: Option<Url>,
41}
42
43impl RequestOptions {
44    /// No extra headers or query parameters.
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    /// Sets a header, replacing a value set before under the same name. Fails, with an error whose
50    /// [`is_builder()`](Error::is_builder) is true, on an invalid name or value.
51    pub fn header<K, V>(mut self, name: K, value: V) -> Result<Self, Error>
52    where
53        HeaderName: TryFrom<K>,
54        <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
55        HeaderValue: TryFrom<V>,
56        <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
57    {
58        let name = HeaderName::try_from(name).map_err(|e| Error::builder(e.into()))?;
59        let value = HeaderValue::try_from(value).map_err(|e| Error::builder(e.into()))?;
60        self.headers.insert(name, value);
61        Ok(self)
62    }
63
64    /// Adds every header of a map, replacing the values set before under its names. A name with several values in
65    /// `headers` keeps them all, and so does the request they are applied to.
66    pub fn headers(mut self, headers: HeaderMap) -> Self {
67        self.headers.extend(headers);
68        self
69    }
70
71    /// Appends query parameters, serialized as `application/x-www-form-urlencoded`: a sequence of pairs, a map or a
72    /// struct, as [`RequestBuilder::query`](crate::RequestBuilder::query) takes them. Fails, with an error whose
73    /// [`is_builder()`](Error::is_builder) is true, on a value that is not a flat list of pairs.
74    pub fn query<T: Serialize + ?Sized>(mut self, query: &T) -> Result<Self, Error> {
75        let encoded = serde_urlencoded::to_string(query).map_err(Error::builder)?;
76        self.query
77            .extend(url::form_urlencoded::parse(encoded.as_bytes()).into_owned());
78        Ok(self)
79    }
80
81    /// Sends the call to another base URL, such as `https://eu.api.example.com/v1`: the scheme, host, port and path
82    /// prefix of the request's base (a generated client's `Configuration.base_path`, declared with
83    /// [`RequestBuilder::base`](crate::RequestBuilder::base)) are replaced, and the operation's path and the query are
84    /// kept. On a request without a declared base, the whole path is kept and put after this base's path. Calling it
85    /// again replaces the base. Fails, with an error whose [`is_builder()`](Error::is_builder) is true, on a URL that is
86    /// not absolute or that has a query or fragment.
87    ///
88    /// ```
89    /// use timvw_rust_runtime::{Client, Method, RequestOptions};
90    ///
91    /// let options = RequestOptions::new().base_url("https://eu.api.example.com/v2")?;
92    /// let request = Client::default()
93    ///     .request(Method::GET, "https://api.example.com/v1/models?limit=2")
94    ///     .base("https://api.example.com/v1")
95    ///     .options(&options)
96    ///     .build()?;
97    /// assert_eq!(request.uri(), "https://eu.api.example.com/v2/models?limit=2");
98    /// # Ok::<(), timvw_rust_runtime::Error>(())
99    /// ```
100    pub fn base_url(mut self, base_url: &str) -> Result<Self, Error> {
101        self.base = Some(parse_base(base_url)?);
102        Ok(self)
103    }
104
105    /// The base URL set with [`RequestOptions::base_url`], if any.
106    pub fn base(&self) -> Option<&str> {
107        self.base.as_ref().map(Url::as_str)
108    }
109
110    pub(crate) fn base_url_parsed(&self) -> Option<&Url> {
111        self.base.as_ref()
112    }
113
114    /// The headers.
115    pub fn header_map(&self) -> &HeaderMap {
116        &self.headers
117    }
118
119    /// The query parameters, decoded, in order.
120    pub fn query_pairs(&self) -> &[(String, String)] {
121        &self.query
122    }
123
124    /// Whether there are no headers, no query parameters and no base URL.
125    pub fn is_empty(&self) -> bool {
126        self.headers.is_empty() && self.query.is_empty() && self.base.is_none()
127    }
128}
129
130/// What [`RequestOptions`] set on a request, kept as a request extension by
131/// [`RequestBuilder::options`](crate::RequestBuilder::options): the [`ProviderLayer`](crate::ProviderLayer) leaves
132/// these alone.
133#[derive(Clone, Debug, Default)]
134pub(crate) struct Applied {
135    /// The names of the headers the options set.
136    pub(crate) headers: Vec<HeaderName>,
137    /// The names of the query parameters the options added.
138    pub(crate) query: Vec<String>,
139    /// The options moved the request to a base URL of their own.
140    pub(crate) base: bool,
141}
142
143impl Applied {
144    pub(crate) fn add(&mut self, options: &RequestOptions) {
145        for name in options.headers.keys() {
146            if !self.headers.contains(name) {
147                self.headers.push(name.clone());
148            }
149        }
150        for (name, _) in &options.query {
151            if !self.query.contains(name) {
152                self.query.push(name.clone());
153            }
154        }
155        self.base |= options.base.is_some();
156    }
157
158    pub(crate) fn has_header(&self, name: &HeaderName) -> bool {
159        self.headers.contains(name)
160    }
161
162    pub(crate) fn has_query(&self, name: &str) -> bool {
163        self.query.iter().any(|q| q == name)
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use http::header::{AUTHORIZATION, CONTENT_TYPE};
171
172    #[test]
173    fn header_replaces_and_headers_keeps_every_value() {
174        let mut extra = HeaderMap::new();
175        extra.append("x-tag", HeaderValue::from_static("a"));
176        extra.append("x-tag", HeaderValue::from_static("b"));
177        let options = RequestOptions::new()
178            .header("x-one", "1")
179            .unwrap()
180            .header("x-one", "2")
181            .unwrap()
182            .header(CONTENT_TYPE, HeaderValue::from_static("text/plain"))
183            .unwrap()
184            .headers(extra);
185        let headers = options.header_map();
186        assert_eq!(headers.get_all("x-one").iter().collect::<Vec<_>>(), ["2"]);
187        assert_eq!(
188            headers.get_all("x-tag").iter().collect::<Vec<_>>(),
189            ["a", "b"]
190        );
191        assert_eq!(headers[CONTENT_TYPE], "text/plain");
192    }
193
194    #[test]
195    fn query_keeps_order_and_decodes() {
196        let options = RequestOptions::new()
197            .query(&[("a", "x y"), ("b", "&")])
198            .unwrap()
199            .query(&[("a", 2)])
200            .unwrap();
201        let pairs: Vec<(&str, &str)> = options
202            .query_pairs()
203            .iter()
204            .map(|(k, v)| (k.as_str(), v.as_str()))
205            .collect();
206        assert_eq!(pairs, [("a", "x y"), ("b", "&"), ("a", "2")]);
207    }
208
209    #[test]
210    fn invalid_input_is_a_builder_error() {
211        assert!(RequestOptions::new()
212            .header("x bad", "v")
213            .unwrap_err()
214            .is_builder());
215        assert!(RequestOptions::new()
216            .header("x-bad", "line\nbreak")
217            .unwrap_err()
218            .is_builder());
219        // a nested value is not a pair
220        assert!(RequestOptions::new()
221            .query(&[("a", [1, 2])])
222            .unwrap_err()
223            .is_builder());
224    }
225
226    #[test]
227    fn empty() {
228        assert!(RequestOptions::new().is_empty());
229        assert!(RequestOptions::default().is_empty());
230        assert!(!RequestOptions::new()
231            .header(AUTHORIZATION, "Bearer x")
232            .unwrap()
233            .is_empty());
234        assert!(!RequestOptions::new()
235            .query(&[("a", "")])
236            .unwrap()
237            .is_empty());
238        assert!(!RequestOptions::new()
239            .base_url("https://example.com")
240            .unwrap()
241            .is_empty());
242    }
243
244    #[test]
245    fn base_url_is_a_base() {
246        let options = RequestOptions::new()
247            .base_url("https://a.example/v1")
248            .unwrap()
249            .base_url("https://b.example/v2/")
250            .unwrap();
251        assert_eq!(options.base(), Some("https://b.example/v2/"));
252        for base in [
253            "not a url",
254            "/relative",
255            "https://p.example/v1?a=1",
256            "https://p.example/v1#top",
257        ] {
258            assert!(
259                RequestOptions::new()
260                    .base_url(base)
261                    .unwrap_err()
262                    .is_builder(),
263                "{base}"
264            );
265        }
266    }
267}