Crate rustify[−][src]
Expand description
Rustify is a small crate which provides a way to easily scaffold code which communicates with HTTP REST API endpoints. It covers simple cases such as basic GET requests as well as more advanced cases such as sending serialized data and deserializing the result. A derive macro is provided to keep code DRY.
Rustify provides both a trait for implementing API endpoints as well as clients for executing requests against the defined endpoints. Currently, only a client using reqwest::blocking is provided.
Presently, rustify only supports JSON serialization and generally assumes the remote endpoint accepts and responds with JSON.
Architecture
This crate consists of two primary traits:
- The Endpoint trait which represents a remote HTTP REST API endpoint
- The Client trait which is responsible for executing the Endpoint
This provides a loosely coupled interface that allows for multiple implementations of the Client trait which may use different HTTP backends. The Client trait in particular was kept intentionally easy to implement and is only required to send a HTTP request consisting of a URL, method, and body and then return the response consisting of a URL, response code, and response body. The crate currently only provides a blocking client based on the reqwest crate.
The Endpoint trait is what will be most implemented by end-users of this
crate. Since the implementation can be verbose and most functionality can be
defined with very little syntax, a macro is provided via rustify_derive
which should be used for generating implementations of this trait.
Usage
The below example creates a Test
endpoint that, when executed, will send a
GET request to http://!api.com/test/path
and expect an empty response:
use rustify::clients::reqwest::ReqwestClient;
use rustify::endpoint::Endpoint;
use rustify_derive::Endpoint;
use serde::Serialize;
#[derive(Debug, Endpoint, Serialize)]
#[endpoint(path = "test/path")]
struct Test {}
let endpoint = Test {};
let client = ReqwestClient::default("http://!api.com");
let result = endpoint.exec(&client);
Advanced Usage
This examples demonstrates the complexity available using the full suite of options offered by the macro:
use derive_builder::Builder;
use rustify::clients::reqwest::ReqwestClient;
use rustify::{endpoint::{Endpoint, MiddleWare}, errors::ClientError};
use rustify_derive::Endpoint;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_with::skip_serializing_none;
struct Middle {}
impl MiddleWare for Middle {
fn request<E: Endpoint>(
&self,
_: &E,
req: &mut rustify::client::Request,
) -> Result<(), ClientError> {
req.headers
.push(("X-API-Token".to_string(), "mytoken".to_string()));
Ok(())
}
fn response<E: Endpoint>(
&self,
_: &E,
resp: &mut rustify::client::Response,
) -> Result<(), ClientError> {
let err_body = resp.body.clone();
let wrapper: TestWrapper =
serde_json::from_slice(&resp.body).map_err(|e| ClientError::ResponseParseError {
source: Box::new(e),
content: String::from_utf8(err_body).ok(),
})?;
resp.body = wrapper.result.to_string().as_bytes().to_vec();
Ok(())
}
}
#[derive(Deserialize)]
struct TestResponse {
age: u8,
}
#[derive(Deserialize)]
struct TestWrapper {
result: Value,
}
fn test_complex() {
#[skip_serializing_none]
#[derive(Builder, Debug, Default, Endpoint, Serialize)]
#[endpoint(
path = "test/path/{self.name}",
method = "POST",
result = "TestResponse",
builder = "true"
)]
#[builder(setter(into, strip_option), default)]
struct Test {
#[serde(skip)]
name: String,
kind: String,
special: Option<bool>,
optional: Option<String>,
}
let client = ReqwestClient::default("http://!api.com");
let result = Test::builder().name("test").kind("test").exec_mut(&client, &Middle {});
}
Breaking this down:
#[endpoint(
path = "test/path/{self.name}",
method = "POST",
result = "TestResponse",
builder = "true"
)]
- The
path
argument supports basic substitution using curly braces. In this case the final url would behttp://!api.com/test/path/test
. Since thename
field is only used to build the endpoint URL, we add the#[serde(skip)]
attribute to informserde
to not serialize this field when building the request. - The
method
argument specifies the type of the HTTP request. - The
result
argument specifies the type of response that the [exec()][crate::endpoint::Endpoint::execute] method will return. This type must derive serde::Deserialize. - The
builder
argument tells the macro to add some useful functions for when the endpoint is using theBuilder
derive macro from derive_builder. In particular, it adds abuilder()
static method to the base struct and theexec()
methods to the generatedTestBuilder
struct which automatically callsbuild()
onTestBuilder
and then executes the result. This allows for concise calls like this:Test::builder().name("test").kind("test").exec(&client);
Endpoints contain two methods for executing requests; in this example the
execute_m()
variant is being used which allows passing an instance of an
object that implements MiddleWare
which can be used to mutate the request
and response object respectively. Here the an arbitrary request header
containing a fictitious API token is being injected and the response has a
wrapper removed before final parsing.
This example also demonstrates a common pattern of using
skip_serializing_none macro to force serde
to not serialize fields of
type Option::None
. When combined with the default
parameter offered by
derive_builder the result is an endpoint which can have required and/or
optional fields as needed and which don’t get serialized when not specified
when building. For example:
// Errors, `kind` field is required
let result = Test::builder().name("test").exec(&client);
// Produces POST http://!api.com/test/path/test {"kind": "test"}
let result = Test::builder().name("test").kind("test").exec(&client);
// Produces POST http://!api.com/test/path/test {"kind": "test", "optional": "yes"}
let result = Test::builder().name("test").kind("test").optional("yes").exec&client);
Error Handling
All errors generated by this crate are wrapped in the ClientError enum provided by the crate.