Skip to main content

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}