Skip to content

Latest commit

 

History

History
389 lines (308 loc) · 14.5 KB

GUIDE.md

File metadata and controls

389 lines (308 loc) · 14.5 KB

Anterofit User's Guide

Anterofit makes it easy to abstract over REST APIs and asynchronous requests.

The core of Anterofit's abstraction power lies in its macro-based generation of service traits, eliminating the noisy boilerplate involved in creating and issuing HTTP requests. The focal point of this power lies in the service!{} macro.

See the README for setting up dependencies.

####Note This document is a work-in-progress. If there is any information which you think would be helpful to add, please feel free to open a pull request.

The API Documentation also contains a wealth of information about Anterofit and its functionality.

Creating Service Traits with service!{}

service!{} simply takes the definition of a trait item, with its method bodies in a particular format, and generates an object-safe implementation of the service trait for the Adapter type.

service! {
    /// Trait wrapping `myservice.com` API.
    pub trait MyService {
        /// Get the version of this API.
        fn api_version(&self) -> String {
            GET("/version")
        }

        /// Register a user with the API.
        fn register(&self, username: &str, password: &str) {
            POST("/register");
            fields! {
                username, password
            }
        }
    }
}

Service Trait Method Overview

Service trait methods always take &self as the first parameter; this is purely an implementation detail. Method parameters are passed to the implementation, unchanged. They can be borrowed or owned, but there is some restriction on the usage of borrowed parameters.

The return type of a trait method can be any type that implements the correct deserialization trait for the serialization framework you're using:

  • Serde (enabled by default): Deserialize, derived with #[derive(Deserialize)] via either serde_codegen (build script) or serde_derive (procedural macro).

  • rustc-serialize: Decodable, derived with #[derive(RustcDecodable)]

The return type can also be omitted. Just like in regular Rust, it is implied to be ().

The body of a trait method is syntactically the same as any (non-empty) Rust function: zero or more semicolon-terminated statements/expressions followed by an unterminated expression. However, there are a couple of major differences:

HTTP Verb and URL

The first expression, which is always required, is structured like a function call, where the identifier outside is an HTTP verb and the inside is the URL string and any optional formatting arguments, in the vein of format!() or println!(). This allows parameters to be interpolated into the URL. The most common HTTP verbs are supported: GET POST PUT PATCH DELETE

// If `id` is some parameter that implements `Display`
GET("/version")
GET("/posts/{}", id)
POST("/posts/update/{}", id)
DELETE("/posts/{id}", id=id)

Notice that the paths in these declarations are not assumed to be complete URLs; instead, they will be appended to the base URL provided in the adapter. However, if necessary, they can be complete URLs, with the base URL being omitted during the construction of the adapter.

Request Modifiers (query pairs, form fields, etc)

All expressions following the first, if any, are treated as modifiers to the request. Syntactically, any expression is allowed, but arbitrary expressions will likely not typecheck due to some implementation details of the service!{} macro. Instead, you are expected to use the other macros provided by Anterofit to modify the request.

See the Macros header in the crate docs for more information.

Query Parameters

To add query parameters, sometimes called GET parameters, use query!{}, which takes a series of key-value pairs; Display implementation is required, but the types don't have to be homogeneous and don't have to be Send or 'static:

service! {
    /// Hypothetical service getting user profiles.
    pub trait UserService {
        /// List usernames partially matching `search`, returning at most `max_count`.
        fn search_username(&self, search: &str, max_count: u32) -> Vec<User> {
            GET("/user");
            query! { 
                "search" => search,
                "max_count" => max_count,
            }
        }
    }
}

HTTP Form Fields

To add form fields, sometimes called POST parameters, use fields!{}, which takes a series of key-value pairs similar to query!{}; the requirements are mostly the same, but fields!{} also has an optional short-hand syntax for when the identifier is the same as the field name. As an expansion of the first example:

