rustify

A Rust crate which provides an abstraction layer over HTTP REST API endpoints

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. This crate targets async first, however, blocking support can be enabled with the blocking feature flag. Additionally, the Endpoint trait offers both async and blocking variants of each execution method (blocking methods are available when flag is enabled).

Presently, rustify only supports JSON serialization and generally assumes the remote endpoint accepts and responds with JSON. Raw byte data can be sent by tagging a field with #[endpoint(data)] and can be received by using the Endpoint::exec_raw() method.

Installation

cargo add rustify

Architecture

This crate consists of two primary traits:

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 http::Requests and return http::Responses. A blocking variant of the client (rustify::blocking::client::Client) is provided for implementations that block.

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:

```rust use rustify::clients::reqwest::Client; 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 = Client::default("http://api.com"); let result = endpoint.exec(&client); assert!(result.is_ok()); ```

Advanced Usage

This examples demonstrates the complexity available using the full suite of options offered by the macro (this requires the middleware feature):

```rust use bytes::Bytes; use derivebuilder::Builder; use rustify::clients::reqwest::Client; use rustify::endpoint::{Endpoint, MiddleWare}; use rustify::errors::ClientError; use rustifyderive::Endpoint; use serde::{Deserialize, Serialize}; use serdejson::Value; use serdewith::skipserializingnone;

struct Middle {} impl MiddleWare for Middle { fn request( &self, : &E, req: &mut http::Request>, ) -> Result<(), ClientError> { req.headersmut() .append("X-API-Token", http::HeaderValue::fromstatic("mytoken")); Ok(()) } fn response( &self, _: &E, resp: &mut http::Response, ) -> Result<(), ClientError> { let respbody = resp.body().clone(); let wrapper: TestWrapper = serdejson::fromslice(&respbody).maperr(|e| ClientError::ResponseParseError { source: Box::new(e), content: String::fromutf8(respbody.tovec()).ok(), })?; let data = wrapper.result.tostring(); *resp.body_mut() = bytes::Bytes::from(data); Ok(()) } }

[derive(Deserialize)]

struct TestResponse { age: u8, }

[derive(Deserialize)]

struct TestWrapper { result: Value, }

[skipserializingnone]

[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, optional: Option, }

let client = Client::default("http://api.com"); let endpoint = Test::builder() .name("test") .kind("test") .build() .unwrap(); let result = endpoint.exec_mut(&client, &Middle {}); ```

Breaking this down:

rust #[endpoint( path = "test/path/{self.name}", method = "POST", result = "TestResponse", builder = "true" )]

Endpoints contain various methods for executing requests; in this example the exec_mut() 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 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 skipserializingnone 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 if absent when building. For example:

``rust // Errors,kind` field is required let endpoint = Test::builder() .name("test") .build() .unwrap();

// Produces POST http://api.com/test/path/test {"kind": "test"} let endpoint = Test::builder() .name("test") .kind("test") .build() .unwrap();

// Produces POST http://api.com/test/path/test {"kind": "test", "optional": "yes"} let endpoint = Test::builder() .name("test") .kind("test") .optional("yes") .build() .unwrap() ```

Features

The following features are available for this crate:

Error Handling

All errors generated by this crate are wrapped in the ClientError enum provided by the crate.

Testing

See the the tests directory for tests. Run tests with cargo test.

Contributing

  1. Fork it (https://github.com/jmgilman/rustify/fork)
  2. Create your feature branch (git checkout -b feature/fooBar)
  3. Commit your changes (git commit -am 'Add some fooBar')
  4. Push to the branch (git push origin feature/fooBar)
  5. Create a new Pull Request