Awesome
Excon::Hypermedia
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.