Sozdanie gema 3

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.


Want to be as cool as Kirill Shirinkin?

Then enroll to the training! In his articles Kirill shares only a glimpse of his true knowledge. Learn more by making him your personal mentor.

Hire mentor Kirill shirinkin