hammock

github.com/benhoskings/hammock

DESCRIPTION:

Hammock is a Rails plugin that eliminates redundant code in a very RESTful manner. It does this in lots in lots of different places, but in one manner: it encourages specification in place of implementation.

Hammock enforces RESTful resource access by abstracting actions away from the controller in favour of a clean, model-like callback system.

Hammock tackles the hard and soft sides of security at once with a scoping security system on your models. Specify who can verb what resources under what conditions once, and everything else - the actual security, link generation, index filtering - just happens.

Hammock inspects your routes and resources to generate a routing tree for each resource. Parent resources in a nested route are handled transparently at every point - record retrieval, creation, and linking.

It makes more sense when you see how it works though. There's a screencast coming soon.

REQUIREMENTS:

benhoskings-ambition
benhoskings-ambitious-activerecord

These gems will install automatically as long as you've added the GitHub gem source:

gem sources -a http://gems.github.com

INSTALL:

sudo gem install hammock

in config/environment.rb:

Rails::Initializer.run do |config|
  config.gem 'hammock'
  ...

in app/controllers/application_controller.rb:

class ApplicationController
  include Hammock::RestfulActions
  ...

LICENSE:

Hammock is licensed under the BSD license, which can be found in full in the LICENSE file.

SYNOPSIS

At the moment, you can do this with Hammock:

class ApplicationController < ActionController::Base
  include Hammock::RestfulActions
end

class BeersController < ApplicationController
end

class Person < ActiveRecord::Base
end

class Beer < ActiveRecord::Base
  belongs_to :creator, :class_name => 'Person'
  belongs_to :recipient, :class_name => 'Person'

  def self.read_scope_for account
    L{|beer| beer.creator_id == account.id || beer.recipient_id == account.id }
  end
  export_scope :read

  # TODO - Duplication, yuck. There's a proper DSL in the pipes.
  def self.index_scope_for account
    L{|beer| beer.creator_id == account.id || beer.recipient_id == account.id }
  end
  export_scope :index

  creator_scope_for :write
end

<% @beers.each do |beer| %>
  From <%= beer.creator.name %> to <%= beer.recipient.name %>, <%= beer.reason %>, rated <%= beer.rating %>
  <%= hamlink_to :edit, beer %>
<% end %>

The scope methods above require just one thing – a context-free lambda that takes an ActiveRecord record as its argument, and returns true iff that record is within the scope for the specified account. Hammock uses the method (e.g. Beer.read_scope_for) to define resource and record scopes for the model:

Beer.readable_by(account): the set of Beer records whose existence can be known by account
Beer#readable_by?(account): returns true if the existence of this Beer instance can be known by account

You define the logic for read, index and write scopes in Beer._scope_for, and the rest just works.

These scope definitions are exploited extensively, to provide index selection, scoping for record selection, and post-selection object checks.

  • They provide the conditions that should be applied to retrieve the index of each resource.

The scope is used transperently by Hammock on /beers -> BeersController#index, and is available for use through Beer.indexable_by(account).

  • They provide a scope within which records are searched for on single-record actions.

For example, given the request /beers/5 -> BeersController#show=> 5, Rails would generate the following SQL:

SELECT * FROM "beers" WHERE (beers."id" = 5) LIMIT 1

Hammock uses the conditions specified in Beer.read_scope_for to generate (assuming an account_id of 3):

SELECT * FROM "beers" WHERE ((beers.creator_id = 3 OR beers.recipient_id = 3) AND beers."id" = 5) LIMIT 1

Hammock uses Beer.read_scope_for on #show, and write_scope_for on #edit, #update and #destroy. These scopes can be accessed as above through Beer.readable_by(account) and Beer.writeable_by(account). This eliminates authorization checks from the action, because if the ID of a Beer is provided that the user doesn't have access to it will fall outside the scope and will not be found in the DB at all.

  • They are used to discover credentials for already-queried ActiveRecord objects, without touching the database again.

Just as Beer.readable_by(account) returns the set of Beer records whose existence can be known by account, @beer.readable_by?(account) returns true iff @beer's existence can be known by account. This is employed by hamlink_to.

These three uses of the scope, plus another as-yet unimplemented bit, provide the entire security model of the application.

THE MASTER PLAN

Lots of functionality is planned that will take this much further.