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