Skip to main content

crux_http/
lib.rs

1#![allow(clippy::unsafe_derive_deserialize)]
2//! A HTTP client for use with Crux
3//!
4//! `crux_http` allows Crux apps to make HTTP requests by asking the Shell to perform them.
5//!
6//! This is still work in progress and large parts of HTTP are not yet supported.
7//!
8//! # Errors and rejections
9//!
10//! A request resolves to a [`Result<Response<T>>`](Result), and a 4xx or 5xx response is
11//! on the **`Err`** side of it: `crux_http` turns those into
12//! [`HttpError::Http`] — headers and body included — before the event reaches the app, so
13//! [`Response::status`] is never an error status. Read the server's own explanation of a
14//! rejection with [`HttpError::body`] or [`HttpError::body_json`], and whatever policy it
15//! put in the headers (`Retry-After`, `WWW-Authenticate`) with [`HttpError::header`].
16//! [`Response`] documents the shape to write, and [`testing`] the two values a test
17//! should build.
18//!
19//! `HttpError::Http` means a rejection and nothing else, so [`HttpError::code`] returning
20//! `Some` is the test for one. The crate's own failures are separate variants —
21//! [`HttpError::Json`], [`HttpError::BodyAlreadyTaken`], [`HttpError::InvalidStatusCode`] —
22//! as are the shell's ([`HttpError::Url`], [`HttpError::Io`], [`HttpError::Timeout`]).
23// #![warn(missing_docs)]
24
25mod config;
26mod error;
27mod expect;
28mod request;
29mod request_builder;
30mod response;
31
32mod body;
33pub mod client;
34pub mod command;
35#[cfg(feature = "http-types")]
36mod compat;
37pub mod middleware;
38pub mod protocol;
39pub mod testing;
40
41use std::marker::PhantomData;
42
43pub use crate::body::Body;
44pub use http;
45pub use http::Method;
46pub use mime;
47pub use url::Url;
48
49#[cfg(feature = "http-types")]
50pub use http_types;
51
52pub use crate::protocol::{HttpRequest, HttpResponse};
53
54pub use self::{config::Config, error::HttpError, request::Request};
55pub use response::Response;
56
57pub use request_builder::RequestBuilder;
58pub use response::RawResponse;
59
60use client::Client;
61
62pub type Result<T> = std::result::Result<T, HttpError>;
63
64pub struct Http<Effect, Event> {
65    effect: PhantomData<Effect>,
66    event: PhantomData<Event>,
67}
68
69impl<Effect, Event> Http<Effect, Event>
70where
71    Effect: Send + From<crux_core::Request<HttpRequest>> + 'static,
72    Event: Send + 'static,
73{
74    /// Instruct the Shell to perform a HTTP GET request to the provided `url`.
75    ///
76    /// The request can be configured via associated functions on the returned
77    /// [`RequestBuilder`] and then converted to a [`Command`](crux_core::Command)
78    /// with [`RequestBuilder::build`].
79    ///
80    /// # Panics
81    ///
82    /// This will panic if a malformed URL is passed.
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// # use crux_core::macros::effect;
88    /// # use crux_http::HttpRequest;
89    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<String>>) }
90    /// # #[effect]
91    /// # #[allow(unused)]
92    /// # enum Effect { Http(HttpRequest) }
93    /// # type Http = crux_http::command::Http<Effect, Event>;
94    /// Http::get("https://httpbin.org/get")
95    ///     .expect_string()
96    ///     .build()
97    ///     .then_send(Event::ReceiveResponse);
98    /// ```
99    pub fn get(url: impl AsRef<str>) -> command::RequestBuilder<Effect, Event> {
100        command::RequestBuilder::new(Method::GET, url.as_ref().parse().unwrap())
101    }
102
103    /// Instruct the Shell to perform a HTTP HEAD request to the provided `url`.
104    ///
105    /// The request can be configured via associated functions on the returned
106    /// [`RequestBuilder`] and then converted to a [`Command`](crux_core::Command)
107    /// with [`RequestBuilder::build`].
108    ///
109    /// # Panics
110    ///
111    /// This will panic if a malformed URL is passed.
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// # use crux_core::macros::effect;
117    /// # use crux_http::HttpRequest;
118    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
119    /// # #[effect]
120    /// # #[allow(unused)]
121    /// # enum Effect { Http(HttpRequest) }
122    /// # type Http = crux_http::command::Http<Effect, Event>;
123    /// Http::head("https://httpbin.org/get")
124    ///     .build()
125    ///     .then_send(Event::ReceiveResponse);
126    pub fn head(url: impl AsRef<str>) -> command::RequestBuilder<Effect, Event> {
127        command::RequestBuilder::new(Method::HEAD, url.as_ref().parse().unwrap())
128    }
129
130    /// Instruct the Shell to perform a HTTP POST request to the provided `url`.
131    ///
132    /// The request can be configured via associated functions on the returned
133    /// [`RequestBuilder`] and then converted to a [`Command`](crux_core::Command)
134    /// with [`RequestBuilder::build`].
135    ///
136    /// # Panics
137    ///
138    /// This will panic if a malformed URL is passed.
139    ///
140    /// # Examples
141    ///
142    /// ```
143    /// # use crux_core::macros::effect;
144    /// # use crux_http::HttpRequest;
145    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
146    /// # #[effect]
147    /// # #[allow(unused)]
148    /// # enum Effect { Http(HttpRequest) }
149    /// # type Http = crux_http::command::Http<Effect, Event>;
150    /// Http::post("https://httpbin.org/post")
151    ///     .body_bytes(b"hello_world".to_owned())
152    ///     .build()
153    ///     .then_send(Event::ReceiveResponse);
154    pub fn post(url: impl AsRef<str>) -> command::RequestBuilder<Effect, Event> {
155        command::RequestBuilder::new(Method::POST, url.as_ref().parse().unwrap())
156    }
157
158    /// Instruct the Shell to perform a HTTP PUT request to the provided `url`.
159    ///
160    /// The request can be configured via associated functions on the returned
161    /// [`RequestBuilder`] and then converted to a [`Command`](crux_core::Command)
162    /// with [`RequestBuilder::build`].
163    ///
164    /// # Panics
165    ///
166    /// This will panic if a malformed URL is passed.
167    ///
168    /// # Examples
169    ///
170    /// ```
171    /// # use crux_core::macros::effect;
172    /// # use crux_http::HttpRequest;
173    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
174    /// # #[effect]
175    /// # #[allow(unused)]
176    /// # enum Effect { Http(HttpRequest) }
177    /// # type Http = crux_http::command::Http<Effect, Event>;
178    /// Http::put("https://httpbin.org/put")
179    ///     .body_string("hello_world".to_string())
180    ///     .build()
181    ///     .then_send(Event::ReceiveResponse);
182    pub fn put(url: impl AsRef<str>) -> command::RequestBuilder<Effect, Event> {
183        command::RequestBuilder::new(Method::PUT, url.as_ref().parse().unwrap())
184    }
185
186    /// Instruct the Shell to perform a HTTP DELETE request to the provided `url`.
187    ///
188    /// The request can be configured via associated functions on the returned
189    /// [`RequestBuilder`] and then converted to a [`Command`](crux_core::Command)
190    /// with [`RequestBuilder::build`].
191    ///
192    /// # Panics
193    ///
194    /// This will panic if a malformed URL is passed.
195    ///
196    /// # Examples
197    ///
198    /// ```
199    /// # use crux_core::macros::effect;
200    /// # use crux_http::HttpRequest;
201    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
202    /// # #[effect]
203    /// # #[allow(unused)]
204    /// # enum Effect { Http(HttpRequest) }
205    /// # type Http = crux_http::command::Http<Effect, Event>;
206    /// Http::delete("https://httpbin.org/delete")
207    ///     .build()
208    ///     .then_send(Event::ReceiveResponse);
209    pub fn delete(url: impl AsRef<str>) -> command::RequestBuilder<Effect, Event> {
210        command::RequestBuilder::new(Method::DELETE, url.as_ref().parse().unwrap())
211    }
212
213    /// Instruct the Shell to perform a HTTP PATCH request to the provided `url`.
214    ///
215    /// The request can be configured via associated functions on the returned
216    /// [`RequestBuilder`] and then converted to a [`Command`](crux_core::Command)
217    /// with [`RequestBuilder::build`].
218    ///
219    /// # Panics
220    ///
221    /// This will panic if a malformed URL is passed.
222    ///
223    /// # Examples
224    ///
225    /// ```
226    /// # use crux_core::macros::effect;
227    /// # use crux_http::HttpRequest;
228    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
229    /// # #[effect]
230    /// # #[allow(unused)]
231    /// # enum Effect { Http(HttpRequest) }
232    /// # type Http = crux_http::command::Http<Effect, Event>;
233    /// Http::patch("https://httpbin.org/patch")
234    ///     .body_form(&[("name", "Alice")]).unwrap()
235    ///     .build()
236    ///     .then_send(Event::ReceiveResponse);
237    pub fn patch(url: impl AsRef<str>) -> command::RequestBuilder<Effect, Event> {
238        command::RequestBuilder::new(Method::PATCH, url.as_ref().parse().unwrap())
239    }
240
241    /// Instruct the Shell to perform a HTTP OPTIONS request to the provided `url`.
242    ///
243    /// The request can be configured via associated functions on the returned
244    /// [`RequestBuilder`] and then converted to a [`Command`](crux_core::Command)
245    /// with [`RequestBuilder::build`].
246    ///
247    /// # Panics
248    ///
249    /// This will panic if a malformed URL is passed.
250    ///
251    /// # Examples
252    ///
253    /// ```
254    /// # use crux_core::macros::effect;
255    /// # use crux_http::HttpRequest;
256    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
257    /// # #[effect]
258    /// # #[allow(unused)]
259    /// # enum Effect { Http(HttpRequest) }
260    /// # type Http = crux_http::command::Http<Effect, Event>;
261    /// Http::options("https://httpbin.org/get")
262    ///     .build()
263    ///     .then_send(Event::ReceiveResponse);
264    pub fn options(url: impl AsRef<str>) -> command::RequestBuilder<Effect, Event> {
265        command::RequestBuilder::new(Method::OPTIONS, url.as_ref().parse().unwrap())
266    }
267
268    /// Instruct the Shell to perform a HTTP TRACE request to the provided `url`.
269    ///
270    /// The request can be configured via associated functions on the returned
271    /// [`RequestBuilder`] and then converted to a [`Command`](crux_core::Command)
272    /// with [`RequestBuilder::build`].
273    ///
274    /// # Panics
275    ///
276    /// This will panic if a malformed URL is passed.
277    ///
278    /// # Examples
279    ///
280    /// ```
281    /// # use crux_core::macros::effect;
282    /// # use crux_http::HttpRequest;
283    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
284    /// # #[effect]
285    /// # #[allow(unused)]
286    /// # enum Effect { Http(HttpRequest) }
287    /// # type Http = crux_http::command::Http<Effect, Event>;
288    /// Http::trace("https://httpbin.org/get")
289    ///     .build()
290    ///     .then_send(Event::ReceiveResponse);
291    pub fn trace(url: impl AsRef<str>) -> command::RequestBuilder<Effect, Event> {
292        command::RequestBuilder::new(Method::TRACE, url.as_ref().parse().unwrap())
293    }
294
295    /// Instruct the Shell to perform a HTTP CONNECT request to the provided `url`.
296    ///
297    /// The request can be configured via associated functions on the returned
298    /// [`RequestBuilder`] and then converted to a [`Command`](crux_core::Command)
299    /// with [`RequestBuilder::build`].
300    ///
301    /// # Panics
302    ///
303    /// This will panic if a malformed URL is passed.
304    ///
305    /// # Examples
306    ///
307    /// ```
308    /// # use crux_core::macros::effect;
309    /// # use crux_http::HttpRequest;
310    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
311    /// # #[effect]
312    /// # #[allow(unused)]
313    /// # enum Effect { Http(HttpRequest) }
314    /// # type Http = crux_http::command::Http<Effect, Event>;
315    /// Http::connect("https://httpbin.org/get")
316    ///     .build()
317    ///     .then_send(Event::ReceiveResponse);
318    pub fn connect(url: impl AsRef<str>) -> command::RequestBuilder<Effect, Event> {
319        command::RequestBuilder::new(Method::CONNECT, url.as_ref().parse().unwrap())
320    }
321
322    /// Instruct the Shell to perform an HTTP request to the provided `url`.
323    ///
324    /// The request can be configured via associated functions on the returned
325    /// [`RequestBuilder`] and then converted to a [`Command`](crux_core::Command)
326    /// with [`RequestBuilder::build`].
327    ///
328    /// # Panics
329    ///
330    /// This will panic if a malformed URL is passed.
331    ///
332    /// # Examples
333    ///
334    /// ```
335    /// # use crux_http::Method;
336    /// # use crux_core::macros::effect;
337    /// # use crux_http::HttpRequest;
338    /// # enum Event { ReceiveResponse(crux_http::Result<crux_http::Response<Vec<u8>>>) }
339    /// # #[effect]
340    /// # #[allow(unused)]
341    /// # enum Effect { Http(HttpRequest) }
342    /// # type Http = crux_http::command::Http<Effect, Event>;
343    /// Http::request(Method::POST, "https://httpbin.org/post".parse().unwrap())
344    ///     .body_form(&[("name", "Alice")]).unwrap()
345    ///     .build()
346    ///     .then_send(Event::ReceiveResponse);
347    pub fn request(method: Method, url: Url) -> command::RequestBuilder<Effect, Event> {
348        command::RequestBuilder::new(method, url)
349    }
350}