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", 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 { pub page: usize, pub per_page: usize, pub total: usize, pub total_pages: usize, pub data: T, pub support: HashMap, } // 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 Wrapper for PaginationWrapper { 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` 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 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::>() { 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 ); }