Skip to main content

crux_http/
request_builder.rs

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