Crate hapic

Source
Expand description

A Rust crate for quickly creating nice-to-use client libraries for HTTP APIs, in particular, there’s lots of tooling around HTTP JSON APIs.

This is still a work in progress.

§Example: defining a JSON API client

§Super Simple

We’ll start with a simple dummy API, it has the following endpoints:

>> POST /add
>> { "a": 2, "b": 3 }
<< { "c": 5 }

>> POST /sub
>> { "a": 6, "b": 3 }
<< { "c": 3 }

>> POST /factorial
>> { "a": 4 }
<< { "c": 24 }

We can define a client for this API as such:

use hapic::json_api;
use serde::{Deserialize, Serialize};

#[derive(Serialize)]
struct Add {
    a: u32,
    b: u32,
}

#[derive(Serialize)]
struct Sub {
    a: u32,
    b: u32,
}

#[derive(Serialize)]
struct Factorial {
    a: u8,
}

#[derive(Deserialize, PartialEq, Eq, Debug)]
struct Output {
    c: u64,
}

json_api!(
    struct TestClient<B, T: Transport<B>>;
    trait TestApiCall;

    simple {
        "/add": Add => Output;
        "/sub": Sub => Output;
        "/factorial": Factorial => Output;
    }
);

Now, the call types (Add, Sub and Factorial) all implement TestApiCall.

We can make an API call (to http://localhost:8080) using:

let client = TestClient::new("http://localhost:8000".into());
let output = client.call(Add { a: 1, b: 2 }).await.unwrap();
assert_eq!(output, Output { c: 3 });

§With Conversions

Now suppose we have a single endpoint for all these operations:

>> POST /op
>> { "a": 2, "b": 3, "operation": "add" }
<< { "c": 5 }

>> POST /op
>> { "a": 6, "b": 3, "operation": "sub" }
<< { "c": 3 }

>> POST /op
>> { "a": 4, "operation": "factorial" }
<< { "c": 24 }

We could define the type:

#[derive(serde::Serialize)]
struct Operation {
    operation: &'static str,
    a: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    b: Option<u32>,
}

This this isn’t very idiomatic! Firstly, the user can put any string as the operation, but we know our API server only accepts add, sub or factorial. In addition, if b is Some in a factorial operation, or is None in add or sub, we’ll have an invalid call.

We want to make this safe, so we can define two types, one for the API call, and another which will be sent as the JSON body.

enum Operation {
    Add((u32, u32)),
    Sub((u32, u32)),
    Factorial(u8),
}

#[derive(serde::Serialize)]
struct JsonOperation {
    operation: &'static str,
    a: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    b: Option<u32>,
}

impl From<Operation> for JsonOperation {
    fn from(op: Operation) -> JsonOperation {
        match op {
            Operation::Add((a, b)) => JsonOperation {
                operation: "add",
                a,
                b: Some(b),
            },
            Operation::Sub((a, b)) => JsonOperation {
                operation: "sub",
                a,
                b: Some(b),
            },
            Operation::Factorial(x) => JsonOperation {
                operation: "factorial",
                a: x as u32,
                b: None,
            },
        }
    }
}

json_api!(
    struct TestClient<B, T: Transport<B>>;
    trait TestApiCall;

    json {
        "/op": Operation as JsonOperation => Output as Output;
    }
);

Of course, we could have achieved the same thing using a custom serialization implementation, but in many cases (especially for deserializing the output) it’s significantly faster to define two types and implement TryInto or Into between them.

Finally, we make another tweak. Out Output type is just a single number, so why not return a number as the result.

impl From<Output> for u64 {
    fn from(output: Output) -> u64 {
        output.c
    }
}

json_api!(
    struct TestClient<B, T: Transport<B>>;
    trait TestApiCall;

    json {
        "/op": Operation as JsonOperation => Output as u64;
    }
);

Now we can make API calls really neatly:

let client = TestClient::new(std::borrow::Cow::Borrowed("http://localhost:8000"));
let output = client.call(Operation::Add((1, 2))).await.unwrap();
assert_eq!(output, 3);

§Real-World Example

For a real world example, see cloudconvert-rs.

§Using the macros

In the simple examples above, we made use of the json_api! macro to generate the client and API calls.

The json_api! macro is just a shorthand way of using the client! macro, then the json_api_call! macro, both of which have detailed documentation.

§Implementing Traits Directly

You can also (and in some cases will want to) implement the traits directly. Here’s a summary, from most abstract to least abstract:

  • SimpleApiCall: a JSON API call where the input and output types undergo no conversion (automatically implements JsonApiCall).
  • JsonApiCall: a JSON api call, allowing for conversion of the request and response (automatically implements ApiCall).
  • ApiCall: a generic API call (automatically implements RawApiCall).
  • RawApiCall: the lowest level trait for API calls, you probably want to implement one of the others.

Re-exports§

pub use http;
pub use serde_json;
pub use hyper;
pub use hyper_tls;

Modules§

transport

Macros§

client
Macro for generating the client and ApiCall type.
json_api
Define a JSON api client and some API calls.
json_api_call
A macro for quickly implementing the various JSON API call traits.

Structs§

Client

Enums§

Error

Traits§

ApiCall
A generic API call. For a lower level interface, see RawApiCall. For JSON APIs, see JsonApiCall and SimpleApiCall.
JsonApiCall
A JSON API call. (See also: SimpleApiCall)
RawApiCall
The lowest level trait for API calls. You probably shouldn’t implement it directly, instead preferring ApiCall, or, for JSON APIs, JsonApiCall and SimpleApiCall.
SimpleApiCall
A JSON API call where the input and output types undergo no conversion.