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}