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}