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.
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.
lib folder contains 300 lines of code in 13 files. It follows usual best practices for
this kind of gems:
- defines its own
Connectionabstraction (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 (
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
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:
q (search query),
rating of the GIF),
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: "856a5bc1a2304900bf4d60bc7e4dfea6", 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
> 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.