crux_http/request_builder.rs
1use crate::middleware::Middleware;
2use crate::{Client, HttpError, Request, ResponseAsync, Result};
3
4use futures_util::future::BoxFuture;
5use http_types::{
6 Body, Method, Mime, Url,
7 convert::DeserializeOwned,
8 headers::{HeaderName, ToHeaderValues},
9};
10use serde::Serialize;
11
12use std::{fmt, marker::PhantomData};
13
14/// Request Builder
15///
16/// Provides an ergonomic way to chain the creation of a request.
17/// This is generally accessed as the return value from `Http::{method}()`.
18///
19/// # Examples
20///
21/// ```no_run
22/// use crux_http::http::{mime::HTML};
23/// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
24/// # #[crux_core::macros::effect]
25/// # enum Effect { Http(crux_http::HttpRequest) }
26/// # type Http = crux_http::Http<Effect, Event>;
27/// let cmd = Http::post("https://httpbin.org/post")
28/// .body("<html>hi</html>")
29/// .header("custom-header", "value")
30/// .content_type(HTML)
31/// .build()
32/// .then_send(Event::ReceiveResponse);
33/// ```
34#[must_use]
35pub struct RequestBuilder<Event, ExpectBody = Vec<u8>> {
36 /// Holds the state of the request.
37 req: Option<Request>,
38
39 client: Client,
40
41 phantom_event: PhantomData<fn() -> Event>,
42
43 phantom_expect: PhantomData<fn() -> ExpectBody>,
44}
45
46impl RequestBuilder<(), Vec<u8>> {
47 pub(crate) fn new_for_middleware(method: Method, url: Url, client: Client) -> Self {
48 Self {
49 req: Some(Request::new(method, url)),
50 client,
51 phantom_event: PhantomData,
52 phantom_expect: PhantomData,
53 }
54 }
55}
56
57impl<Event, ExpectBody> RequestBuilder<Event, ExpectBody>
58where
59 Event: 'static,
60 ExpectBody: 'static,
61{
62 /// Sets a header on the request.
63 ///
64 /// # Examples
65 ///
66 /// ```no_run
67 /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
68 /// # #[crux_core::macros::effect]
69 /// # enum Effect { Http(crux_http::HttpRequest) }
70 /// # type Http = crux_http::Http<Effect, Event>;
71 /// let cmd = Http::get("https://httpbin.org/get")
72 /// .body("<html>hi</html>")
73 /// .header("header-name", "header-value")
74 /// .build()
75 /// .then_send(Event::ReceiveResponse);
76 /// ```
77 /// # Panics
78 /// Panics if the `RequestBuilder` has not been initialized.
79 pub fn header(mut self, key: impl Into<HeaderName>, value: impl ToHeaderValues) -> Self {
80 self.req.as_mut().unwrap().insert_header(key, value);
81 self
82 }
83
84 /// Sets the Content-Type header on the request.
85 ///
86 /// # Examples
87 ///
88 /// ```no_run
89 /// # use crux_http::http::mime;
90 /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
91 /// # #[crux_core::macros::effect]
92 /// # enum Effect { Http(crux_http::HttpRequest) }
93 /// # type Http = crux_http::Http<Effect, Event>;
94 /// let cmd = Http::get("https://httpbin.org/get")
95 /// .content_type(mime::HTML)
96 /// .build()
97 /// .then_send(Event::ReceiveResponse);
98 /// ```
99 ///
100 /// # Panics
101 /// Panics if the `RequestBuilder` has not been initialized.
102 pub fn content_type(mut self, content_type: impl Into<Mime>) -> Self {
103 self.req
104 .as_mut()
105 .unwrap()
106 .set_content_type(content_type.into());
107 self
108 }
109
110 /// Sets the body of the request from any type with implements `Into<Body>`, for example, any type with is `AsyncRead`.
111 /// # Mime
112 ///
113 /// The encoding is set to `application/octet-stream`.
114 ///
115 /// # Examples
116 ///
117 /// ```no_run
118 /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
119 /// # #[crux_core::macros::effect]
120 /// # enum Effect { Http(crux_http::HttpRequest) }
121 /// # type Http = crux_http::Http<Effect, Event>;
122 /// use serde_json::json;
123 /// use crux_http::http::mime;
124 /// let cmd = Http::post("https://httpbin.org/post")
125 /// .body(json!({"any": "Into<Body>"}))
126 /// .content_type(mime::HTML)
127 /// .build()
128 /// .then_send(Event::ReceiveResponse);
129 /// ```
130 /// # Panics
131 /// Panics if the `RequestBuilder` has not been initialized.
132 pub fn body(mut self, body: impl Into<Body>) -> Self {
133 self.req.as_mut().unwrap().set_body(body);
134 self
135 }
136
137 /// Pass JSON as the request body.
138 ///
139 /// # Mime
140 ///
141 /// The encoding is set to `application/json`.
142 ///
143 /// # Errors
144 ///
145 /// This method will return an error if the provided data could not be serialized to JSON.
146 ///
147 /// # Examples
148 ///
149 /// ```no_run
150 /// # use serde::{Deserialize, Serialize};
151 /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
152 /// # #[crux_core::macros::effect]
153 /// # enum Effect { Http(crux_http::HttpRequest) }
154 /// # type Http = crux_http::Http<Effect, Event>;
155 /// #[derive(Deserialize, Serialize)]
156 /// struct Ip {
157 /// ip: String
158 /// }
159 ///
160 /// let data = &Ip { ip: "129.0.0.1".into() };
161 /// let cmd = Http::post("https://httpbin.org/post")
162 /// .body_json(data)
163 /// .expect("could not serialize body")
164 /// .build()
165 /// .then_send(Event::ReceiveResponse);
166 /// ```
167 pub fn body_json(self, json: &impl Serialize) -> crate::Result<Self> {
168 Ok(self.body(Body::from_json(json)?))
169 }
170
171 /// Pass a string as the request body.
172 ///
173 /// # Mime
174 ///
175 /// The encoding is set to `text/plain; charset=utf-8`.
176 ///
177 /// # Examples
178 ///
179 /// ```no_run
180 /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
181 /// # #[crux_core::macros::effect]
182 /// # enum Effect { Http(crux_http::HttpRequest) }
183 /// # type Http = crux_http::Http<Effect, Event>;
184 /// let cmd = Http::post("https://httpbin.org/post")
185 /// .body_string("hello_world".to_string())
186 /// .build()
187 /// .then_send(Event::ReceiveResponse);
188 /// ```
189 pub fn body_string(self, string: String) -> Self {
190 self.body(Body::from_string(string))
191 }
192
193 /// Pass bytes as the request body.
194 ///
195 /// # Mime
196 ///
197 /// The encoding is set to `application/octet-stream`.
198 ///
199 /// # Examples
200 ///
201 /// ```no_run
202 /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
203 /// # #[crux_core::macros::effect]
204 /// # enum Effect { Http(crux_http::HttpRequest) }
205 /// # type Http = crux_http::Http<Effect, Event>;
206 /// let cmd = Http::post("https://httpbin.org/post")
207 /// .body_bytes(b"hello_world".to_owned())
208 /// .build()
209 /// .then_send(Event::ReceiveResponse);
210 /// ```
211 pub fn body_bytes(self, bytes: impl AsRef<[u8]>) -> Self {
212 self.body(Body::from(bytes.as_ref()))
213 }
214
215 /// Pass form data as the request body. The form data needs to be
216 /// serializable to name-value pairs.
217 ///
218 /// # Mime
219 ///
220 /// The `content-type` is set to `application/x-www-form-urlencoded`.
221 ///
222 /// # Errors
223 ///
224 /// An error will be returned if the provided data cannot be serialized to
225 /// form data.
226 ///
227 /// # Examples
228 ///
229 /// ```no_run
230 /// # use std::collections::HashMap;
231 /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
232 /// # #[crux_core::macros::effect]
233 /// # enum Effect { Http(crux_http::HttpRequest) }
234 /// # type Http = crux_http::Http<Effect, Event>;
235 /// let form_data = HashMap::from([
236 /// ("name", "Alice"),
237 /// ("location", "UK"),
238 /// ]);
239 /// let cmd = Http::post("https://httpbin.org/post")
240 /// .body_form(&form_data)
241 /// .expect("could not serialize body")
242 /// .build()
243 /// .then_send(Event::ReceiveResponse);
244 /// ```
245 pub fn body_form(self, form: &impl Serialize) -> crate::Result<Self> {
246 Ok(self.body(Body::from_form(form)?))
247 }
248
249 /// Set the URL querystring.
250 ///
251 /// # Examples
252 ///
253 /// ```no_run
254 /// # use serde::{Deserialize, Serialize};
255 /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
256 /// # #[crux_core::macros::effect]
257 /// # enum Effect { Http(crux_http::HttpRequest) }
258 /// # type Http = crux_http::Http<Effect, Event>;
259 /// #[derive(Serialize, Deserialize)]
260 /// struct Index {
261 /// page: u32
262 /// }
263 ///
264 /// let query = Index { page: 2 };
265 /// let cmd = Http::post("https://httpbin.org/post")
266 /// .query(&query)
267 /// .expect("could not serialize query string")
268 /// .build()
269 /// .then_send(Event::ReceiveResponse);
270 /// ```
271 /// # Panics
272 /// Panics if the `RequestBuilder` has not been initialized.
273 /// # Errors
274 /// Returns an error if the query string cannot be serialized.
275 pub fn query(mut self, query: &impl Serialize) -> std::result::Result<Self, HttpError> {
276 self.req.as_mut().unwrap().set_query(query)?;
277
278 Ok(self)
279 }
280
281 /// Push middleware onto a per-request middleware stack.
282 ///
283 /// **Important**: Setting per-request middleware incurs extra allocations.
284 /// Creating a `Client` with middleware is recommended.
285 ///
286 /// Client middleware is run before per-request middleware.
287 ///
288 /// See the [middleware] submodule for more information on middleware.
289 ///
290 /// [middleware]: ../middleware/index.html
291 ///
292 /// # Examples
293 ///
294 /// ```no_run
295 /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
296 /// # #[crux_core::macros::effect]
297 /// # enum Effect { Http(crux_http::HttpRequest) }
298 /// # type Http = crux_http::Http<Effect, Event>;
299 /// let cmd = Http::get("https://httpbin.org/redirect/2")
300 /// .middleware(crux_http::middleware::Redirect::default())
301 /// .build()
302 /// .then_send(Event::ReceiveResponse);
303 /// ```
304 /// # Panics
305 /// Panics if the `RequestBuilder` has not been initialized.
306 pub fn middleware(mut self, middleware: impl Middleware) -> Self {
307 self.req.as_mut().unwrap().middleware(middleware);
308 self
309 }
310
311 /// Return the constructed `Request`.
312 /// # Panics
313 /// Panics if the `RequestBuilder` has not been initialized.
314 #[must_use]
315 pub fn build(self) -> Request {
316 self.req.unwrap()
317 }
318
319 /// Decode a String from the response body prior to dispatching it to the apps `update`
320 /// function.
321 ///
322 /// This has no effect when used with the [async API](RequestBuilder::send_async).
323 ///
324 /// # Examples
325 ///
326 /// ```no_run
327 /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<String>>) }
328 /// # #[crux_core::macros::effect]
329 /// # enum Effect { Http(crux_http::HttpRequest) }
330 /// # type Http = crux_http::Http<Effect, Event>;
331 /// let cmd = Http::post("https://httpbin.org/post")
332 /// .expect_string()
333 /// .build()
334 /// .then_send(Event::ReceiveResponse);
335 /// ```
336 pub fn expect_string(self) -> RequestBuilder<Event, String> {
337 RequestBuilder {
338 req: self.req,
339 client: self.client,
340 phantom_event: PhantomData,
341 phantom_expect: PhantomData,
342 }
343 }
344
345 /// Decode a `T` from a JSON response body prior to dispatching it to the apps `update`
346 /// function.
347 ///
348 /// This has no effect when used with the [async API](RequestBuilder::send_async).
349 ///
350 /// # Examples
351 ///
352 /// ```no_run
353 /// # use serde::{Deserialize, Serialize};
354 /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Slideshow>>) }
355 /// # #[crux_core::macros::effect]
356 /// # enum Effect { Http(crux_http::HttpRequest) }
357 /// # type Http = crux_http::Http<Effect, Event>;
358 /// #[derive(Deserialize)]
359 /// struct Response {
360 /// slideshow: Slideshow
361 /// }
362 ///
363 /// #[derive(Deserialize)]
364 /// struct Slideshow {
365 /// author: String
366 /// }
367 ///
368 /// let cmd = Http::post("https://httpbin.org/json")
369 /// .expect_json::<Slideshow>()
370 /// .build()
371 /// .then_send(Event::ReceiveResponse);
372 /// ```
373 pub fn expect_json<T>(self) -> RequestBuilder<Event, T>
374 where
375 T: DeserializeOwned + 'static,
376 {
377 RequestBuilder {
378 req: self.req,
379 client: self.client,
380 phantom_event: PhantomData,
381 phantom_expect: PhantomData,
382 }
383 }
384
385 /// Sends the constructed `Request` and returns a future that resolves to [`ResponseAsync`].
386 /// but does not consume it or convert the body to an expected format.
387 ///
388 /// Note that this is equivalent to calling `.into_future()` on the `RequestBuilder`, which
389 /// will happen implicitly when calling `.await` on the builder, which does implement
390 /// [`IntoFuture`](std::future::IntoFuture). Calling `.await` on the builder is recommended.
391 ///
392 /// Not all code working with futures (such as the `join` macro) works with `IntoFuture` (yet?), so this
393 /// method is provided as a more discoverable `.into_future` alias, and may be deprecated later.
394 #[must_use]
395 pub fn send_async(self) -> BoxFuture<'static, Result<ResponseAsync>> {
396 <Self as std::future::IntoFuture>::into_future(self)
397 }
398}
399
400impl<T, Eb> std::future::IntoFuture for RequestBuilder<T, Eb> {
401 type Output = Result<ResponseAsync>;
402
403 type IntoFuture = BoxFuture<'static, Result<ResponseAsync>>;
404
405 /// Sends the constructed `Request` and returns a future that resolves to the response
406 fn into_future(self) -> Self::IntoFuture {
407 Box::pin(async move { self.client.send(self.req.unwrap()).await })
408 }
409}
410
411impl<Ev> fmt::Debug for RequestBuilder<Ev> {
412 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
413 fmt::Debug::fmt(&self.req, f)
414 }
415}