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