The case study for The Last API Wrapper
TL;DR: This post shows why you probably should consider using “The Last API Wrapper” next time you need to get something from HTTP API.
GIPHY’s API is taken as an example.
Preface
Idea of this example was born when I’ve seen recently created giphy_api gem. I don’t want to offend author of this (clean and well designed) gem in any way, but it inspired me to show an example of what it takes to create API wrapper from scratch, even for simple API, and what other options we can have.
giphy_api
’s lib
folder contains 300 lines of code in 13 files. It follows usual best practices for
this kind of gems:
- defines its own
Connection
abstraction (wrapper around HTTP client); - defines its own configuration DSL (global in this case, e.g. you can’t use two different API keys in the same code, which, in fact, is not the best practice);
- wraps all domain objects (
Gif
,Sticker
,Image
,User
) in their own classes.
What this all about?
Lots of people have done this kind of thing lots of time, for all kinds of APIs you can imagine, starting the wheel over and over again: connection, exceptions, configurations, domain objects… Repeat.
And still, there are much more APIs that are still lacking decent Ruby wrapper, not talking about unmaintained or badly designed wrappers, or several competing ones for the same task.
So, maybe there can exist some common foundation to DRY up the task?..
Showcase of TLAW: API wrapper for GIPHY
Preparation:
Create API wrapper class:
class Giphy < TLAW::API
define do
# all DSL definition will go to this block,
# in order not to pollute resulting class with DSL methods
end
end
Documentation says, that all API endpoints start with http://api.giphy.com/v1/
, and have mandatory
api_key
param (Note, that GIPHY policies require you
to use different API keys for development and production.)
class Giphy < TLAW::API
define do
base 'http://api.giphy.com/v1'
param :api_key, required: true
end
end
There are several endpoints, basically grouped into work with gifs and stickers (transparent gifs suitable for chats):
class Giphy < TLAW::API
define do
# ...
namespace :gifs do # Now API object will have a #gifs method for accessing the namespaces
# URL prefix also automatically deduced to be '/gifs'
end
namespace :stickers, '/stickers' do # ...but can be rewritten, if you want method name to be different
# from URL prefix.
end
end
end
For the sake of simplicity, let’s focus on implementing only /gifs/search endpoint (fortunately, GIPHY’s API is pretty regular, so this example will be enough to continue the work).
class Giphy < TLAW::API
define do
# ...
namespace :gifs do
endpoint :search do # method #search, URL deduced to be '/gifs/search' and can be rewritten, also
end
The documentation says it has 7 params: api_key
, q
(search query), limit
, offset
, rating
(MPAA
rating of the GIF), lang
, fmt
. Of those seven, api_key
is already whole API parameter, and for
lang
it is also reasonable to be so. And fmt
parameter is probably useless. So…
class Giphy < TLAW::API
define do
base 'http://api.giphy.com/v1'
param :api_key, required: true
param :lang
namespace :gifs do
endpoint :search do
param :q
param :limit
param :offset
param :rating
end
We can immediately investigate our object in IRB/Pry:
> Giphy
# => #<Giphy: call-sequence: Giphy.new(api_key:, lang: nil); namespaces: gifs; docs: .describe>
> Giphy.describe # as hint above suggests
# Giphy.new(api_key:, lang: nil)
# @param api_key
# @param lang
#
# Namespaces:
#
# .gifs()
> Giphy.namespaces[:gifs]
# => #<Giphy::Gifs: call-sequence: gifs(); endpoints: search; docs: .describe>
> Giphy.namespaces[:gifs].describe
# .gifs()
#
# Endpoints:
#
# .search(q: nil, limit: nil, offset: nil, rating: nil)
…and really use it:
giphy = Giphy.new(api_key: '<YOUR API KEY>')
# => #<Giphy.new(api_key: "<YOUR API KEY>", lang: nil) namespaces: gifs; docs: .describe>
giphy.gifs.search(q: 'tardis')
# => hooray! search results!
In 16 lines of code, we have working API wrapper. It also provides you with:
- on-the-fly docs, as shown above;
- errors handling with meaningful reporting;
- some response post-processing (out of the scope of this short showcase, but really useful).
There are also several other helpful methods in DSL, let’s look at some of them as a bonus.
Note the method signature for #search
in on-the-fly docs:
search(q: nil, limit: nil, offset: nil, rating: nil)
In fact, we can do it this way:
endpoint :search do
desc 'Search all GIFs by word or phrase.' # short docs for endpoint
param :query, # give param mor meaningful name
field: :q, # ...while preserving q in query string
required: true, # it should not have default value
keyword: false, # ...and probably should not be keyword argument, being ovious
desc: 'Search string' # ...and can have docs
param :limit,
:to_i, # ...passed value should be converted with value.to_i method
desc: 'Max number of results to return'
param :offset, :to_i, desc: 'Results offset'
param :rating,
enum: %w[y g pg pg-13 r unrated nsfw], # only one of those is accepted, otherwise error thrown
desc: 'Parental advisory rating'
end
…resulting in:
> Giphy.namespaces[:gifs].describe
# .gifs()
#
# Endpoints:
#
# .search(query, limit: nil, offset: nil, rating: nil)
# Search all GIFs by word or phrase.
> Giphy.namespaces[:gifs].endpoints[:search].describe
# .search(query, limit: nil, offset: nil, rating: nil)
# Search all GIFs by word or phrase.
#
# @param query Search string
# @param limit [#to_i] Max number of results to return
# @param offset [#to_i] Results offset
# @param rating Parental advisory rating
# Possible values: "y", "g", "pg", "pg-13", "r", "unrated", "nsfw"
You can now review “full” (with docs, some output post-processing and all endpoints implemented) Giphy wrapper in tlaw’s repo (it takes 75 LoC).
But one of the points of TLAW is: in many cases, you don’t need the full wrapper. For example, using GIPHY’s translate functionality in chat-bot, all you need is a very simplistic client, but still a lot of boilerplate code (errors processing, response parsing, data extraction). What TLAW does is providing this “boilerplate foundation”, so you can get straight to business.