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