Writing an API gem: What is Hypermedia API and how to make friends with it
In the previous article we have marked the main structure of a gem, decided on the library for HTTP-requests and written a method for work with the first API point β getting the list of all the tickets. After that I was going to start adding all the other points, but have faced with a problem, which we will solve in this article: Hypermedia API support
Hypermedia API
Hypermedia API is a relatively new and a relatively debatable concept. Its main purpose is that application API returns not only data but also links to other, connected with the initial request, resources. Let's cover it through the example of the GrooveHQ API. This is the format in which it returns json for the ticket:
{
"ticket": {
"assigned_group": null,
"created_at": "2012-07-17T13:41:01Z",
"number": 1,
"priority": null,
"resolution_time": null,
"title": null,
"updated_at":"2012-07-17T13:41:01Z",
"href": "https://api.groovehq.com/v1/tickets/1",
"closed_by": "matt@groovehq.com",
"tags": ["important"],
"links": {
"assignee": {
"href": "https://api.groovehq.com/v1/agents/matt@groovehq.com"
},
"customer": {
"href": "https://api.groovehq.com/v1/customers/test@test.com"
},
"state": {
"href": "https://api.groovehq.com/v1/tickets/1/state"
},
"messages": {
"href": "https://api.groovehq.com/v1/tickets/1/messages"
}
}
}
}
See the "links" key? This is the links to the other resources, which I have mentioned. A developer doesn't need to open the documentation, he can explore the API just navigating by such links. This concept has been documented as Hypertext Application Language, and its JSON version even has a formal documentation.
If you are interested in my personal opinion, I think this idea is at least strange. Learn the API not by the documentation, but by looking at the answers of the API itself? No, thanks.
My first thought was just to brush these links aside. But then I realized one advantage: these links can make the process of writing a library a little bit easier, as they allow to easily build chains of calls, using just a few small abstractions on top of the common request. That's what we are going to do today.
One more advantage I got from the comments β changing resources addresses when using HAL doesn't break clients.
Writing Resource and Relation
As I have mentioned above, using HAL allows to build chains of calls to resources. All that's left to do is think how to implement it. At this point, it's becoming apparent that simple JSON hashes will not be enough: we will need new classes, which objects will be returned by the API.
There will be two of them: GrooveHQ::Resource
and GrooveHQ::Relation
.
GrooveHQ::Resource
is no more than a little JSON data wrapping. Aside from data itself, objects of this class store the client API (GrooveHQ::Client
) and the collection of links to other resources. That's how our class looks like:
# ./lib/groovehq/resource.rb
module GrooveHQ
class Resource
attr_reader :rels, :data
def initialize(client, data)
@client = client
@data = data
@rels = parse_rels
end
def parse_rels
@data["links"].each_with_object({}) do |(relation, value), result|
result[relation] = Relation.new(@client, value["href"])
end
end
end
end
A #parse_rels
method takes links from a json response and for each link creates an object of the GrooveHQ::Relation
class, which allows to make further requests to the linked resources there. Pay attention to each_with_object
β if you haven't seen this method before, then look through its documentation and use it whenever you need.
GrooveHQ::Relation
also stores an API object plus a link to the resource itself. Thus, in order to implement this class, we only need a simple Struct and one method, which makes the request using the client:
# ./lib/groovehq/relation.rb
module GrooveHQ
class Relation < Struct.new(:client, :href)
def get(options = {})
client.get href, options
end
end
end
Making requests to the API more complicated
Now, when we have written wrappers, which make the work with Hypermedia API easier, we just need to use them when making calls to the API. For this, we will need one more wrapper, a wrapper around HTTParty methods. I have created a new module GrooveHQ::Client::Connection
for this purpose, it now consists only from three methods: request
, parse_data
and get
. Let's review them one by one:
def request(http_method, path, options)
response = self.class.send(http_method, path, { query: options })
data = response.parsed_response
parse_data(data)
end
#request
takes an HTTP-method, a link and options as arguments, then uses HTTParty for making a request and sends the received JSON to the #parse_data
method. The result of the #parse_data
method will be the result of any request:
def parse_data(data)
data = data.values.first
case data
when Hash then Resource.new(self, data)
when Array then data.map { |hash| parse_data({resource: hash}) }
when nil then nil
else data
end
end
Depending on the type of data, we return either the Resource
class object right away or an array of such objects (by calling #parse_data
recursively) or the result of the request right away.
Pay attention to data = data.values.first
and {resource: hash}
: the thing is that our GrooveHQ API always returns a result with a root key, while our resource doesn't need this root element. That's why we always cast away the root key and take only the data itself. And since we don't want this to break something, we set our root key in a cycle.
Putting it all together
Now all that's left to do is update the #tickets
method and check if our new code works with chains of calls of any length! As I have already linked the GrooveHQ::Client::Connection
module inside of the GrooveHQ::Client
class, I can use a new method #get
inside of a module GrooveHQ::Client::Tickets
:
module GrooveHQ
class Client
module Tickets
def ticket(ticket_number, options = {})
get("/tickets/#{ticket_number}", { query: options })
end
end
end
end
Now we can do such crazy things:
client = GrooveHQ::Client.new
puts client.ticket(1).rels["assignee"].get.rels["tickets"].get(page: 2).first.data["created_atβ]
Now the gem is actually ready for adding any other API points. But you may have noticed one horrible screwup: I still haven't written any tests! I agree, it's time. In the next article we will learn to cover gems with tests and make API requests mocks.
By the way, commit is here: 8056904. And your comments and answers to them β here below: You must have had a lot of questions I will be happy to answer.
Series "Development of GrooveHQ Ruby Gem"
- How to create a gem for working with an API
- Writing an API gem: choosing the structure and the tools
- Writing an API gem: how to write tests for an external API
- Writing an API gem: What is Hypermedia API and how to make friends with it
- Writing an API gem: the final metaprogramming