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}