service! {
    pub trait RegisterService {
        /// Register a user with the API.
        fn register(&self, username: &str, password: &str) {
            POST("/register");
            fields! {
                "username" => username,
                // Shorthand for `"password" => password,
                password
            }
        }
    }
}

File Upload

To add a file to be uploaded, use path!() (takes anything convertible to PathBuf) as a field value: (Also showcases the generic syntax limitation of service!{})

service! {
    pub trait AvatarService {
        /// Set a new avatar for the logged-in user.
        /// `UploadResponse` would say whether or not the file was accepted.
        /// If there was an error opening the file for upload, it will be in the `Request`
        fn upload_avatar[P: Into<PathBuf>](&self, file_path: P) -> UploadResponse {
            POST("/avatar");
            fields! {
                "avatar" => path!(file_path)
            }
        }
    }
}

To add a stream to be uploaded (can be any generic Read impl), use stream!() as a field value. The server will see this as a file field. stream!() has a few different variants depending on how much information you want to provide: (Also showing where clause syntax)

// This gives us the `mime!()` macro shorthand
#[macro_use] extern crate mime;

service! {
    /// Some hypothetical file upload service
    pub trait UploadService {
        /// Uploads an `application/octet-stream` file
        fn upload_stream[R: Send + 'static](&self, stream: R) -> UploadResponse [where R: Read] {
            POST("/stream");
            fields! {
                // The generic `Read` impl is the first value. `Send + 'static` is required.
                "stream" => stream!(stream),
            }
        }
        
        /// Upload a file to be interpreted as a PNG
        fn upload_png[R: Send + 'static](&self, image: R) -> UploadResponse [where R: Read] {
            POST("/image");
            fields! {
                "image" => stream!(image, content_type = mime!(Image/Png)),
            }
        }
        
        /// Upload a file to be interpreted as text, with the given filename
        fn upload_text[R: Send + 'static](&self, filename: &str, text: R) -> UploadResponse [where R: Read] {
            POST("/text");
            fields! {
                // Both `filename` and `content_type` keys are optional
                // The `filename` key can be borrowed
                "text" => stream!(text, filename = filename, content_type = mime!(Text/Plain)),
            }
        }
    }
}

Custom Body

To set the request body, use the body!() macro. You would use this if your REST API is expecting parameters passed as, e.g. JSON, instead of in an HTTP form. body!(), of course, requires the given type to implement the serialization trait for your chosen framework (rustc-serialize or Serde). Also, by default, it requires the type to be Send + 'static, as it will be serialized on the executor. Adding the EAGER: keyword forces immediate serialization so that you have more freedom in the types you use, but is not recommended for large values where serialization could take a long time or use a large memory buffer to store the serialized value.

#[derive(RustcEncodable)]
pub struct NewPost<'a> {
    userId: u64,
    title: &'a str,
    message: &'a str
}

service! {
    pub trait PostService {
        fn create_post(&self, new_post: NewPost) {
            POST("/post");
            // The `EAGER` keyword forces immediate serialization
            // This allows values that are not `Send` or `'static`
            body!(EAGER: new_post)
        }
    }
}

You will need to set a serializer which can encode types in the right format; see the Getting an Adapter / Serialization header for more information.

Custom Body with Key-Value Pairs

To set the request body as a series of key-value pairs, use body_map!(). This behaves as if you passed a HashMap or BTreeMap of the key-value pairs to body!(), but does not require the keys to implement any trait except std::fmt::Display (thus, keys are not deduplicated or reordered--the server is expected to handle it); values are, of course, expected to implement the serialization trait from the serialization framework you're using.

Advanced

To apply arbitrary mutations or transformations to the request builder, use with_builder!() or map_builder!(), respectively.

For more advanced usage, you can use bare closure expressions that take RequestBuilder and return Result<RequestBuilder, anterofit::Error>. See RequestBuilder::apply(), which is used as a type hint so that no type annotations are required on the closures. All the aforementioned macros wrap this mechanism.

Delegation

