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    ///
356    #[allow(clippy::missing_panics_doc)]
357    pub fn header(mut self, key: impl Into<HeaderName>, value: impl ToHeaderValues) -> Self {
358        self.req.as_mut().unwrap().insert_header(key, value);
359        self
360    }
361
362    /// Sets the Content-Type header on the request.
363    ///
364    /// # Examples
365    ///
366    /// ```
367    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
368    /// # #[derive(crux_core::macros::Effect)]
369    /// # #[allow(unused)]
370    /// # struct Capabilities { http: crux_http::Http<Event> }
371    /// # type Http = crux_http::command::Http<Effect, Event>;
372    /// Http::get("https://httpbin.org/get")
373    ///     .content_type(crux_http::http::mime::HTML)
374    ///     .build()
375    ///     .then_send(Event::ReceiveResponse);
376    /// ```
377    #[allow(clippy::missing_panics_doc)]
378    pub fn content_type(mut self, content_type: impl Into<Mime>) -> Self {
379        self.req
380            .as_mut()
381            .unwrap()
382            .set_content_type(content_type.into());
383        self
384    }
385
386    /// Sets the body of the request from any type that implements `Into<Body>`
387    ///
388    /// # Mime
389    ///
390    /// The encoding is set to `application/octet-stream`.
391    ///
392    /// # Examples
393    ///
394    /// ```
395    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
396    /// # #[derive(crux_core::macros::Effect)]
397    /// # #[allow(unused)]
398    /// # struct Capabilities { http: crux_http::Http<Event> }
399    /// # type Http = crux_http::command::Http<Effect, Event>;
400    /// Http::post("https://httpbin.org/post")
401    ///     .body(serde_json::json!({"any": "Into<Body>"}))
402    ///     .content_type(crux_http::http::mime::HTML)
403    ///     .build()
404    ///     .then_send(Event::ReceiveResponse);
405    /// ```
406    #[allow(clippy::missing_panics_doc)]
407    pub fn body(mut self, body: impl Into<Body>) -> Self {
408        self.req.as_mut().unwrap().set_body(body);
409        self
410    }
411
412    /// Pass JSON as the request body.
413    ///
414    /// # Mime
415    ///
416    /// The encoding is set to `application/json`.
417    ///
418    /// # Errors
419    ///
420    /// This method will return an error if the provided data could not be serialized to JSON.
421    ///
422    /// # Examples
423    ///
424    /// ```
425    /// # use serde::{Deserialize, Serialize};
426    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
427    /// # #[derive(crux_core::macros::Effect)]
428    /// # #[allow(unused)]
429    /// # struct Capabilities { http: crux_http::Http<Event> }
430    /// # type Http = crux_http::command::Http<Effect, Event>;
431    /// #[derive(Deserialize, Serialize)]
432    /// struct Ip {
433    ///     ip: String
434    /// }
435    ///
436    /// let data = &Ip { ip: "129.0.0.1".into() };
437    /// Http::post("https://httpbin.org/post")
438    ///     .body_json(data)
439    ///     .expect("could not serialize body")
440    ///     .build()
441    ///     .then_send(Event::ReceiveResponse);
442    /// ```
443    pub fn body_json(self, json: &impl Serialize) -> crate::Result<Self> {
444        Ok(self.body(Body::from_json(json)?))
445    }
446
447    /// Pass a string as the request body.
448    ///
449    /// # Mime
450    ///
451    /// The encoding is set to `text/plain; charset=utf-8`.
452    ///
453    /// # Examples
454    ///
455    /// ```
456    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
457    /// # #[derive(crux_core::macros::Effect)]
458    /// # #[allow(unused)]
459    /// # struct Capabilities { http: crux_http::Http<Event> }
460    /// # type Http = crux_http::command::Http<Effect, Event>;
461    /// Http::post("https://httpbin.org/post")
462    ///     .body_string("hello_world".to_string())
463    ///     .build()
464    ///     .then_send(Event::ReceiveResponse);
465    /// ```
466    pub fn body_string(self, string: String) -> Self {
467        self.body(Body::from_string(string))
468    }
469
470    /// Pass bytes as the request body.
471    ///
472    /// # Mime
473    ///
474    /// The encoding is set to `application/octet-stream`.
475    ///
476    /// # Examples
477    ///
478    /// ```
479    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
480    /// # #[derive(crux_core::macros::Effect)]
481    /// # #[allow(unused)]
482    /// # struct Capabilities { http: crux_http::Http<Event> }
483    /// # type Http = crux_http::command::Http<Effect, Event>;
484    /// Http::post("https://httpbin.org/post")
485    ///     .body_bytes(b"hello_world".to_owned())
486    ///     .build()
487    ///     .then_send(Event::ReceiveResponse);
488    /// ```
489    pub fn body_bytes(self, bytes: impl AsRef<[u8]>) -> Self {
490        self.body(Body::from(bytes.as_ref()))
491    }
492
493    /// Pass form data as the request body. The form data needs to be
494    /// serializable to name-value pairs.
495    ///
496    /// # Mime
497    ///
498    /// The `content-type` is set to `application/x-www-form-urlencoded`.
499    ///
500    /// # Errors
501    ///
502    /// An error will be returned if the provided data cannot be serialized to
503    /// form data.
504    ///
505    /// # Examples
506    ///
507    /// ```
508    /// # use std::collections::HashMap;
509    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
510    /// # #[derive(crux_core::macros::Effect)]
511    /// # #[allow(unused)]
512    /// # struct Capabilities { http: crux_http::Http<Event> }
513    /// # type Http = crux_http::command::Http<Effect, Event>;
514    /// let form_data = HashMap::from([
515    ///     ("name", "Alice"),
516    ///     ("location", "UK"),
517    /// ]);
518    /// Http::post("https://httpbin.org/post")
519    ///     .body_form(&form_data)
520    ///     .expect("could not serialize body")
521    ///     .build()
522    ///     .then_send(Event::ReceiveResponse);
523    /// ```
524    pub fn body_form(self, form: &impl Serialize) -> crate::Result<Self> {
525        Ok(self.body(Body::from_form(form)?))
526    }
527
528    /// Set the URL querystring.
529    ///
530    /// # Examples
531    ///
532    /// ```
533    /// # use serde::{Deserialize, Serialize};
534    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
535    /// # #[derive(crux_core::macros::Effect)]
536    /// # #[allow(unused)]
537    /// # struct Capabilities { http: crux_http::Http<Event> }
538    /// # type Http = crux_http::command::Http<Effect, Event>;
539    /// #[derive(Serialize, Deserialize)]
540    /// struct Index {
541    ///     page: u32
542    /// }
543    ///
544    /// let query = Index { page: 2 };
545    /// Http::post("https://httpbin.org/post")
546    ///     .query(&query)
547    ///     .expect("could not serialize query string")
548    ///     .build()
549    ///     .then_send(Event::ReceiveResponse);
550    /// ```
551    ///
552    /// # Errors
553    /// Returns an error if the query string could not be serialized.
554    #[allow(clippy::missing_panics_doc)]
555    pub fn query(mut self, query: &impl Serialize) -> std::result::Result<Self, HttpError> {
556        self.req.as_mut().unwrap().set_query(query)?;
557
558        Ok(self)
559    }
560
561    /// Push middleware onto a per-request middleware stack.
562    ///
563    /// **Important**: Setting per-request middleware incurs extra allocations.
564    /// Creating a `Client` with middleware is recommended.
565    ///
566    /// Client middleware is run before per-request middleware.
567    ///
568    /// See the [middleware] submodule for more information on middleware.
569    ///
570    /// [middleware]: ../middleware/index.html
571    ///
572    /// # Examples
573    ///
574    /// ```
575    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
576    /// # #[derive(crux_core::macros::Effect)]
577    /// # #[allow(unused)]
578    /// # struct Capabilities { http: crux_http::Http<Event> }
579    /// # type Http = crux_http::command::Http<Effect, Event>;
580    /// Http::get("https://httpbin.org/redirect/2")
581    ///     .middleware(crux_http::middleware::Redirect::default())
582    ///     .build()
583    ///     .then_send(Event::ReceiveResponse);
584    /// ```
585    ///
586    #[allow(clippy::missing_panics_doc)]
587    pub fn middleware(mut self, middleware: impl Middleware) -> Self {
588        self.req.as_mut().unwrap().middleware(middleware);
589        self
590    }
591
592    /// Return the constructed `Request` in a [`crux_core::command::RequestBuilder`].
593    ///
594    #[allow(clippy::missing_panics_doc)]
595    #[must_use]
596    pub fn build(
597        self,
598    ) -> command::RequestBuilder<
599        Effect,
600        Event,
601        impl Future<Output = Result<Response<ExpectBody>, HttpError>>,
602    > {
603        let req = self.req.expect("RequestBuilder::build called twice");
604
605        command::RequestBuilder::new(|ctx| async move {
606            let operation = req
607                .into_protocol_request()
608                .await
609                .expect("should be able to convert request to protocol request");
610
611            let result = Command::request_from_shell(operation)
612                .into_future(ctx)
613                .await;
614
615            match result {
616                HttpResult::Ok(response) => Response::<Vec<u8>>::new(response.into())
617                    .await
618                    .and_then(|r| self.expectation.decode(r)),
619                HttpResult::Err(error) => Err(error),
620            }
621        })
622    }
623
624    /// Decode a String from the response body prior to dispatching it to the apps `update`
625    /// function.
626    ///
627    /// # Examples
628    ///
629    /// ```
630    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<String>>) }
631    /// # #[derive(crux_core::macros::Effect)]
632    /// # #[allow(unused)]
633    /// # struct Capabilities { http: crux_http::Http<Event> }
634    /// # type Http = crux_http::command::Http<Effect, Event>;
635    /// Http::post("https://httpbin.org/json")
636    ///     .expect_string()
637    ///     .build()
638    ///     .then_send(Event::ReceiveResponse);
639    /// ```
640    pub fn expect_string(self) -> RequestBuilder<Effect, Event, String> {
641        let expectation = Box::<ExpectString>::default();
642        RequestBuilder {
643            req: self.req,
644            effect: PhantomData,
645            event: PhantomData,
646            expectation,
647        }
648    }
649
650    /// Decode a `T` from a JSON response body prior to dispatching it to the apps `update`
651    /// function.
652    ///
653    /// # Examples
654    ///
655    /// ```
656    /// # use serde::{Deserialize, Serialize};
657    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Slideshow>>) }
658    /// # #[derive(crux_core::macros::Effect)]
659    /// # #[allow(unused)]
660    /// # struct Capabilities { http: crux_http::Http<Event> }
661    /// # type Http = crux_http::command::Http<Effect, Event>;
662    /// #[derive(Deserialize)]
663    /// struct Response {
664    ///     slideshow: Slideshow
665    /// }
666    ///
667    /// #[derive(Deserialize)]
668    /// struct Slideshow {
669    ///     author: String
670    /// }
671    ///
672    /// Http::post("https://httpbin.org/json")
673    ///     .expect_json::<Slideshow>()
674    ///     .build()
675    ///     .then_send(Event::ReceiveResponse);
676    /// ```
677    pub fn expect_json<T>(self) -> RequestBuilder<Effect, Event, T>
678    where
679        T: DeserializeOwned + 'static,
680    {
681        let expectation = Box::<ExpectJson<T>>::default();
682        RequestBuilder {
683            req: self.req,
684            effect: PhantomData,
685            event: PhantomData,
686            expectation,
687        }
688    }
689}
690
691impl<Effect, Event> fmt::Debug for RequestBuilder<Effect, Event> {
692    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
693        fmt::Debug::fmt(&self.req, f)
694    }
695}