Teaches Excon how to talk to HyperMedia APIs.
- Installation
- Quick Start
- Usage
- resources
- links
- relations
- properties
- embedded
- Hypertext Cache Pattern
- shortcuts
- License
Installation
Add this line to your application's Gemfile:
gem 'excon-hypermedia'And then execute:
bundleOr install it yourself as:
gem install excon-hypermediaQuick 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::ResponseNOTE: 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::ResourceObjectA 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::LinksYou 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 # => truerelations
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::ConnectionSince 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').getOnce 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' }).getAs 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 # => trueNested properties are supported as well:
product.resource.derailleurs.class # => Excon::HyperMedia::ResourceObject::Properties
product.resource.derailleurs.front # => 3
product.resource.derailleurs.back # => 7Property 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 # => falseProperties should be implicitly accessed on a resource, but are internally
accessed via the _properties method:
product.resource._properties.class # => Excon::HyperMedia::ResourceObject::PropertiesThe 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] = trueshortcuts
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::EmbeddedLicense
The gem is available as open source under the terms of the MIT License.