crux_http/
command.rs

1//! The Command based API for crux_http
2
3use std::{fmt, future::Future, marker::PhantomData};
4
5use crux_core::{command, Command};
6use http_types::{
7    convert::DeserializeOwned,
8    headers::{HeaderName, ToHeaderValues},
9    Body, Method, Mime, Url,
10};
11use serde::Serialize;
12
13use crate::{
14    expect::{ExpectBytes, ExpectJson, ExpectString, ResponseExpectation},
15    middleware::Middleware,
16    protocol::{HttpRequest, HttpResult, ProtocolRequestBuilder},
17    HttpError, Request, Response,
18};
19
20pub struct Http<Effect, Event> {
21    effect: PhantomData<Effect>,
22    event: PhantomData<Event>,
23}
24
25impl<Effect, Event> Http<Effect, Event>
26where
27    Effect: Send + From<crux_core::Request<HttpRequest>> + 'static,
28    Event: Send + 'static,
29{
30    /// Instruct the Shell to perform a HTTP GET request to the provided `url`.
31    ///
32    /// The request can be configured via associated functions on the returned
33    /// [`RequestBuilder`] and then converted to a [`Command`]
34    /// with [`RequestBuilder::build`].
35    ///
36    /// # Panics
37    ///
38    /// This will panic if a malformed URL is passed.
39    ///
40    /// # Examples
41    ///
42    /// ```
43    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<String>>) }
44    /// # #[derive(crux_core::macros::Effect)]
45    /// # #[allow(unused)]
46    /// # struct Capabilities { http: crux_http::Http<Event> }
47    /// # type Http = crux_http::command::Http<Effect, Event>;
48    /// Http::get("https://httpbin.org/get")
49    ///     .expect_string()
50    ///     .build()
51    ///     .then_send(Event::ReceiveResponse);
52    /// ```
53    pub fn get(url: impl AsRef<str>) -> RequestBuilder<Effect, Event> {
54        RequestBuilder::new(Method::Get, url.as_ref().parse().unwrap())
55    }
56
57    /// Instruct the Shell to perform a HTTP HEAD request to the provided `url`.
58    ///
59    /// The request can be configured via associated functions on the returned
60    /// [`RequestBuilder`] and then converted to a [`Command`]
61    /// with [`RequestBuilder::build`].
62    ///
63    /// # Panics
64    ///
65    /// This will panic if a malformed URL is passed.
66    ///
67    /// # Examples
68    ///
69    /// ```
70    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
71    /// # #[derive(crux_core::macros::Effect)]
72    /// # #[allow(unused)]
73    /// # struct Capabilities { http: crux_http::Http<Event> }
74    /// # type Http = crux_http::command::Http<Effect, Event>;
75    /// Http::head("https://httpbin.org/get")
76    ///     .build()
77    ///     .then_send(Event::ReceiveResponse);
78    pub fn head(url: impl AsRef<str>) -> RequestBuilder<Effect, Event> {
79        RequestBuilder::new(Method::Head, url.as_ref().parse().unwrap())
80    }
81
82    /// Instruct the Shell to perform a HTTP POST request to the provided `url`.
83    ///
84    /// The request can be configured via associated functions on the returned
85    /// [`RequestBuilder`] and then converted to a [`Command`]
86    /// with [`RequestBuilder::build`].
87    ///
88    /// # Panics
89    ///
90    /// This will panic if a malformed URL is passed.
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
96    /// # #[derive(crux_core::macros::Effect)]
97    /// # #[allow(unused)]
98    /// # struct Capabilities { http: crux_http::Http<Event> }
99    /// # type Http = crux_http::command::Http<Effect, Event>;
100    /// Http::post("https://httpbin.org/post")
101    ///     .body_bytes(b"hello_world".to_owned())
102    ///     .build()
103    ///     .then_send(Event::ReceiveResponse);
104    pub fn post(url: impl AsRef<str>) -> RequestBuilder<Effect, Event> {
105        RequestBuilder::new(Method::Post, url.as_ref().parse().unwrap())
106    }
107
108    /// Instruct the Shell to perform a HTTP PUT request to the provided `url`.
109    ///
110    /// The request can be configured via associated functions on the returned
111    /// [`RequestBuilder`] and then converted to a [`Command`]
112    /// with [`RequestBuilder::build`].
113    ///
114    /// # Panics
115    ///
116    /// This will panic if a malformed URL is passed.
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
122    /// # #[derive(crux_core::macros::Effect)]
123    /// # #[allow(unused)]
124    /// # struct Capabilities { http: crux_http::Http<Event> }
125    /// # type Http = crux_http::command::Http<Effect, Event>;
126    /// Http::put("https://httpbin.org/put")
127    ///     .body_string("hello_world".to_string())
128    ///     .build()
129    ///     .then_send(Event::ReceiveResponse);
130    pub fn put(url: impl AsRef<str>) -> RequestBuilder<Effect, Event> {
131        RequestBuilder::new(Method::Put, url.as_ref().parse().unwrap())
132    }
133
134    /// Instruct the Shell to perform a HTTP DELETE request to the provided `url`.
135    ///
136    /// The request can be configured via associated functions on the returned
137    /// [`RequestBuilder`] and then converted to a [`Command`]
138    /// with [`RequestBuilder::build`].
139    ///
140    /// # Panics
141    ///
142    /// This will panic if a malformed URL is passed.
143    ///
144    /// # Examples
145    ///
146    /// ```
147    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
148    /// # #[derive(crux_core::macros::Effect)]
149    /// # #[allow(unused)]
150    /// # struct Capabilities { http: crux_http::Http<Event> }
151    /// # type Http = crux_http::command::Http<Effect, Event>;
152    /// Http::delete("https://httpbin.org/delete")
153    ///     .build()
154    ///     .then_send(Event::ReceiveResponse);
155    pub fn delete(url: impl AsRef<str>) -> RequestBuilder<Effect, Event> {
156        RequestBuilder::new(Method::Delete, url.as_ref().parse().unwrap())
157    }
158
159    /// Instruct the Shell to perform a HTTP PATCH request to the provided `url`.
160    ///
161    /// The request can be configured via associated functions on the returned
162    /// [`RequestBuilder`] and then converted to a [`Command`]
163    /// with [`RequestBuilder::build`].
164    ///
165    /// # Panics
166    ///
167    /// This will panic if a malformed URL is passed.
168    ///
169    /// # Examples
170    ///
171    /// ```
172    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
173    /// # #[derive(crux_core::macros::Effect)]
174    /// # #[allow(unused)]
175    /// # struct Capabilities { http: crux_http::Http<Event> }
176    /// # type Http = crux_http::command::Http<Effect, Event>;
177    /// Http::patch("https://httpbin.org/patch")
178    ///     .body_form(&[("name", "Alice")]).unwrap()
179    ///     .build()
180    ///     .then_send(Event::ReceiveResponse);
181    pub fn patch(url: impl AsRef<str>) -> RequestBuilder<Effect, Event> {
182        RequestBuilder::new(Method::Patch, url.as_ref().parse().unwrap())
183    }
184
185    /// Instruct the Shell to perform a HTTP OPTIONS request to the provided `url`.
186    ///
187    /// The request can be configured via associated functions on the returned
188    /// [`RequestBuilder`] and then converted to a [`Command`]
189    /// with [`RequestBuilder::build`].
190    ///
191    /// # Panics
192    ///
193    /// This will panic if a malformed URL is passed.
194    ///
195    /// # Examples
196    ///
197    /// ```
198    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
199    /// # #[derive(crux_core::macros::Effect)]
200    /// # #[allow(unused)]
201    /// # struct Capabilities { http: crux_http::Http<Event> }
202    /// # type Http = crux_http::command::Http<Effect, Event>;
203    /// Http::options("https://httpbin.org/get")
204    ///     .build()
205    ///     .then_send(Event::ReceiveResponse);
206    pub fn options(url: impl AsRef<str>) -> RequestBuilder<Effect, Event> {
207        RequestBuilder::new(Method::Options, url.as_ref().parse().unwrap())
208    }
209
210    /// Instruct the Shell to perform a HTTP TRACE request to the provided `url`.
211    ///
212    /// The request can be configured via associated functions on the returned
213    /// [`RequestBuilder`] and then converted to a [`Command`]
214    /// with [`RequestBuilder::build`].
215    ///
216    /// # Panics
217    ///
218    /// This will panic if a malformed URL is passed.
219    ///
220    /// # Examples
221    ///
222    /// ```
223    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
224    /// # #[derive(crux_core::macros::Effect)]
225    /// # #[allow(unused)]
226    /// # struct Capabilities { http: crux_http::Http<Event> }
227    /// # type Http = crux_http::command::Http<Effect, Event>;
228    /// Http::trace("https://httpbin.org/get")
229    ///     .build()
230    ///     .then_send(Event::ReceiveResponse);
231    pub fn trace(url: impl AsRef<str>) -> RequestBuilder<Effect, Event> {
232        RequestBuilder::new(Method::Trace, url.as_ref().parse().unwrap())
233    }
234
235    /// Instruct the Shell to perform a HTTP CONNECT request to the provided `url`.
236    ///
237    /// The request can be configured via associated functions on the returned
238    /// [`RequestBuilder`] and then converted to a [`Command`]
239    /// with [`RequestBuilder::build`].
240    ///
241    /// # Panics
242    ///
243    /// This will panic if a malformed URL is passed.
244    ///
245    /// # Examples
246    ///
247    /// ```
248    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
249    /// # #[derive(crux_core::macros::Effect)]
250    /// # #[allow(unused)]
251    /// # struct Capabilities { http: crux_http::Http<Event> }
252    /// # type Http = crux_http::command::Http<Effect, Event>;
253    /// Http::connect("https://httpbin.org/get")
254    ///     .build()
255    ///     .then_send(Event::ReceiveResponse);
256    pub fn connect(url: impl AsRef<str>) -> RequestBuilder<Effect, Event> {
257        RequestBuilder::new(Method::Connect, url.as_ref().parse().unwrap())
258    }
259
260    /// Instruct the Shell to perform an HTTP request to the provided `url`.
261    ///
262    /// The request can be configured via associated functions on the returned
263    /// [`RequestBuilder`] and then converted to a [`Command`]
264    /// with [`RequestBuilder::build`].
265    ///
266    /// # Panics
267    ///
268    /// This will panic if a malformed URL is passed.
269    ///
270    /// # Examples
271    ///
272    /// ```
273    /// # use http_types::Method;
274    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
275    /// # #[derive(crux_core::macros::Effect)]
276    /// # #[allow(unused)]
277    /// # struct Capabilities { http: crux_http::Http<Event> }
278    /// # type Http = crux_http::command::Http<Effect, Event>;
279    /// Http::request(Method::Post, "https://httpbin.org/post".parse().unwrap())
280    ///     .body_form(&[("name", "Alice")]).unwrap()
281    ///     .build()
282    ///     .then_send(Event::ReceiveResponse);
283    pub fn request(method: Method, url: Url) -> RequestBuilder<Effect, Event> {
284        RequestBuilder::new(method, url)
285    }
286}
287
288/// Request Builder
289///
290/// Provides an ergonomic way to chain the creation of a request.
291/// This is generally accessed as the return value from
292/// `crux_http::command::Http::{method}()`.
293///
294/// # Examples
295///
296/// ```
297/// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
298/// # #[derive(crux_core::macros::Effect)]
299/// # #[allow(unused)]
300/// # struct Capabilities { http: crux_http::Http<Event> }
301/// # type Http = crux_http::command::Http<Effect, Event>;
302/// Http::post("https://httpbin.org/post")
303///     .body("<html>hi</html>")
304///     .header("custom-header", "value")
305///     .content_type(crux_http::http::mime::HTML)
306///     .build()
307///     .then_send(Event::ReceiveResponse);
308/// ```
309#[must_use]
310pub struct RequestBuilder<Effect, Event, ExpectBody = Vec<u8>> {
311    /// Holds the state of the request.
312    req: Option<Request>,
313    effect: PhantomData<Effect>,
314    event: PhantomData<fn() -> Event>,
315    expectation: Box<dyn ResponseExpectation<Body = ExpectBody> + Send>,
316}
317
318impl<Effect, Event> RequestBuilder<Effect, Event, Vec<u8>>
319where
320    Effect: Send + From<crux_core::Request<HttpRequest>> + 'static,
321    Event: 'static,
322{
323    pub(crate) fn new(method: Method, url: Url) -> Self {
324        Self {
325            req: Some(Request::new(method, url)),
326            effect: PhantomData,
327            event: PhantomData,
328            expectation: Box::new(ExpectBytes),
329        }
330    }
331}
332
333impl<Effect, Event, ExpectBody> RequestBuilder<Effect, Event, ExpectBody>
334where
335    Effect: Send + From<crux_core::Request<HttpRequest>> + 'static,
336    Event: Send + 'static,
337    ExpectBody: 'static,
338{
339    /// Sets a header on the request.
340    ///
341    /// # Examples
342    ///
343    /// ```
344    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
345    /// # #[derive(crux_core::macros::Effect)]
346    /// # #[allow(unused)]
347    /// # struct Capabilities { http: crux_http::Http<Event> }
348    /// # type Http = crux_http::command::Http<Effect, Event>;
349    /// Http::get("https://httpbin.org/get")
350    ///     .body("<html>hi</html>")
351    ///     .header("header-name", "header-value")
352    ///     .build()
353    ///     .then_send(Event::ReceiveResponse);
354    /// ```
355    pub fn header(mut self, key: impl Into<HeaderName>, value: impl ToHeaderValues) -> Self {
356        self.req.as_mut().unwrap().insert_header(key, value);
357        self
358    }
359
360    /// Sets the Content-Type header on the request.
361    ///
362    /// # Examples
363    ///
364    /// ```
365    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
366    /// # #[derive(crux_core::macros::Effect)]
367    /// # #[allow(unused)]
368    /// # struct Capabilities { http: crux_http::Http<Event> }
369    /// # type Http = crux_http::command::Http<Effect, Event>;
370    /// Http::get("https://httpbin.org/get")
371    ///     .content_type(crux_http::http::mime::HTML)
372    ///     .build()
373    ///     .then_send(Event::ReceiveResponse);
374    /// ```
375    pub fn content_type(mut self, content_type: impl Into<Mime>) -> Self {
376        self.req
377            .as_mut()
378            .unwrap()
379            .set_content_type(content_type.into());
380        self
381    }
382
383    /// Sets the body of the request from any type that implements `Into<Body>`
384    ///
385    /// # Mime
386    ///
387    /// The encoding is set to `application/octet-stream`.
388    ///
389    /// # Examples
390    ///
391    /// ```
392    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
393    /// # #[derive(crux_core::macros::Effect)]
394    /// # #[allow(unused)]
395    /// # struct Capabilities { http: crux_http::Http<Event> }
396    /// # type Http = crux_http::command::Http<Effect, Event>;
397    /// Http::post("https://httpbin.org/post")
398    ///     .body(serde_json::json!({"any": "Into<Body>"}))
399    ///     .content_type(crux_http::http::mime::HTML)
400    ///     .build()
401    ///     .then_send(Event::ReceiveResponse);
402    /// ```
403    pub fn body(mut self, body: impl Into<Body>) -> Self {
404        self.req.as_mut().unwrap().set_body(body);
405        self
406    }
407
408    /// Pass JSON as the request body.
409    ///
410    /// # Mime
411    ///
412    /// The encoding is set to `application/json`.
413    ///
414    /// # Errors
415    ///
416    /// This method will return an error if the provided data could not be serialized to JSON.
417    ///
418    /// # Examples
419    ///
420    /// ```
421    /// # use serde::{Deserialize, Serialize};
422    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
423    /// # #[derive(crux_core::macros::Effect)]
424    /// # #[allow(unused)]
425    /// # struct Capabilities { http: crux_http::Http<Event> }
426    /// # type Http = crux_http::command::Http<Effect, Event>;
427    /// #[derive(Deserialize, Serialize)]
428    /// struct Ip {
429    ///     ip: String
430    /// }
431    ///
432    /// let data = &Ip { ip: "129.0.0.1".into() };
433    /// Http::post("https://httpbin.org/post")
434    ///     .body_json(data)
435    ///     .expect("could not serialize body")
436    ///     .build()
437    ///     .then_send(Event::ReceiveResponse);
438    /// ```
439    pub fn body_json(self, json: &impl Serialize) -> crate::Result<Self> {
440        Ok(self.body(Body::from_json(json)?))
441    }
442
443    /// Pass a string as the request body.
444    ///
445    /// # Mime
446    ///
447    /// The encoding is set to `text/plain; charset=utf-8`.
448    ///
449    /// # Examples
450    ///
451    /// ```
452    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
453    /// # #[derive(crux_core::macros::Effect)]
454    /// # #[allow(unused)]
455    /// # struct Capabilities { http: crux_http::Http<Event> }
456    /// # type Http = crux_http::command::Http<Effect, Event>;
457    /// Http::post("https://httpbin.org/post")
458    ///     .body_string("hello_world".to_string())
459    ///     .build()
460    ///     .then_send(Event::ReceiveResponse);
461    /// ```
462    pub fn body_string(self, string: String) -> Self {
463        self.body(Body::from_string(string))
464    }
465
466    /// Pass bytes as the request body.
467    ///
468    /// # Mime
469    ///
470    /// The encoding is set to `application/octet-stream`.
471    ///
472    /// # Examples
473    ///
474    /// ```
475    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
476    /// # #[derive(crux_core::macros::Effect)]
477    /// # #[allow(unused)]
478    /// # struct Capabilities { http: crux_http::Http<Event> }
479    /// # type Http = crux_http::command::Http<Effect, Event>;
480    /// Http::post("https://httpbin.org/post")
481    ///     .body_bytes(b"hello_world".to_owned())
482    ///     .build()
483    ///     .then_send(Event::ReceiveResponse);
484    /// ```
485    pub fn body_bytes(self, bytes: impl AsRef<[u8]>) -> Self {
486        self.body(Body::from(bytes.as_ref()))
487    }
488
489    /// Pass form data as the request body. The form data needs to be
490    /// serializable to name-value pairs.
491    ///
492    /// # Mime
493    ///
494    /// The `content-type` is set to `application/x-www-form-urlencoded`.
495    ///
496    /// # Errors
497    ///
498    /// An error will be returned if the provided data cannot be serialized to
499    /// form data.
500    ///
501    /// # Examples
502    ///
503    /// ```
504    /// # use std::collections::HashMap;
505    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
506    /// # #[derive(crux_core::macros::Effect)]
507    /// # #[allow(unused)]
508    /// # struct Capabilities { http: crux_http::Http<Event> }
509    /// # type Http = crux_http::command::Http<Effect, Event>;
510    /// let form_data = HashMap::from([
511    ///     ("name", "Alice"),
512    ///     ("location", "UK"),
513    /// ]);
514    /// Http::post("https://httpbin.org/post")
515    ///     .body_form(&form_data)
516    ///     .expect("could not serialize body")
517    ///     .build()
518    ///     .then_send(Event::ReceiveResponse);
519    /// ```
520    pub fn body_form(self, form: &impl Serialize) -> crate::Result<Self> {
521        Ok(self.body(Body::from_form(form)?))
522    }
523
524    /// Set the URL querystring.
525    ///
526    /// # Examples
527    ///
528    /// ```
529    /// # use serde::{Deserialize, Serialize};
530    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
531    /// # #[derive(crux_core::macros::Effect)]
532    /// # #[allow(unused)]
533    /// # struct Capabilities { http: crux_http::Http<Event> }
534    /// # type Http = crux_http::command::Http<Effect, Event>;
535    /// #[derive(Serialize, Deserialize)]
536    /// struct Index {
537    ///     page: u32
538    /// }
539    ///
540    /// let query = Index { page: 2 };
541    /// Http::post("https://httpbin.org/post")
542    ///     .query(&query)
543    ///     .expect("could not serialize query string")
544    ///     .build()
545    ///     .then_send(Event::ReceiveResponse);
546    /// ```
547    pub fn query(mut self, query: &impl Serialize) -> std::result::Result<Self, HttpError> {
548        self.req.as_mut().unwrap().set_query(query)?;
549
550        Ok(self)
551    }
552
553    /// Push middleware onto a per-request middleware stack.
554    ///
555    /// **Important**: Setting per-request middleware incurs extra allocations.
556    /// Creating a `Client` with middleware is recommended.
557    ///
558    /// Client middleware is run before per-request middleware.
559    ///
560    /// See the [middleware] submodule for more information on middleware.
561    ///
562    /// [middleware]: ../middleware/index.html
563    ///
564    /// # Examples
565    ///
566    /// ```
567    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
568    /// # #[derive(crux_core::macros::Effect)]
569    /// # #[allow(unused)]
570    /// # struct Capabilities { http: crux_http::Http<Event> }
571    /// # type Http = crux_http::command::Http<Effect, Event>;
572    /// Http::get("https://httpbin.org/redirect/2")
573    ///     .middleware(crux_http::middleware::Redirect::default())
574    ///     .build()
575    ///     .then_send(Event::ReceiveResponse);
576    /// ```
577    pub fn middleware(mut self, middleware: impl Middleware) -> Self {
578        self.req.as_mut().unwrap().middleware(middleware);
579        self
580    }
581
582    /// Return the constructed `Request` in a [`crux_core::command::RequestBuilder`].
583    pub fn build(
584        self,
585    ) -> command::RequestBuilder<
586        Effect,
587        Event,
588        impl Future<Output = Result<Response<ExpectBody>, HttpError>>,
589    > {
590        let req = self.req.expect("RequestBuilder::build called twice");
591
592        command::RequestBuilder::new(|ctx| async move {
593            let operation = req
594                .into_protocol_request()
595                .await
596                .expect("should be able to convert request to protocol request");
597
598            let result = Command::request_from_shell(operation)
599                .into_future(ctx)
600                .await;
601
602            match result {
603                HttpResult::Ok(response) => Response::<Vec<u8>>::new(response.into())
604                    .await
605                    .and_then(|r| self.expectation.decode(r)),
606                HttpResult::Err(error) => Err(error),
607            }
608        })
609    }
610
611    /// Decode a String from the response body prior to dispatching it to the apps `update`
612    /// function.
613    ///
614    /// # Examples
615    ///
616    /// ```
617    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<String>>) }
618    /// # #[derive(crux_core::macros::Effect)]
619    /// # #[allow(unused)]
620    /// # struct Capabilities { http: crux_http::Http<Event> }
621    /// # type Http = crux_http::command::Http<Effect, Event>;
622    /// Http::post("https://httpbin.org/json")
623    ///     .expect_string()
624    ///     .build()
625    ///     .then_send(Event::ReceiveResponse);
626    /// ```
627    pub fn expect_string(self) -> RequestBuilder<Effect, Event, String> {
628        let expectation = Box::<ExpectString>::default();
629        RequestBuilder {
630            req: self.req,
631            effect: PhantomData,
632            event: PhantomData,
633            expectation,
634        }
635    }
636
637    /// Decode a `T` from a JSON response body prior to dispatching it to the apps `update`
638    /// function.
639    ///
640    /// # Examples
641    ///
642    /// ```
643    /// # use serde::{Deserialize, Serialize};
644    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Slideshow>>) }
645    /// # #[derive(crux_core::macros::Effect)]
646    /// # #[allow(unused)]
647    /// # struct Capabilities { http: crux_http::Http<Event> }
648    /// # type Http = crux_http::command::Http<Effect, Event>;
649    /// #[derive(Deserialize)]
650    /// struct Response {
651    ///     slideshow: Slideshow
652    /// }
653    ///
654    /// #[derive(Deserialize)]
655    /// struct Slideshow {
656    ///     author: String
657    /// }
658    ///
659    /// Http::post("https://httpbin.org/json")
660    ///     .expect_json::<Slideshow>()
661    ///     .build()
662    ///     .then_send(Event::ReceiveResponse);
663    /// ```
664    pub fn expect_json<T>(self) -> RequestBuilder<Effect, Event, T>
665    where
666        T: DeserializeOwned + 'static,
667    {
668        let expectation = Box::<ExpectJson<T>>::default();
669        RequestBuilder {
670            req: self.req,
671            effect: PhantomData,
672            event: PhantomData,
673            expectation,
674        }
675    }
676}
677
678impl<Effect, Event> fmt::Debug for RequestBuilder<Effect, Event> {
679    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
680        fmt::Debug::fmt(&self.req, f)
681    }
682}