When using Anterofit in a library context, such as when writing a wrapper for a public REST API, like Reddit's or Github's, you may want to control construction of and access to the Adapter to limit potential footguns, but you may still want to use service traits in your public API to limit duplication.

By default, service traits are implemented for Adapter, so this may seem at odds with the desire for abstraction. However, you can override this and have Anterofit automatically generate implementations of your service traits for a custom type: all that is required is a closure expression that will serve as an accessor for the inner Adapter instance:

pub struct MyDelegate {
    adapter: ::anterofit::Adapter,
}

service! {    
    pub trait MyService {
        /// Get the version of this API.
        fn api_version(&self) -> String {
            GET("/version")
        }

        /// Register a user with the API.
        fn register(&self, username: &str, password: &str) {
            POST("/register");
            fields! {
                username, password
            }
        }    
    }
    
    // The expression inside the braces is expected to be `FnOnce(&Self) -> &Adapter<...>`
    impl for MyDelegate { |this| &this.adapter }
}

Notice that the adapter is completely concealed inside MyDelegate, but because of Rust's visibility rules, the service trait's impl can still access it.

Making a Call

Now that you have a service trait defined, you're going to want to start issuing requests and getting responses, i.e. making calls.

Getting an Adapter

The Adapter type is the starting point of all requests in Anterofit. As implied in the service traits section, all service traits are implemented for Adapter so that you can call their methods on it.

You can start building an adapter by calling Adapter::builder(), and you finish the builder by calling build(). You'll also want to supply a base URL, which will be prepended to all service method URLs:

use anterofit::Adapter;

let adapter = Adapter::builder()
    .base_url("https://myservice.com")
    .build();

Serialization

Anterofit supports both serialization of request bodies, and deserialization of response bodies. However, Anterofit does not use any specified data format by default. The default serializer returns an error for all types, and the default deserializer only supports primitives and strings.

If you want to use the body!() or body_map!() macros in a request method, you'll need to set a Serializer during construction of the adapter. Similarly, if you want to deserialize responses as complex types, you'll need to set a Deserializer at the same time:

use anterofit::Adapter;

let adapter = Adapter::builder()
    .base_url("https://myservice.com")
    .serializer(FooSerializer)
    .deserializer(FooDeserializer)
    .build();

For serializing and deserializing JSON, the adapter builder has a convenience method: serialize_json(), and the JsonAdapter typedef for ease of naming:

use anterofit::{Adapter, JsonAdapter};

let adapter: JsonAdapter = Adapter::builder()
    .base_url("https://myservice.com")
    .serialize_json()
    .build();

As of January 2017, Anterofit only supports JSON serialization and deserialization.

Relevant types are in the serialize module.

Submitting a Request

Now that you have an Adapter instance, you can simply call your service trait methods on it. However, to maintain good namespacing, you should coerce it to a trait object or pass it to a generic function which restricts the API surface:

let my_service: &MyService = &adapter;

// Execute the request in the background, blocking until a result is available
let api_version = my_service.api_version().exec().block().unwrap());
println!("API version: {}", api_version);

fn register_user<S: MyService>(service: &S) {
    // Shorthand for `.exec().block()` but executes on the current thread instead
    my_service.register("my_user", "my_password").exec_here().unwrap();
}

register_user(&adapter);

The return type of exec() also implements Future, so you can integrate it into your event loop if you have one or do other Future-y things with it.

If you want to supply callbacks that map the result when completed, you can add on_complete() or on_result() before exec():

// Execute the request in the background; the callback will be executed if it completes successfully;
// `ignore()` silences the "unused value" lint as we don't care about the result if it wasn't successful.
my_service.api_version().on_complete(| println!("API version: {}", api_version)
    .exec().ignore();

my_service.register("my_user", "my_password")
    .on_result(|res| {
        if let Err(e) = res {
            println!("Error registering user: {}", e);
        }
        
        Ok(())
    }).exec().ignore();