LazyResource

ActiveResource with its feet up. The block less, do more consumer of delicious APIs.

LazyResource is ActiveRecord made less blocking. Built on top of Typhoeus, it queues up your requests to make your API consumer a whole lot quicker. Work smarter, not harder.

It also has a simple, readable, easy-to-use API, borrowing some of the best parts of ActiveResource with a bit of ActiveRecord method-chaining flair. Not only is it faster, it's better-looking, too.

Don't believe me? Check out some of the examples in the examples directory to see for yourself.

Build Status

Installation

Add this line to your application's Gemfile:

gem 'lazy_resource'

And then execute:

$ bundle

Or install it yourself as:

$ gem install lazy_resource

Usage

Define a model:

class User
  include LazyResource::Resource

  self.site = 'http://example.com'

  attribute :id, Integer
  attribute :first_name, String
  attribute :last_name, String
end

Then use it:

me = User.find(1)                                                    # => GET /users/1
bobs = User.where(:first_name => 'Bob')                              # => GET /users?first_name=Bob
terry = User.new(:first_name => 'Terry', :last_name => 'Simpson')
terry.save                                                           # => POST /users
terry.last_name = 'Jackson'
terry.save                                                           # => PUT /users/4
terry.destroy                                                        # => DELETE /users/4

What about associations?

class Post
  include LazyResource::Resource

  self.site = 'http://example.com'

  attribute :id, Integer
  attribute :title, String
  attribute :body, String
  attribute :user, User
end

class User
  include LazyResource::Resource
  # Attributes that have a type in an array are has-many
  attribute :posts, [Post]
end

me = User.find(1)
me.posts.all       # => GET /users/1/posts

That's cool, but what if my end-point doesn't map with my association name?

class Photo
  include LazyResource::Resource

  attribute :id, Integer
  attribute :urls, Hash
  attribute :photographer, User, :from => 'users'
end

# similarly, model-level
class User
  self.from = 'people'
end

What's this about blocking less?

Unlike ActiveResource, LazyResource doesn't initiate a request on every find. Instead, whenever you do a find, where, etc. it throws those requests into a queue that gets processed when you hit an accessor. Built on Typhoeus, all of those queued requests get executed at the same time.

That original example above with me, the Bobs, Sam, and Terry? Those first four requests would all get executed at the same time, when Terry was saved. Pretty neat, eh?

That's great, but could you show me some examples that are a bit more complex?

Sure thing! Take a look at the files in the examples directory, or read through the specs.

How about a section of random features?

Here you go:

Fetch associations without hitting the URL generation code.

class Photo
  include LazyResource::Resource

  attribute :id, Fixnum

  # define the route inline
  attribute :location, Location, :route => '/location/:lat,:long'

  # define the route inline using a proc
  attribute :model, Model, :route => lambda { "/photos/#{id}/model" }

  # define the route using a method or attribute
  attribute :photographer, User, :route => :photographer_url
  attribute :photographer_url, String
  def photographer_url
    "/photographer/:name"
  end
end

Parsing responses like { 'photo': ... }

class Photo
  include LazyResource::Resource

  self.root_node_name = 'photo'
end

or multiple options

class Photo
  include LazyResource::Resource

  self.root_node_name = ['photo', 'photos']
end

Parsing responses like { 'photos': ..., 'total': 100, 'page': 2, ... }

photos = Photo.where(:user_id => 123)
photos.other_attributes # => { 'total' => 100, 'page' => 2, ... }

Sending default headers or params

Keep in mind that this can be accomplished by using, .e.g, .where(:access_token => current_user.access_token, :headers => { :"X-Access-Token" => current_user.access_token }), but I prefer this method because it keeps the logic in one place and doesn't litter your where calls with stuff that doesn't look ActiveRecord-y. Using Thread.current does seem a bit icky, but at least it's in one place...

# in an around_filter or similar
Thread.current[:default_headers] = { :"X-Access-Token" => current_user.access_token }
Thread.current[:default_params] = { :"access_token" => current_user.access_token }
yield
# this is important, otherwise the headers/params could persist amongst various requests
Thread.current[:default_headers] = nil
Thread.current[:default_params] = nil

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Added some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Make sure you have some decent test coverage, and please don't bump up the version number. If you want to maintain your own version, go for it, but put it in a separate commit so I can ignore it when I merge the rest of your stuff in.

Recognition

Thanks to:

TODO

  • Clean up LazyResource::Attributes#create_setter