Writing an API gem: the final metaprogramming
In the previous article we have covered the groovehq gem with tests and made a decision on the remaining scope of work:
- Add a little bit of metaprogramming
- 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:
- Write gems;
- Cover the code, including the one working with an external API, with tests;
- Work with an external API, and even with the Hypermedia API;
- To use metaprogramming;
- 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.