Crates.io | http-typed |
lib.rs | http-typed |
version | 0.4.0 |
source | src |
created_at | 2023-07-31 22:40:39.56872 |
updated_at | 2023-08-28 22:47:58.794827 |
description | HTTP client supporting custom request and response types. |
homepage | |
repository | https://github.com/dnut/http-typed |
max_upload_size | |
id | 931268 |
size | 31,282 |
HTTP client supporting custom request and response types. Pass any type into a send
function or method, and it will return a result of your desired response type. send
handles request serialization, http messaging, and response deserialization.
To keep this crate simple, it is is oriented towards a specific but very common pattern. If your use case meets the following conditions, this crate will work for you:
To use this library, your request and response types must implement serde::Serialize and serde::Deserialize, respectively.
To take full advantage of all library features, you can implement Request
for each of your request types, instantiate a Client
, and then you can simply invoke Client::send
to send requests.
let client = Client::new("http://example.com");
let response = client.send(MyRequest::new()).await?;
If you don't want to implement Request or create a Client, the most manual and basic way to use this library is by using send_custom
.
let my_response: MyResponse = send_custom(
"http://example.com/path/to/my/request/",
HttpMethod::Get,
MyRequest::new()
)
.await?;
One downside of the send_custom
(and send
) function is that it instantiates a client for every request, which is expensive. To improve performance, you can use the Client::send_custom
(and Client::send
) method instead to re-use an existing client for every request.
let client = Client::default();
let my_response: MyResponse = client.send_custom(
"http://example.com/path/to/my/request/",
HttpMethod::Get,
MyRequest::new()
)
.await?;
You may also prefer not to specify metadata about the request every time you send a request, since these things will likely be the same for every request of this type. Describe the request metadata in the type system by implementing the Request trait.
pub trait Request {
type Response;
fn method(&self) -> HttpMethod;
fn path(&self) -> String;
}
This increases design flexibility and reduces boilerplate. See the API Client Design section below for an explanation.
If you do not control the crate with the request and response structs, you can implement any traits for them using the newtype pattern, or with a reusable generic wrapper struct.
After implementing this trait, you can use the send function and method, which requires the base url to be included, instead of the full url. All other information about how to send the request and response is implied by the type of the input. This still creates a client on every request, so the performance is not optimal if you are sending multiple requests.
let my_response = send("http://example.com", MyRequest::new()).await?;
// The type of my_response is determined by the trait's associated type.
// It does not need to be inferrable from the calling context.
return my_response.some_field
If you want to send multiple requests, or if you don't want to include the base url when calling send
, instantiate a Client:
let client = Client::new("http://example.com");
let my_response = client.send(MyRequest::new()).await?;
You can also define request groups. This defines a client type that is explicit about exactly which requests it can handle. The code will not compile if you try to send a request with the wrong client.
request_group!(MyApi { MyRequest1, MyRequest2 });
let my_client = Client::<MyApi>::new("http://example.com");
let my_response1 = my_client.send(MyRequest1::new()).await?; // works
let other_response = my_client.send(OtherRequest::new()).await?; // does not compile
If you want to restrict the request group, but still want to include the url for every call to send
, MyClient
has a send_to
method that can be used with the default client to specify the url at the call-site.
let my_client = Client::<MyApi>::default();
let my_response2 = my_client.send_to("http://example.com", MyRequest2::new()).await?; // works
let other_response = my_client.send_to("http://example.com", OtherRequest::new()).await?; // does not compile
The send_to method can also be used to insert a string after the base_url and before the Request path.
let my_client = Client::new("http://example.com");
let my_response = my_client.send_to("/api/v2", MyRequest::new()).await?;
Typically, the default features should be fine:
http-typed = "0.3"
The default features include the full Client implementation, and depend on system tls libraries.
All features:
default = ["client", "native-tls"]
client: Includes the Client implementation described above and depends on reqwest.
native-tls: Depend on dynamically linked system tls libraries.
rustls-tls: Statically link all tls dependencies with webpki, no tls is required in the system.
To statically link the tls dependencies, use this:
http-typed = { version = "0.3", default-features = false, features = ["client", "rustls-tls"] }
If you'd like to exclude the Client
implementation and all of its dependencies on reqwest and tls libraries, use this:
http-typed = { version = "0.3", default-features = false }
This allows you, as a server developer, to exclude unnecessary dependencies from your server. For example, you may have an API crate with all the request and response structs, which you both import in the server and also make available to clients. You can feature gate the client in your API library:
# api library's Cargo.toml
[features]
default = ["client"]
client = ["http-typed/client"]
...and then you can disable it in the server's Cargo.toml. Something like this:
# server binary's Cargo.toml
[dependencies]
my-api-client = { path = "../client", default-features = false }
A similar pattern can be used to give clients the option between native-tls and rustls-tls.
Normally, a you might implement a custom client struct to connect to an API, including a custom method for every request. In doing so, you've forced all dependents of the API to make a choice between two options:
Instead, you can describe the metadata through trait definitions for ultimate flexibility, without locking dependents into a client implementation or needing to implement any custom clients structs. Dependents of the API now have better options:
Client
struct.If your use case does not meet some of the conditions 2-7 described in the introduction, you'll find my other crate useful, which individually generalizes each of those, allowing any of them to be individually customized with minimal boilerplate. It is currently a work in progress, but almost complete. This crate and that crate will be source-code-compatible, meaning the other crate can be used as a drop-in replacement of this one without changing any code, just with more customization available.