Writing an API gem: the final metaprogramming

A joyful cartoonish person with a scarf doing a victory dance next to a large gemstone and a shovel embedded in the dirt. A joyful cartoonish person with a scarf doing a victory dance next to a large gemstone and a shovel embedded in the dirt.

In the previous article we have covered the groovehq gem with tests and made a decision on the remaining scope of work:

  1. Add a little bit of metaprogramming
  2. Have our gem documented and publish it

In this article I will show an easy and fast way to reduce client.ticket(74).data[:summary] to client.ticket(74).summary, thus making the use of the library more intuitive and convenient.

OpenStruct for smart hashes

Ruby has a great class OpenStruct, which is very similar to Hash, but it additionally determines getters for keys. A simple example looks like this:

ticket = OpenStruct.new(number: 2, summary: "blah")
ticket.number # => 2
ticket.summary # => "blah"

Having thought for a little, I have decided that OpenStruct is perfect for the data attribute of the GrooveHQ::Resource class. The modified constructor for this class turned out to be like this:

# ./lib/groovehq/resource.rb
# ...
    def initialize(client, data)
      data = {} unless data.is_a?(Hash)

      @client = client

      data = data.with_indifferent_access

      links = data.delete(:links) { Hash.new }
      links[:self] = data.delete(:href) if data.has_key?(:href)

       # Now @data is not just a hash but an object of the OpenStruct class.
      @data = OpenStruct.new(data.with_indifferent_access)

      @rels = parse_links(links).with_indifferent_access
    end
# ...

This has got us closer to the final goal, now we can make client.ticket(74).data.summary. But how to get rid of the transitional data? Use a method_missing!

method_missing is the best friend of a metaprogrammer

method_missing is a method which Ruby calls when you try to call for a method that doesn't exist. By default, it throws an exception, but you can override this method so that it did some interesting stuff. I use it, so that the GrooveHQ::Resource class objects first tried to call this method with an attribute @data when calling methods like summary, number and the other keys API has returned, and only after threw an exception. Sounds complicated? An example is worth many words:

# ./lib/groovehq/resource.rb
# ...
    def method_missing(method_sym, *arguments, &block)
      # It seems like the method is missing. But, perhaps, it exists in @data?
      if @data.respond_to?(method_sym)
        # It actually does! Then lets call it on @data and return the response
        @data.send(method_sym)
      else
        # And if it doesn't, then throw an exception
        super
      end
    end

Now we can make client.ticket(74).summary. I was surprised myself, when the problem with adding getters for API-resources was resolved so easily. The commit is here: ad39e1d.

Resource collections

One more problem has left: calling resource collections. For example, an array of tickets: right now we still need to write: client.tickets(page: 2).collection.first.number. I wish we could work with the results of such requests directly, without the transitional collection. For this, we will have to get acquainted with Enumerable.

A module Enumerable represents methods for work with collections – first, last, map etc. (look at the list by the link to the documentation above). The only condition of its usage is that the class should have a method each. The simplest way is to add this module to the GrooveHQ::Resource class, and implement the method each the following way:

    def each(&block)
      collection.each { |item| yield item }
    end

But then one problem arises: why do simple resources need all this package of methods for work with the collection? Isn't it time to separate the Resource from the Resource Collection? I'll remind you that now the Resource Collection is the same Resource, but it has a key collection instead of data.

# ./lib/groovehq/client/connection.rb
# ...
        when Array then Resource.new(self,
          {
            collection: data.map { |hash| parse_data({resource: hash}) },
            meta: original_data["meta"],
            links: {
              next: {
                href: original_data["meta"]["pagination"]["next_page"]
              },
              prev: {
                href: original_data["meta"]["pagination"]["prev_page"]
              }
            }
          })
# ...

Having thought for a little bit more, I decide to add a GrooveHQ::ResourceCollection class. It looks like this, see the comments in code:

# ./lib/groovehq/resource_collection.rb
module GrooveHQ

  class ResourceCollection < Resource
    include Enumerable

    def initialize(client, data)
      data = {} unless data.is_a?(Hash)
      data = data.with_indifferent_access

      @client = client

      meta_data = data.delete(:meta) { Hash.new }

      # Making the array itself. Array() allows to ensure that even if data.values.first == nil,
      # we still pass the array to the map.
      collection = Array(data.values.first).map do |item|
        Resource.new(client, item)
      end

      links = {}

      # Building up the links to the next pages
      if meta_data.has_key?("pagination")
        links = {
          next: {
            "href" => meta_data["pagination"]["next_page"]
          },
          prev: {
            "href" => meta_data["pagination"]["prev_page"]
          }
        }
      end

      @data = OpenStruct.new(meta: meta_data, collection: collection)
      @rels = parse_links(links).with_indifferent_access
    end

    def each(&block)
      collection.each { |item| yield item }
    end

  end

end

Now the gem allows to use such constructions: client.tickets(page: 2).map { |ticket| ticket.number }. Having one more class covered with tests, I make one more commit.

Coming to an end

I have promised two articles, but it seems that there is not enough information for one more. After solving the last problem with ResourceCollection, I have written a short documentation to the gem and published it on rubygems: https://rubygems.org/gems/groovehq. This is the end of the series. Let's remember everything we've learned in this series:

  1. Write gems;
  2. Cover the code, including the one working with an external API, with tests;
  3. Work with an external API, and even with the Hypermedia API;
  4. To use metaprogramming;
  5. Also, we have got acquainted with several good gems, which by all means will be useful for you.

I did my best for this series to be only 10% about creating gems, and 90% about Ruby development on the whole, and have added as many interesting things, gems, methods and discussions as I could. I hope you will learn a lot with the help of these posts! Write at the comments, what you think about this series, what you liked, what it lacks etc.