timvw_rust_runtime/
request.rs1use std::fmt;
2
3use base64::Engine;
4use http::header::{HeaderName, HeaderValue, AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE};
5use http::HeaderMap;
6use serde::Serialize;
7use url::Url;
8
9use crate::multipart::Form;
10use crate::{Body, Client, Error, Method, Response};
11
12pub struct RequestBuilder {
16 client: Client,
17 request: Result<Parts, Error>,
18}
19
20struct Parts {
21 method: Method,
22 url: Url,
23 headers: HeaderMap,
24 body: Option<Body>,
25}
26
27impl RequestBuilder {
28 pub(crate) fn new(client: Client, method: Method, url: &str) -> Self {
29 let request = Url::parse(url)
30 .map(|url| Parts {
31 method,
32 url,
33 headers: HeaderMap::new(),
34 body: None,
35 })
36 .map_err(Error::builder);
37 Self { client, request }
38 }
39
40 fn with(mut self, f: impl FnOnce(&mut Parts) -> Result<(), Error>) -> Self {
41 if let Ok(parts) = &mut self.request {
42 if let Err(error) = f(parts) {
43 self.request = Err(error);
44 }
45 }
46 self
47 }
48
49 pub fn header<K, V>(self, name: K, value: V) -> Self
51 where
52 HeaderName: TryFrom<K>,
53 <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
54 HeaderValue: TryFrom<V>,
55 <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
56 {
57 self.with(|parts| {
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 parts.headers.append(name, value);
61 Ok(())
62 })
63 }
64
65 pub fn headers(self, headers: HeaderMap) -> Self {
67 self.with(|parts| {
68 parts.headers.extend(headers);
69 Ok(())
70 })
71 }
72
73 pub fn query<T: Serialize + ?Sized>(self, query: &T) -> Self {
76 self.with(|parts| {
77 {
78 let mut pairs = parts.url.query_pairs_mut();
79 query
80 .serialize(serde_urlencoded::Serializer::new(&mut pairs))
81 .map_err(Error::builder)?;
82 }
83 if parts.url.query() == Some("") {
84 parts.url.set_query(None);
85 }
86 Ok(())
87 })
88 }
89
90 pub fn bearer_auth<T: fmt::Display>(self, token: T) -> Self {
92 self.sensitive_header(AUTHORIZATION, format!("Bearer {token}"))
93 }
94
95 pub fn basic_auth<U: fmt::Display, P: fmt::Display>(
97 self,
98 username: U,
99 password: Option<P>,
100 ) -> Self {
101 let credentials = match password {
102 Some(password) => format!("{username}:{password}"),
103 None => format!("{username}:"),
104 };
105 let encoded = base64::engine::general_purpose::STANDARD.encode(credentials);
106 self.sensitive_header(AUTHORIZATION, format!("Basic {encoded}"))
107 }
108
109 fn sensitive_header(self, name: HeaderName, value: String) -> Self {
110 self.with(|parts| {
111 let mut value = HeaderValue::try_from(value).map_err(Error::builder)?;
112 value.set_sensitive(true);
113 parts.headers.insert(name, value);
114 Ok(())
115 })
116 }
117
118 pub fn body<T: Into<Body>>(self, body: T) -> Self {
120 self.with(|parts| {
121 parts.body = Some(body.into());
122 Ok(())
123 })
124 }
125
126 pub fn json<T: Serialize + ?Sized>(self, json: &T) -> Self {
128 self.with(|parts| {
129 let bytes = serde_json::to_vec(json).map_err(Error::builder)?;
130 parts
131 .headers
132 .entry(CONTENT_TYPE)
133 .or_insert(HeaderValue::from_static("application/json"));
134 parts.body = Some(bytes.into());
135 Ok(())
136 })
137 }
138
139 pub fn form<T: Serialize + ?Sized>(self, form: &T) -> Self {
141 self.with(|parts| {
142 let text = serde_urlencoded::to_string(form).map_err(Error::builder)?;
143 parts.headers.insert(
144 CONTENT_TYPE,
145 HeaderValue::from_static("application/x-www-form-urlencoded"),
146 );
147 parts.body = Some(text.into());
148 Ok(())
149 })
150 }
151
152 pub fn multipart(self, form: Form) -> Self {
154 self.with(|parts| {
155 let content_type =
156 HeaderValue::try_from(format!("multipart/form-data; boundary={}", form.boundary()))
157 .map_err(Error::builder)?;
158 parts.headers.insert(CONTENT_TYPE, content_type);
159 let body = form.into_body();
160 if let Some(bytes) = body.as_bytes() {
161 parts
162 .headers
163 .insert(CONTENT_LENGTH, HeaderValue::from(bytes.len()));
164 }
165 parts.body = Some(body);
166 Ok(())
167 })
168 }
169
170 pub fn build(self) -> Result<http::Request<Body>, Error> {
172 let parts = self.request?;
173 let mut request = http::Request::builder()
174 .method(parts.method)
175 .uri(parts.url.as_str())
176 .body(parts.body.unwrap_or_default())
177 .map_err(Error::builder)?;
178 *request.headers_mut() = parts.headers;
179 Ok(request)
180 }
181
182 pub async fn send(self) -> Result<Response, Error> {
184 let client = self.client.clone();
185 client.execute(self.build()?).await
186 }
187}
188
189impl fmt::Debug for RequestBuilder {
190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191 match &self.request {
192 Ok(parts) => f
193 .debug_struct("RequestBuilder")
194 .field("method", &parts.method)
195 .field("url", &parts.url.as_str())
196 .field("headers", &parts.headers)
197 .finish_non_exhaustive(),
198 Err(error) => f
199 .debug_struct("RequestBuilder")
200 .field("error", error)
201 .finish(),
202 }
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 fn builder(url: &str) -> RequestBuilder {
211 Client::default().request(Method::GET, url)
212 }
213
214 #[test]
215 fn query_pairs_are_appended_and_encoded() {
216 let request = builder("https://example.com/items?a=1")
217 .query(&[("b", "x y"), ("c", "&")])
218 .query(&[("d", 2)])
219 .build()
220 .unwrap();
221 assert_eq!(
222 request.uri(),
223 "https://example.com/items?a=1&b=x+y&c=%26&d=2"
224 );
225 }
226
227 #[test]
228 fn an_empty_query_leaves_no_question_mark() {
229 let empty: [(&str, &str); 0] = [];
230 let request = builder("https://example.com/items")
231 .query(&empty)
232 .build()
233 .unwrap();
234 assert_eq!(request.uri(), "https://example.com/items");
235 }
236
237 #[test]
238 fn json_sets_content_type_and_body() {
239 let request = builder("https://example.com")
240 .json(&serde_json::json!({"a": 1}))
241 .build()
242 .unwrap();
243 assert_eq!(request.headers()[CONTENT_TYPE], "application/json");
244 assert_eq!(request.body().as_bytes(), Some(&br#"{"a":1}"#[..]));
245 }
246
247 #[test]
248 fn form_sets_content_type_and_body() {
249 let request = builder("https://example.com")
250 .form(&[("a", "1"), ("b", "x y")])
251 .build()
252 .unwrap();
253 assert_eq!(
254 request.headers()[CONTENT_TYPE],
255 "application/x-www-form-urlencoded"
256 );
257 assert_eq!(request.body().as_bytes(), Some(&b"a=1&b=x+y"[..]));
258 }
259
260 #[test]
261 fn auth_headers_are_sensitive() {
262 let bearer = builder("https://example.com")
263 .bearer_auth("secret")
264 .build()
265 .unwrap();
266 assert_eq!(bearer.headers()[AUTHORIZATION], "Bearer secret");
267 assert!(bearer.headers()[AUTHORIZATION].is_sensitive());
268 let basic = builder("https://example.com")
269 .basic_auth("user", Some("pass"))
270 .build()
271 .unwrap();
272 assert_eq!(basic.headers()[AUTHORIZATION], "Basic dXNlcjpwYXNz");
273 }
274
275 #[test]
276 fn errors_surface_at_build() {
277 assert!(builder("not a url").build().unwrap_err().is_builder());
278 let invalid = builder("https://example.com")
279 .header("x-bad", "line\nbreak")
280 .build();
281 assert!(invalid.unwrap_err().is_builder());
282 }
283}