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