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