Crate pretend[−][src]
Expand description
pretend
HTTP client
pretend
is a modular, Feign-inspired, HTTP client based on macros. It’s goal is to decouple
the definition of a REST API from it’s implementation.
Some features:
- Declarative
- Support Asynchronous and blocking requests
- HTTP client agnostic
- JSON support thanks to serde
Getting started
A REST API is described by annotating a trait:
use pretend::{pretend, request, Result}; #[pretend] trait HttpBin { #[request(method = "POST", path = "/anything")] async fn post_anything(&self, body: &'static str) -> Result<String>; }
Under the hood, pretend
will implement this trait for Pretend
. An instance of this
struct can be constructed by passing a client implementation, and the REST API’s base url. In
the following example, we are using the reqwest
based client.
use pretend::{Pretend, Url}; use pretend_reqwest::Client; let client = Client::default(); let url = Url::parse("https://httpbin.org").unwrap(); let pretend = Pretend::for_client(client).with_url(url); let response = pretend.post_anything("hello").await.unwrap(); assert!(response.contains("hello"));
Sending headers, query parameters and bodies
Headers are provided as attributes using header
.
use pretend::{header, pretend, request, Result}; #[pretend] trait HttpBin { #[request(method = "GET", path = "/get")] #[header(name = "X-Test-Header-1", value = "abc")] #[header(name = "X-Test-Header-2", value = "other")] async fn get_with_headers(&self, value: i32, custom: &str) -> Result<()>; }
Query parameters and bodies are provided as method parameters. Body type is guessed based on the parameter name:
- Parameter
body
will be sent as raw bytes. - Parameter
form
will be serialized as form-encoded usingserde
. - Parameter
json
will be serialized as JSON usingserde
.
Query parameter is passed with the query
parameter. It is also serialized using serde
.
use pretend::{pretend, request, Json, Result}; use serde::Serialize; #[derive(Serialize)] struct Data { value: i32, } #[pretend] trait HttpBin { #[request(method = "POST", path = "/anything")] async fn post_bytes(&self, body: Vec<u8>) -> Result<()>; #[request(method = "POST", path = "/anything")] async fn post_string(&self, body: &'static str) -> Result<()>; #[request(method = "POST", path = "/anything")] async fn post_with_query_params(&self, query: &Data) -> Result<()>; #[request(method = "POST", path = "/anything")] async fn post_json(&self, json: &Data) -> Result<()>; }
Handling responses
pretend
support a wide range of response types, based on the return type of the method.
The body can be returned as a Vec<u8>
, a string or as JSON by using the Json
wrapper
type. The unit type ()
can also be used if the body should be discarded.
JsonResult
is also offered as a convenience type. It will deserialize into a value type
or an error type depending on the HTTP status code.
When retrieving body alone, an HTTP error will cause the method to return an error. It is
possible to prevent the method to fail and access the HTTP status code by wrapping these
types inside a Response
. This also allows accessing response headers.
use pretend::{pretend, request, Json, JsonResult, Response, Result}; use serde::Deserialize; #[derive(Deserialize)] struct Data { value: i32, } #[derive(Deserialize)] struct Error { error: String, } #[pretend] trait HttpBin { #[request(method = "POST", path = "/anything")] async fn read_bytes(&self) -> Result<Vec<u8>>; #[request(method = "POST", path = "/anything")] async fn read_string(&self) -> Result<String>; #[request(method = "POST", path = "/anything")] async fn read_json(&self) -> Result<Json<Data>>; #[request(method = "POST", path = "/anything")] async fn read_json_result(&self) -> Result<JsonResult<Data, Error>>; #[request(method = "POST", path = "/anything")] async fn read_status(&self) -> Result<Response<()>>; }
Templating
Request paths and headers support templating. A value between braces will be replaced by
a parameter with the same name. The replacement is done with format!
, meaning that
any type that implement Display
is supported.
use pretend::{header, pretend, request, Json, Pretend, Result}; use pretend_reqwest::Client; use serde::Deserialize; use std::collections::HashMap; #[derive(Deserialize)] struct Data { url: String, headers: HashMap<String, String>, } #[pretend] trait HttpBin { #[request(method = "POST", path = "/{path}")] #[header(name = "X-{header}", value = "{value}$")] async fn read(&self, path: &str, header: &str, value: i32) -> Result<Json<Data>>; } let client = Client::default(); let url = Url::parse("https://httpbin.org").unwrap(); let pretend = Pretend::for_client(client).with_url(url); let response = pretend.read("anything", "My-Header", 123).await.unwrap(); let data = response.value(); assert_eq!(data.url, "https://httpbin.org/anything"); assert_eq!(*data.headers.get("X-My-Header").unwrap(), "123$".to_string());
Blocking requests
When all methods in the pretend
-annotated trait are async, pretend
will generate
an async implementation. To generate a blocking implementation, simply remove the async
.
Blocking implementations will need a blocking client implementation.
use pretend::{pretend, request, Pretend, Result, Url}; use pretend_reqwest::BlockingClient; #[pretend] trait HttpBin { #[request(method = "POST", path = "/anything")] fn post_anything(&self, body: &'static str) -> Result<String>; } let client = BlockingClient::default(); let url = Url::parse("https://httpbin.org").unwrap(); let pretend = Pretend::for_client(client).with_url(url); let response = pretend.post_anything("hello").unwrap(); assert!(response.contains("hello"));
Non-Send implementation
Today, Rust do not support futures in traits. pretend
uses async_trait
to workaround
that limitation. By default, async_trait
adds the Send
bound to futures. This implies
that Pretend
itself is Send
and Sync
, and implies that the client implementation it uses
is also Send
and Sync
.
However, some clients are not thread-safe, and cannot be shared between threads. To use
these clients with Pretend
, you have toopt-out from the Send
constraint on returned
futures by using #[pretend(?Send)]
. This is similar to what is done in async_trait
.
Clients implementations that are not thread-safe are usually called “local clients”.
Available client implementations
pretend
can be used with the following HTTP clients
Implementing a pretend
HTTP client
pretend
clients wraps HTTP clients from other crates. They allow Pretend
to execute
HTTP requests. See the client module level documentation for more information about
how to implement a client.
URL resolvers
pretend
uses URL resolvers to resolve a full URL from the path in request
. By default
the URL resolver will simply append the path to a base URL. More advanced resolvers can
be implemented with the resolver module.
Examples
More examples are available in the examples folder.
MSRV
MSRV for the pretend
ecosystem is Rust 1.44.
The future
Here is a quick roadmap
- Introduce more attributes to mark method parameters (body, json, params)
- Introduce interceptors
Re-exports
pub use http; | |
pub use serde; | |
pub use url; |
Modules
client | Client traits |
resolver | URL resolver |
Structs
HeaderMap | A set of HTTP headers |
Json | JSON body |
Pretend | The pretend HTTP client |
Response | Response type |
StatusCode | An HTTP status code ( |
Url | A parsed URL record. |
Enums
Error | Pretend errors |
JsonResult | JSON result |
Type Definitions
Result | Pretend Result type |
Attribute Macros
header | |
pretend | |
request |