Home

Awesome

Excon::Hypermedia wercker status

Teaches Excon how to talk to HyperMedia APIs.

Installation

Add this line to your application's Gemfile:

gem 'excon-hypermedia'

And then execute:

bundle

Or install it yourself as:

gem install excon-hypermedia

Quick Start

Excon.defaults[:middlewares].push(Excon::HyperMedia::Middleware)

api = Excon.get('https://www.example.org/api.json')
api.class # => Excon::Response

product = api.rel('product', expand: { uid: 'bicycle' })
product.class # => Excon::Connection

response = product.get
response.class # => Excon::Response
response.resource.name # => 'bicycle'

Usage

To let Excon know the API supports HyperMedia, simply enable the correct middleware (either globally, or per-connection):

Excon.defaults[:middlewares].push(Excon::HyperMedia::Middleware)

api = Excon.get('https://www.example.org/api.json')
api.class # => Excon::Response

NOTE: we'll use the following JSON response body in the below examples:

https://www.example.org/api.json

{
  "_links": {
    "self": {
      "href": "https://www.example.org/api.json"
    },
    "product": {
      "href": "https://www.example.org/product/{uid}",
      "templated": true
    }
  }
}

https://www.example.org/product/bicycle

{
  "_links": {
    "self": {
      "href": "https://www.example.org/product/bicycle"
    }
  },
  "bike-type": "Mountain Bike",
  "BMX": false,
  "derailleurs": {
    "back": 7,
    "front": 3
  },
  "name": "bicycle",
  "reflectors": true,
  "_embedded": {
    "pump": {
      "_links": {
        "self": "https://www.example.org/product/pump"
      },
      "weight": "2kg",
      "type": "Floor Pump",
      "valve-type": "Presta"
    }
  }
}

With this middleware injected in the stack, Excon's model is now expanded with several key concepts:

resources

A resource is the representation of the object returned by the API. Almost all other concepts and methods originate from this object.

Use the newly available Excon::Response#resource method to access the resource object:

api.resource.class # => Excon::HyperMedia::ResourceObject

A resource has several methods exposed:

api.resource.public_methods(false) # => [:_links, :_properties, :_embedded]

Each of these methods represents one of the following HyperMedia concepts.

links

A resource has links, that point to related resources (and itself), these can be accessed as well:

api.resource._links.class # => Excon::HyperMedia::ResourceObject::Links

You can get a list of valid links using keys:

api.resource._links.keys # => ['self', 'product']

Each links is represented by a LinkObject instance:

api.resource._links.product.class # => Excon::HyperMedia::LinkObject
api.resource._links.product.href # => 'https://www.example.org/product/{uid}'
api.resource._links.product.templated # => true

relations

Links are the primary way to traverse between relations. This is what makes a HyperMedia-based API "self-discoverable".

To go from one resource, to the next, you use the rel (short for relation) method. This method is available on any LinkObject instance.

Using rel, returns an Excon::Connection object, the same as if you where to call Excon.new:

relation = api.resource._links.self.rel
relation.class # => Excon::Connection

Since the returned object is of type Excon::Connection, all Excon-provided options are available as well:

relation.get(idempotent: true, retry_limit: 6)

Excon::Response also has a convenient delegation to LinkObject#rel:

relation = api.rel('self').get

Once you call get (or post, or any other valid Excon request method), you are back where you started, with a new Excon::Response object, imbued with HyperMedia powers:

relation.resource._links.keys # => ['self', 'product']

In this case, we ended up back with the same type of object as before. To go anywhere meaningful, we want to use the product rel:

product = api.rel('product', expand: { uid: 'bicycle' }).get

As seen above, you can expand URI Template variables using the expand option, provided by the excon-addressable library.

properties

Properties are what make a resource unique, they tell us more about the state of the resource, they are the key/value pairs that define the resource.

In HAL/JSON terms, this is everything returned by the response body, excluding the _links and _embedded sections:

product.resource.name # => "bicycle"
product.resource.reflectors # => true

Nested properties are supported as well:

product.resource.derailleurs.class # => Excon::HyperMedia::ResourceObject::Properties
product.resource.derailleurs.front # => 3
product.resource.derailleurs.back # => 7

Property names that aren't valid method names can always be accessed using the hash notation:

product.resource['bike-type'] # => 'Mountain Bike'
product.resource['BMX'] # => false
product.resource.bmx # => false

Properties should be implicitly accessed on a resource, but are internally accessed via the _properties method:

product.resource._properties.class # => Excon::HyperMedia::ResourceObject::Properties

The Properties object inherits its logics from Enumerable:

product.resource._properties.to_h.class # => Hash
product.resource._properties.first # => ['name', 'bicycle']

embedded

Embedded resources are resources that are available through link relations, but embedded in the current resource for easier access.

For more information on this concept, see the formal specification.

Embedded resources work the same as the top-level resource:

product.resource._embedded.pump.class # => Excon::HyperMedia::ResourceObject
product.resource._embedded.pump.weight # => '2kg'

Hypertext Cache Pattern

You can leverage embedded resources to dynamically reduce the number of requests you have to make to get the desired results, improving the efficiency and performance of the application. This technique is called "Hypertext Cache Pattern".

When you enable hcp, the library detects if a requested resource is already embedded, and will use that resource as a mocked response, eliminating any extra request to get the resource:

pump = product.rel('pump', hcp: true).get

pump[:hcp] # => true
pump.remote_ip # => '127.0.0.1'
pump.resource.weight # => '2kg'

This feature only works if you are sure the embedded resource is equal to the resource returned by the link relation. Also, the embedded resource needs to have a self link in order to stub the correct endpoint.

Because of these requirement, the default configuration has hcp disabled, you can either enable it per request (which also enables it for future requests in the chain), or enable it globally:

Excon.defaults[:hcp] = true

shortcuts

While the above examples shows the clean separation between the different concepts like response, resource, links, properties and embeds.

Traversing these objects always starts from the response object. To make moving around a bit faster, there are several methods available on the Excon::Response object for ease-of-use:

product.links.class # => Excon::HyperMedia::ResourceObject::Links
product.properties.class # => Excon::HyperMedia::ResourceObject::Properties
product.embedded.class # => Excon::HyperMedia::ResourceObject::Embedded

License

The gem is available as open source under the terms of the MIT License.