Crib Build Status Coverage Status Documentation Status Code Climate Gem Version

Crib allows you to dynamically explore and package most REST APIs using an intuitive syntax that resembles a HTTP URI path. It uses Sawyer under the hood so things like authentication and passing certain headers to every request are simple.

In the below example, we are using the Dribbble API to get the name of the currently authenticated user, then just a single user in order to demonstrate arguments:

dribbble = Crib::API.new('https://api.dribbble.com/v1') do |http|
  http.authorization 'Bearer', '1aea05cfdbb92294be2fcf63ee11b412fd88c65051bd3144302c30ae8ba18896'
end

me = dribbble.user
me._get.name
 # => "Rafal Chmiel"

dan = dribbble.users('simplebits')
dan._get.name
 # => "Dan Cederholm"

If you're interested in building a REST API client you can use the Crib::Resource class which provides a DSL. Here's an interesting example of this functionality demonstrated using the GitHub API:

class GitHub < Crib::Resource
  define 'https://api.github.com' do |http|
    http.headers[:user_agent] = 'crib'
  end

  action :user do |user|
    get api.users(user)
  end

  action :issues do |repo, options = {}|
    get api.repos(*repo.split('/')).issues, options
  end
end

github = GitHub.new

me = github.user('rafalchmiel')
me.name
 # => "Rafal Chmiel"

rails_issues = github.issues('rails/rails', per_page: 2)
rails_issues.count
 # => 2

Philosophy

The aim of this project is to be able to explore and package most REST APIs using a straightforward syntax and an intuitive DSL. Crib uses Sawyer to simplify requests and easily read responses, which means there's no need to overcomplicate response handling and middleware.

Inspiration

Crib takes a lot of its inspiration from Blanket. The aim of this project is not to compete with Blanket, but to produce a very different flavour of it. Some parts of the documentation and code are borrowed from Octokit and Resource Kit.

Quick Start

Install the latest stable version of Crib via RubyGems:

$ gem install crib

Alternatively add it to your Gemfile and run bundle install:

gem 'crib'

At the top of your code, require the library:

require 'crib'

Defining an API

In order to define a REST API in Crib you need to initialise a new Crib::API class passing it the endpoint, any optional Sawyer options, and an optional block for Sawyer to yield connection options to.

As Sawyer is built on top of Faraday, the options in this block are yielded to a Faraday::Connection which means you can set headers, authentication stuff, and middleware like this:

dribbble = Crib::API.new('https://api.dribbble.com/v1') do |http|
  http.headers[:user_agent] = 'crib'
  http.authorization 'Bearer', '1aea05cfdbb92294be2fcf63ee11b412fd88c65051bd3144302c30ae8ba18896'
  http.response :logger # take note of this, it logs all requests in the following examples
end

dribbble.users('simplebits')._get.id
 # I, [2015-01-02T14:59:20.183319 #6990]  INFO -- : get https://api.dribbble.com/v1/users/simplebits
 # D, [2015-01-02T14:59:20.183436 #6990] DEBUG -- request: User-Agent: "crib"
 # Authorization: "Bearer 1aea05cfdbb92294be2fcf63ee11b412fd88c65051bd3144302c30ae8ba18896"
 # I, [2015-01-02T14:59:20.183742 #6990]  INFO -- Status: 200
 # D, [2015-01-02T14:59:20.183890 #6990] DEBUG -- response: server: "nginx"
 # date: "Fri, 02 Jan 2015 14:59:20 GMT"
 # content-type: "application/json; charset=utf-8"
 # (...)
 #
 # => 1

