API BEE

API Bee is a small client / spec for a particular style of JSON API.

These APIs must

  • Expose resource collections as paginated lists, with entries and paging properties.
  • Resource entities in collections must include an 'href' property pointing to the individual resource, so as to provide a degree of discoverability.

A single resource might look like:

{
  'name':        'Foo',
  'description': 'foo resoruce',
  'href':        'http://api.myservice.com/resources/foo'
}

A resource collection looks like:

{
  'href': 'http://api.myservice.com/resources',
  'total_entries': 100,
  'page': 1,
  'per_page': 10,
  'entries': [
    {
      'name':        'Foo',
      'description': 'foo resoruce',
      'href':         'http://api.myservice.com/resources/foo'
    },
    {
      'name':        'Bar',
      'description': 'bar resoruce',
      'href':        'http://api.myservice.com/resources/bar'
    },
    ...
  ]
}

Collection resources must include the fields 'href', 'total_entries', 'page', 'per_page' and 'entries'. This allows clients to paginate and fetch more pages.

Adapters

It is up to individual adapters to talk to different services, handle auth, etc. An adapter must at least implement 'get' for read-only APIs.

class ApiBee::Adapters::Special
  def get(path, options = {})
    # fetch JSON from remote API, passing pagination options if available
  end
end

ApiBee wraps your adapter and makes results lazily-loaded. That is, actual requests won't be made until accessing attributes or iterating the data set.

Use it:

api = ApiBee.setup :special, optional_custom_data

# No actual request made
resources = api.get('/my/resources')

# Requests once under the hood so you can iterate
resources.each do |r|
  r[:name]
end

Lazy-loading

If an object in a response has a 'href' attribute, it will be used to fetch more data if you ask for an attribute currently not in the object.

/* JSON Dataset */
{
  'user': {
    'name': 'Ismael',
    'href': 'http://api.com/users/ismael'
  }
}
# Instantiate object
user = api.get('/user')
# No extra request made
user[:name]
# Extra request to http://api.com/users/ismael to fetch more user data
user[:last_name]

This works for objects in collections too.

/* JSON collection */
{
  'users': {
    'total_entries': 100,
    'page': 1,
    'per_page': 2,
    'href': 'http://api.com/users',
    'entries': [
      {
        'name': 'Ismael',
        'href': 'http://api.com/users/ismael'
      },
      {
        'name': 'John',
        'href': 'http://api.com/users/john'
      }
    ]
  }
}
# Instantiate collection
users = api.get('/users') # no request yet

users.total_entries # => 100 # request made
users.size # => 2, current page
users.each ... # iterate current page
users.current_page # => 1
users.has_next_page? # => true
users.next_page # => 2

# Access entry. No request made
ismael = users.first
ismael[:name] # => 'ismael'
ismael[:last_name'] #=> request made to http://api.com/users/ismael

Per instance configuration

You can configure some variables on a per-instance basis. To configure the attribute name used to access a resource's URL (defaults to :href):

api = ApiBee.setup(MyCustomAdapter) do |config|
  config.uri_property_name = :uri # use :uri instead
end

Adapter-wide configuration

You can declare configuration in the adapter definition itself, too. Just define the config_api_bee class method in your adapter:

class MyCustomAdapter

  def get(path, options)
    # ...
  end

  def self.config_api_bee(config)
    config.uri_property_name = :uri
  end
end

api = ApiBee.setup(MyCustomAdapter) # instance is configured correctly

Delegate to adapter

Lazy-loading and paginating resources is great for GET requests, but you might want to still use your adapter's other methods.

api = ApiBee.setup(MyCustomAdapter) do |config|
  config.expose :delete, :post
end

# This still wraps your adapter's get() method and adds lazy-loading and pagination
api.get('/products').first[:title]

# This delegates directly to MyCustomAdapter#post()
api.post('/products', :title => 'Foo', :price => 100.0)

Wrapping nodes

You can wrap nodes in collections in your custom classes. If your adapter defines a #wrap method, it will be passed each single node in your data.

class ProductWrapper
  def initialize(node)
    @node = node
  end

  def name
    @node[:name]
  end
end

class MyCustomAdapter

  def get(*args)
    # ...
  end

  # Wrap product objects
  def wrap(node, name)
    if name =~ /product/
      ProductWrapper.new node
    else
      node
    end
  end

end

api = ApiBee.setup(MyCustomAdapter)

object = api.get('/products').first #=> an instance of ProductWrapper

object.name # => delegates to ApiBee::Node::Single#[]

finding a single resource

There's a special get_one method that you can call on lists. It delegates to the adapter and it's useful for finding a single resource in the context of a paginated list.

resources = api.get('/my/resources')
resource = resources.get_one('foobar')

That delegates to Adapter#get_one passing 2 arguments: the list's href and the passed name or identifier, so:

class ApiBee::Adapters::Special
  # ...
  def get_one(href, id)
    get "#{href}/#{id}"
  end
end

Hash adapter

ApiBee ships with an in-memory Hash adapter so it can be use with test/local data (for example a YAML file).

api = ApiBee.setup(:hash, YAML.load_file('./my_data.yml'))    

products = api.get('/products') # => ApiBee::Node::List

products.first # => ApiBee::Node::Single    
products.each() # iterate current page
products.current_page # => 1
products.paginate(:page => 2, :per_page => 4) # => ApiBee::Node::List # Next page
products.has_next_page? # => false
products.has_prev_page? # => true
products.prev_page # => 1

Examples

See examples/github_api.rb for an adapter that paginates Github's API by decorating it's results with ApiBee's required pagination properties

LICENSE

Copyright (C) 2011 Ismael Celis

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.