rustify 0.6.1

A Rust library for interacting with HTTP API endpoints.
Documentation
use std::collections::HashMap;

use derive_builder::Builder;
use rustify::{Client, Endpoint, Wrapper};
use rustify_derive::Endpoint;
use serde::{de::DeserializeOwned, Deserialize};

// While using a builder archetype for requests is not required, it's often the
// cleanest way for building requests. For this endpoint it doesn't bring too
// much benefit, however, for consistency it's implemented anyways.
//
// Setting the `builder` attribute to true adds a `builder()` method to the
// struct for easily getting a default instance of the request builder.
//
// Rustify supports a few more parameters in the endpoint definition than are
// shown here. It resorts to sane defaults in most cases. For our example they
// are as follows:
// * method: defaults to GET
// * request_type: defaults to JSON
// * response_type: defaults to JSON
#[derive(Builder, Endpoint)]
#[endpoint(path = "/api/users", response = "Vec<User>", builder = "true")]
struct ListUsersRequest {
    // Tagging this field with #[endpoint(query)] informs rustify that this
    // field should be appended as a query parameter to the request URL.
    #[endpoint(query)]
    pub page: usize,
}

// Some responses from the API are paginated and contain a common wrapper around
// the actual resulting data. Since this is so prevalent in APIs, rustify offers
// a `Wrapper` which can be used to define this behavior.
//
// Below we define the details of the wrapper that appears around paginated
// responses. The form of the resulting data field is specified with a generic
// and will be supplied when we call the endpoint. Endpoints have a special
// `exec_wrap()` method which will automatically wrap the response from the
// endpoint in the given wrapper.
#[derive(Debug, Deserialize)]
pub struct PaginationWrapper<T> {
    pub page: usize,
    pub per_page: usize,
    pub total: usize,
    pub total_pages: usize,
    pub data: T,
    pub support: HashMap<String, String>,
}

// This is almost always the form that the implementation will take.
// Unfortunately, Rust does not support associated types having a default
// type set to a generic, so we must define it when we use it.
impl<T: DeserializeOwned + Send + Sync> Wrapper for PaginationWrapper<T> {
    type Value = T;
}

// Our endpoint returns a JSON array of objects which each contain information
// about a user. We represent this by creating a `User` struct and then using
// `Vec<User>` in the `response` parameter of the endpoint to inform rustify on
// how it should deserialize the response. We don't need to worry about the
// wrapper because it's handled for us!
#[derive(Debug, Deserialize)]
struct User {
    pub id: usize,
    pub email: String,
    pub first_name: String,
    pub last_name: String,
}

#[tokio::main]
async fn main() {
    // In order to execute endpoints, we must first create a client configured
    // with the base URL of our HTTP API server. In this case we're using the
    // popular reqres.in for our example.
    // Asynchronous clients can be found in rustify::clients and synchronous
    // clients in rustify::blocking::clients.
    let client = Client::default("https://reqres.in/");

    // We use the builder archetype here for constructing an instance of the
    // endpoint that we can then execute. It's safe to unwrap because we know
    // that all required fields have been specified.
    let endpoint = ListUsersRequest::builder().page(1).build().unwrap();

    // Here is where the magic of rustify happens. We call `exec()` which
    // takes an instance of a `Client` and behind the scenes rustify will
    // initiate a connection to the API server and send a HTTP request as
    // defined by the endpoint. In this case, it sends a GET request to
    // https://reqres.in/api/users?page=1 and automatically deserializes the
    // response into a PaginationWrapper<ListUsersResponse> when we call parse.
    let result = endpoint.exec(&client).await;

    // Executing an endpoint can fail for a number of reasons: there was a
    // problem building the request, an underlying network issue, the server
    // returned a non-200 response, the response could not be properly
    // deserialized, etc. Rustify uses a common error enum which contains a
    // number of variants for identifying the root cause.
    match result {
        // We inform rustify of the wrapped response by calling `wrap()` instead
        // of `parse()` which takes a single type argument that instructs
        // rustify how to properly parse the result (in this case our data is
        // wrapped in a pagination wrapper).
        Ok(r) => match r.wrap::<PaginationWrapper<_>>() {
            Ok(d) => {
                d.data.iter().for_each(print_user);
            }
            Err(e) => println!("Error: {:#?}", e),
        },
        Err(e) => println!("Error: {:#?}", e),
    };
}

fn print_user(user: &User) {
    println!(
        "ID: {}\nEmail: {}\nFirst Name: {}\nLast Name: {}\n\n",
        user.id, user.email, user.first_name, user.last_name
    );
}