http_typed/lib.rs
1//! HTTP client supporting custom request and response types. Pass any type into
2//! a `send` function or method, and it will return a result of your desired
3//! response type. `send` handles request serialization, http messaging, and
4//! response deserialization.
5//!
6//! To keep this crate simple, it is is oriented towards a specific but very
7//! common pattern. If your use case meets the following conditions, this crate
8//! will work for you:
9//! 1. request-response communication
10//! 2. async rust functions
11//! 3. communicate over http (uses reqwest under the hood)
12//! 4. http body is serialized as json
13//! 5. status codes outside the 200 range are considered errors
14//! 6. request and response types must be serializable and deserializable using
15//! serde
16//! 7. the path and HTTP method can be determined from the concrete rust type
17//! used for the request
18//!
19//! ## Usage
20//!
21//! ### Typical
22//!
23//! To use this library, your request and response types must implement
24//! serde::Serialize and serde::Deserialize, respectively.
25//!
26//! To take full advantage of all library features, you can implement `Request`
27//! for each of your request types, instantiate a `Client`, and then you can
28//! simply invoke `Client::send` to send requests.
29//!
30//! ```rust
31//! let client = Client::new("http://example.com");
32//! let response = client.send(MyRequest::new()).await?;
33//! ```
34//!
35//! ### Basic
36//!
37//! If you don't want to implement Request or create a Client, the most manual
38//! and basic way to use this library is by using `send_custom`.
39//!
40//! ```rust
41//! let my_response: MyResponse = send_custom(
42//! "http://example.com/path/to/my/request/",
43//! HttpMethod::Get,
44//! MyRequest::new()
45//! )
46//! .await?;
47//! ```
48//!
49//! ### Client
50//!
51//! One downside of the `send_custom` (and `send`) *function* is that it
52//! instantiates a client for every request, which is expensive. To improve
53//! performance, you can use the `Client::send_custom` (and `Client::send`)
54//! *method* instead to re-use an existing client for every request.
55//!
56//! ```rust
57//! let client = Client::default();
58//! let my_response: MyResponse = client.send_custom(
59//! "http://example.com/path/to/my/request/",
60//! HttpMethod::Get,
61//! MyRequest::new()
62//! )
63//! .await?;
64//! ```
65//!
66//! ### Request
67//!
68//! You may also prefer not to specify metadata about the request every time you
69//! send a request, since these things will likely be the same for every request
70//! of this type. Describe the request metadata in the type system by
71//! implementing the Request trait.
72//!
73//! ```rust
74//! pub trait Request {
75//! type Response;
76//! fn method(&self) -> HttpMethod;
77//! fn path(&self) -> String;
78//! }
79//! ```
80//!
81//! This increases design flexibility and reduces boilerplate. See the [API
82//! Client Design](#api-client-design) section below for an explanation.
83//!
84//! If you do not control the crate with the request and response structs, you
85//! can implement any traits for them using the newtype pattern, or with a
86//! reusable generic wrapper struct.
87//!
88//! After implementing this trait, you can use the send function and method,
89//! which requires the base url to be included, instead of the full url. All
90//! other information about how to send the request and response is implied by
91//! the type of the input. This still creates a client on every request, so the
92//! performance is not optimal if you are sending multiple requests.
93//!
94//! ```rust
95//! let my_response = send("http://example.com", MyRequest::new()).await?;
96//! // The type of my_response is determined by the trait's associated type.
97//! // It does not need to be inferrable from the calling context.
98//! return my_response.some_field
99//! ```
100//!
101//! If you want to send multiple requests, or if you don't want to include the
102//! base url when calling `send`, instantiate a Client:
103//!
104//! ```rust
105//! let client = Client::new("http://example.com");
106//! let my_response = client.send(MyRequest::new()).await?;
107//! ```
108//!
109//! ### Request groups
110//!
111//! You can also define request groups. This defines a client type that is
112//! explicit about exactly which requests it can handle. The code will not
113//! compile if you try to send a request with the wrong client.
114//!
115//! ```rust
116//! request_group!(MyApi { MyRequest1, MyRequest2 });
117//! ```
118//! ```rust
119//! let my_client = Client::<MyApi>::new("http://example.com");
120//! let my_response1 = my_client.send(MyRequest1::new()).await?; // works
121//! let other_response = my_client.send(OtherRequest::new()).await?; // does not compile
122//! ```
123//!
124//! ### send_to
125//!
126//! If you want to restrict the request group, but still want to include the url
127//! for every call to `send`, `MyClient` has a `send_to` method that can be used
128//! with the default client to specify the url at the call-site.
129//! ```rust
130//! let my_client = Client::<MyApi>::default();
131//! let my_response2 = my_client.send_to("http://example.com", MyRequest2::new()).await?; // works
132//! let other_response = my_client.send_to("http://example.com", OtherRequest::new()).await?; // does not compile
133//! ```
134//!
135//! The send_to method can also be used to insert a string after the base_url
136//! and before the Request path.
137//!
138//! ```rust
139//! let my_client = Client::new("http://example.com");
140//! let my_response = my_client.send_to("/api/v2", MyRequest::new()).await?;
141//! ```
142//!
143//! ## Cargo Features
144//!
145//! Typically, the default features should be fine:
146//!
147//! ```toml
148//! http-typed = "0.3"
149//! ```
150//!
151//! The default features include the full Client implementation, and depend on
152//! system tls libraries.
153//!
154//! All features:
155//!
156//! - **default** = ["client", "native-tls"]
157//! - **client**: Includes the Client implementation described above and depends
158//! on reqwest.
159//! - **native-tls**: Depend on dynamically linked system tls libraries.
160//! - **rustls-tls**: Statically link all tls dependencies with webpki, no tls
161//! is required in the system.
162//!
163//!
164//! ### No system tls? Use rustls
165//!
166//! To statically link the tls dependencies, use this:
167//!
168//! ```toml
169//! http-typed = { version = "0.3", default-features = false, features = ["client", "rustls-tls"] }
170//! ```
171//!
172//! ### No Client
173//!
174//! If you'd like to exclude the `Client` implementation and all of its
175//! dependencies on reqwest and tls libraries, use this:
176//!
177//! ```toml
178//! http-typed = { version = "0.3", default-features = false }
179//! ```
180//!
181//! This allows you, as a server developer, to exclude unnecessary dependencies
182//! from your server. For example, you may have an API crate with all the
183//! request and response structs, which you both import in the server and also
184//! make available to clients. You can feature gate the client in your API
185//! library:
186//!
187//! ```toml
188//! # api library's Cargo.toml
189//!
190//! [features]
191//! default = ["client"]
192//! client = ["http-typed/client"]
193//! ```
194//!
195//! ...and then you can disable it in the server's Cargo.toml. Something like
196//! this:
197//!
198//! ```toml
199//! # server binary's Cargo.toml
200//!
201//! [dependencies]
202//! my-api-client = { path = "../client", default-features = false }
203//! ```
204//!
205//! A similar pattern can be used to give clients the option between native-tls
206//! and rustls-tls.
207//!
208//! ## API Client Design
209//! Normally, a you might implement a custom client struct to connect to an API,
210//! including a custom method for every request. In doing so, you've forced all
211//! dependents of the API to make a choice between two options:
212//! 1. use the specific custom client struct that was already implemented,
213//! accepting any issues with it.
214//! 2. implement a custom client from scratch, re-writing and maintaining all
215//! the details about each request, including what http method to use, what
216//! path to use, how to serialize/deserialized the message, etc.
217//!
218//! Instead, you can describe the metadata through trait definitions for
219//! ultimate flexibility, without locking dependents into a client
220//! implementation or needing to implement any custom clients structs.
221//! Dependents of the API now have better options:
222//! 1. use the Client struct provided by http-typed, accepting any issues with
223//! it. This is easier for you to support because you don't need to worry
224//! about implementation details of sending requests in general. You can just
225//! export or alias the `Client` struct.
226//! 2. implement a custom client that can generically handle types implementing
227//! Request by using the data returned by their methods. This is easier for
228//! dependents because they don't need to write any request-specific code.
229//! The Request trait exposes that information without locking them into a
230//! client implementation. Only a single generic request handler is
231//! sufficient.
232//!
233//! ## Other use cases
234//! If your use case does not meet some of the conditions 2-7 described in the
235//! introduction, you'll find my other crate useful, which individually
236//! generalizes each of those, allowing any of them to be individually
237//! customized with minimal boilerplate. It is currently a work in progress, but
238//! almost complete. This crate and that crate will be source-code-compatible,
239//! meaning the other crate can be used as a drop-in replacement of this one
240//! without changing any code, just with more customization available.
241
242#[cfg(feature = "client")]
243mod client;
244
245use std::convert::Infallible;
246
247#[cfg(feature = "client")]
248pub use client::*;
249
250pub trait Request: Sized {
251 // TODO: use when stable: https://github.com/rust-lang/rust/issues/29661
252 /// Specify a pre-defined approach to serialize a request body. For example:
253 /// - SerdeJson
254 /// - NoBody
255 type Serializer: SerializeBody<Self>;
256
257 /// Type to deserialize from the http response body
258 type Response;
259
260 /// HTTP method that the request will be sent with
261 fn method(&self) -> HttpMethod;
262
263 /// String to appended to the end of url when sending this request.
264 fn path(&self) -> String;
265}
266
267pub struct SerdeJson;
268pub struct NoBody;
269
270pub trait SerializeBody<T> {
271 type Error;
272 fn serialize_body(request: &T) -> Result<Vec<u8>, Self::Error>;
273}
274
275impl<T> SerializeBody<T> for SerdeJson
276where
277 T: serde::Serialize,
278{
279 type Error = serde_json::error::Error;
280
281 fn serialize_body(request: &T) -> Result<Vec<u8>, Self::Error> {
282 Ok(serde_json::to_string(&request)?.into_bytes())
283 }
284}
285
286impl<T> SerializeBody<T> for NoBody {
287 type Error = Infallible;
288
289 fn serialize_body(_: &T) -> Result<Vec<u8>, Self::Error> {
290 Ok(vec![])
291 }
292}
293
294/// Define a request group to constrain which requests can be used with a client.
295/// ```ignore
296/// request_group!(MyApi { MyRequest1, MyRequest2 });
297/// ```
298#[macro_export]
299macro_rules! request_group {
300 ($viz:vis $Name:ident { $($Request:ident),*$(,)? }) => {
301 $viz struct $Name;
302 $(impl $crate::InRequestGroup<$Name> for $Request {})*
303 };
304}
305
306/// Indicates that a request is part of a request group. If you use the
307/// request_group macro to define the group, it will handle the implementation
308/// of this trait automatically.
309pub trait InRequestGroup<Group> {}
310
311/// The default group. All requests are in this group.
312pub struct All;
313impl<T> InRequestGroup<All> for T {}
314
315#[derive(Debug, Clone, Copy)]
316pub enum HttpMethod {
317 Options,
318 Get,
319 Post,
320 Put,
321 Delete,
322 Head,
323 Trace,
324 Connect,
325 Patch,
326}