crux_http/
request_builder.rs

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