Defining an API using the DSL is almost the same: just replace Crib::API.new with define (the definition will be accessible via #api):

class Dribbble < Crib::Resource
  define 'https://api.dribbble.com/v1' do |http|
    http.headers[:user_agent] = 'crib'
    http.authorization 'Bearer', '1aea05cfdbb92294be2fcf63ee11b412fd88c65051bd3144302c30ae8ba18896'
    http.response :logger
  end

  # ...
end

Constructing Requests

Crib uses #method_missing for Crib::API and Crib::Request instances. This means when you call, for example, #users('rafalchmiel') on your API a new instance of Crib::Request is created and returned (with the private instance variable @uri set to "users/rafalchmiel"). This allows you to chain methods together, because these new instances create new instances of their own class, each time joining together their URIs. For example:

# GET /users/:user/followers
dribbble.users('rafalchmiel').followers
 # => <Crib::Request @api=#<Crib::API @_agent=<Sawyer::Agent https://api.dribbble.com/v1>, @_last_response=#<Sawyer::Response 200 @rels={} @data={...}>, @uri="users/rafalchmiel/followers">

Alternatively you can use #send passing it a String containing a path. For example the above can be written as:

dribbble.send('users/rafalchmiel/followers')
 # => <Crib::Request @api=#<Crib::API @_agent=<Sawyer::Agent https://api.dribbble.com/v1>, @_last_response=#<Sawyer::Response 200 @rels={} @data={...}>, @uri="users/rafalchmiel/followers">

Constructing requests when using the DSL is exactly the same. After you defined your API, your instance of Crib::API will be stored in the class that inherits Crib::Resource. You can get that instance using #api:

class Dribbble < Crib::Resource
  # ...

  action :user do
    get api.user # currently authenticated user
  end
end

Consuming Resources

So far we have only constructed a request. It's time to execute the request with a HTTP verb of your choice. You can call any of the HTTP verbs (prefixed with an underscore to keep the namespace unpoluted) on any Crib::Request instance you constructed. All these HTTP verb methods take an optional parameter and header Hash also (pass parameters in :query Hash and headers in :headers Hash). For example:

followers = dribbble.users('rafalchmiel').followers
followers._get
 # I, [2015-01-02T15:54:00.196740 #6990]  INFO -- : get https://api.dribbble.com/v1/users/rafalchmiel/followers
 # (...)
 #
 # => [{:id=>3178237 (...)}]

dribbble.shots._get(query: { sort: :comments })
 # I, [2015-01-02T16:44:00.989936 #8284]  INFO -- : get https://api.dribbble.com/v1/shots?sort=comments
 # (...)
 #
 # => [{:id=>1865147 (...)}]

All responses are Sawyer::Resource objects which provide dot notation and [] access for fields returned in the API response:

me = dribbble.users('rafalchmiel')._get
me.name
 # => "Rafal Chmiel"
me.fields
 # => #<Set: {:id, :name, :username, :html_url, :avatar_url, :bio, :location, :links, :buckets_count, :followers_count, :followings_count, :likes_count, :projects_count, :shots_count, :teams_count, :type, :pro, :buckets_url, :followers_url, :following_url, :likes_url, :projects_url, :shots_url, :teams_url, :created_at, :updated_at}>
me[:username]
 # => "RafalChmiel"
me.rels[:followers].href
 # => "https://api.dribbble.com/v1/users/97203/followers"

Note: URL fields are culled into a separate .rels collection for easier Hypermedia support.

Consuming requests is similar when using the DSL. You define actions which have a name and a block. They are then created as instance methods of the class that inherited Crib::Resource. The return value of the block you pass to .action is the return value of the method it defines, most likely a Sawyer::Response.

In order to keep your DSL code as clean as possible, Crib provides HTTP verb methods so that you don't have to call #_get, #_post, etc. every single time you define an action and want to request a resource. The DSL methods include: .get, .post, .put, .patch, .delete, and .head. They take a Crib::Request object and an optional options Hash (just like the verb methods prefixed with an underscore).

A great example of all this can be presented using this example:

class GitHub < Crib::Resource
  # ...

  action :issues do |repo, options = {}|
    get api.repos(*repo.split('/')).issues, options
  end
end

github = GitHub.new
github.issues('rails/rails', per_page: 2)
 # => ...

If you need to, you can override the API definition set in the class with .define and instead pass your own, when initialising your class:

custom_github = GitHub.new(Crib::API.new('https://api.github.dev'))

Accessing HTTP Responses

While all HTTP verb methods (#_get, #_post, etc.) return a Sawyer::Resource object, sometimes you may need access to the raw HTTP response headers. You can access the last HTTP response like this:

last_response = dribbble._last_response
 # => #<Sawyer::Response 200 @rels={} @data={:id=>97203 (...)}>
last_response.headers[:last_modified]
 # => "Fri, 02 Jan 2015 14:35:08 GMT"

When using the DSL, you can access the most recent response using #last_response:

class Dribbble < Crib::Resource
  # ...
end

dribbble = Dribbble.new
dribbble.user
 # => ...

dribbble.last_response
 # => #<Sawyer::Response 200 @rels={} @data={:id=>97203 (...)}>

Hypermedia Agent

Crib is hypermedia-enabled. Under the hood, Crib uses Sawyer, a hypermedia client built on Faraday.

Resources returned by the underscore-prefixed HTTP methods contain not only data but hypermedia link relations:

me = dribbble.users('rafalchmiel')._get

me.rels
 # => {:html_url=>"https://dribbble.com/RafalChmiel",
 #   :avatar_url=>
 #   "https://d13yacurqjgara.cloudfront.net/users/97203/avatars/normal/profile-icon-margin-transparent.png?1385898916",
 #   :buckets_url=>"https://api.dribbble.com/v1/users/97203/buckets",
 #   :followers_url=>"https://api.dribbble.com/v1/users/97203/followers",
 #   :following_url=>"https://api.dribbble.com/v1/users/97203/following",
 #   :likes_url=>"https://api.dribbble.com/v1/users/97203/likes",
 #   :projects_url=>"https://api.dribbble.com/v1/users/97203/projects",
 #   :shots_url=>"https://api.dribbble.com/v1/users/97203/shots",
 #   :teams_url=>"https://api.dribbble.com/v1/users/97203/teams"}

# Get the followers 'rel', returned from the API as 'followers_url'
me.rels[:followers].href
 # => "https://api.dribbble.com/v1/users/97203/followers"

followers = me.rels[:followers].get.data
followers.first.follower.name
 # => "Kevin Halley"

When processing API responses, all *_url attributes are culled in to the link relations collection. Any url attribute becomes .rels[:self].

Supported Ruby Versions

Crib aims to support and is tested against the following Ruby implementations:

  • 2.2.0
  • 2.1.5
  • 2.0.0-p598
  • 1.9.3-p551

If something doesn't work on one of these Ruby versions, it's a bug. Crib may inadvertently work (or seem to work) on other Ruby implementations, however support will only be provided for the versions listed above.

If you would like Crib to support another Ruby version, you may volunteer to be a maintainer. Being a maintainer entails making sure all tests run and pass on that implementation. When something breaks on your implementation, you will be responsible for providing patches in a timely fashion. If critical issues for a particular implementation exist at the time of a major release, support for that Ruby version may be dropped.

Development

If you want to hack on Crib locally, we try to make bootstrapping the project as painless as possible. Just clone and run:

$ script/bootstrap

This will install project dependencies and get you up and running. If you want to run a Pry console to poke on Crib, you can crank one up with:

$ script/console

Using the scripts in ./script instead of bundle exec rspec, bundle console, etc. ensures your dependencies are up-to-date.