Skip to content

Latest commit

 

History

History
331 lines (240 loc) · 11.4 KB

README.rdoc

File metadata and controls

331 lines (240 loc) · 11.4 KB

BasicAssumption

BasicAssumption is a gem meant to let you declare resources inside of a class in a concise manner. It is targetted at making Rails controllers and views cleaner.

It is a clone of DecentExposure that adds a psuedo-modular interface for providing custom defaults and some useful defaults out of the box.

What is DecentExposure?

It’s a plugin written by Stephen Caudill that provides a way of cleaning up Rails controller and view code. Find it at its GitHub repository.

DecentExposure coins an idiom for writing certain kinds of code in a declarative way. Particularly for your Rails apps, it’s worth checking out. BasicAssumption is another implementation of the idiom. Check the BasicAssumption::Configuration options for enabling compatibility with the DecentExposure API.

Install BasicAssumption

It’s a gem, so do the usual:

[sudo] gem install basic_assumption

Using it in a Rails app

For Rails 2, in environment.rb:

gem.config 'basic_assumption'

For Rails 3, in your Gemfile:

gem 'basic_assumption', :require => ['basic_assumption', 'basic_assumption/rails']

It’s possible to use BasicAssumption in your Rails 3 app without requiring ‘basic_assumption/rails’, and will in fact allow more flexibility if you don’t, but will incur the cost of additional setup. See the file ‘lib/basic_assumption/rails.rb’ for guidance.

To use the library in another context, it is enough to extend the BasicAssumption module inside the class you would like it available.

Examples

Inside a Rails controller

The presumed most-common use case for BasicAssumption is in a Rails app. By default, BasicAssumption is extended within ActionController::Base, making it available inside your controllers.

The most important (of the few) methods made available in controller classes is assume, which is used to declaratively define a resource of some kind in controller instances and to make that resource available inside corresponding views. For all the wordiness of the description, it’s a simple concept, as illustrated below. First, we will use assume to expose a resource inside our controller actions that will take the value resulting from the block passed to assume. In this case, the resource will be called ‘widget’:

class WidgetController < ActionController::Base

  assume(:widget) { Widget.find(params[:id]) }

  ...

  def purchase
    current_user.purchase!(widget)
    render :template => 'widgets/purchase_complete'
  end
end

And then inside of the ‘widgets/purchase_complete.html.haml’ view:

%h2= "#{current_user.name}, your purchase is complete!
.widget
  %span#thanks
    = "Thank you for purchasing #{widget.name}!"
  %table#details
    %tr
      %td Cost
      %td= widget.cost
    %tr
      %td Manufacturer
      %td= widget.manufacturer

By calling assume with the symbol :widget and passing it a block, an instance method widget is created on the controller that is also exposed as a helper inside views.

Special cases in controllers

A named resource created with assume may be used in multiple controller actions, or the same view template or partial referencing the named resource may be rendered by more than one action. There will be times when the behavior given to assume is correct for most cases save one or two. It’s possible to override the value of the resource method within a particular action to accommodate an exceptional case more easily. For example:

class WidgetController < ActionController::Base
  assume :widget

  def show
  end

  def show_mine
    self.widget = current_user.widgets.find(params[:widget_id])
    render :action => 'show'
  end

  def destroy
    widget.destroy if widget.owned_by? current_user
  end
end

In this case, the show_mine action overrides the value of widget so that it may reuse the view template for the regular show action. Overriding the assumed resource should be the exception, not the rule.

For more details on how BasicAssumption is wired into your Rails app, please see the BasicAssumption::Railtie documentation.

When to use it

Whenever you find yourself writing a before_filter in a Rails controller that sets instance variables as part of the context of your request, you should probably use an assumption instead. Because BasicAssumption is written to use lazy evaluation, there’s no need to worry about avoiding calls on actions that don’t need some particular setup.

For example, this:

class RecordController < ActionController::Base
  before_filter :find_record, :only => [:show, :edit, :update, :destroy]

  ...

  protected
  def find_record
    @record = Record.find(params[:record_id] || params[:id])
  end
end

would become this:

class RecordController < ActionController::Base
  assume :record
end

and would provide the added benefit of not tossing instance variables around. If a controller has protected or hidden methods that find or create instance variables used in actions and/or views, it might be cleaner to use an assumption instead. This:

class CompanyController < ActionController::Base

  def show
    @company = Company.find(params[:company_id])
  end

  def unique_groups
    @unique_groups = Group.unique_groups(@company)
  end
  helper_method :unique_groups
  hide_action   :unique_groups

end

could instead be written as:

class CompanyController < ActionController::Base

  assume :company
  assume(:unique_groups) { Group.unique_groups(company) }

end

BasicAssumption allows for a simple, declarative, and very lightweight approach to RESTful controllers. It also tends to make for a cleaner, more testable interface for controller or view testing. There may even be uses outside of Rails apps. Give it a shot.

Defaults

BasicAssumption allows for default behavior to be associated with methods created by assume whenever a block is not passed. Here is a simple example:

class MariosController < ActionController::Base
  default_assumption { "It's a me, Mario!" }
  assume :mario

  ...
end

MariosController.new.mario    #=> 'It's a me, Mario!'

In this case, any calls to assume that don’t provide a block will create methods that return the string “It’s a me, Mario!”.

In addition to passing a default block, a symbol may be passed if it corresponds to a specifically-defined helper class that came packaged with the BasicAssumption library or was provided by the application as a custom default. See below for more information on providing custom defaults.

Specifying a built-in or application-defined default can be done on assume calls as well.

assume :luigi, :using => :luigi_strategy

In Rails

In Rails, a useful default is already active out of the box. It attempts to guess the name of a class derived from ActiveRecord::Base and perform a find on it based on an id available in the params of the request. Because of this, the following two constructs would be equivalent in your controllers:

assume(:film) { Film.find(params[:film_id] || params[:id]) }
# The above line is exactly the same as:
assume(:film)

Please see Rails for implementation details . Because finding on :id could be considered dangerous, there is another default available for use, CautiousRails, that will only find on :name_id. (In the example above, this would be :film_id.) Enable that for one of your controllers like so:

class FilmController < ActionController::Base
  default_assumption :cautious_rails
end

Another option is :restful_rails, which attempts to provide appropriate behavior for the basic RESTful actions. Please see RestfulRails for a description of how it works.

Default assumptions are inherited by derived classes.

Supplying custom default behavior classes

There is an ability to provide custom, modular default extensions to BasicAssumption and then use them by passing a symbol, as in the following:

class WidgetController < ActionController::Base
  default_assumption :my_custom_rails_default
end

The symbol is converted to a class in the same manner as Rails classify/constantize operates, but it is looked up in the BasicAssumption::DefaultAssumption namespace. The following code implements the custom default specified in the preceding example. It reimplements the behavior that is active by default within Rails.

module BasicAssumption
  module DefaultAssumption
    class MyCustomRailsDefault

      def initialize(name=nil, params={})
        @name   = name.to_s
        @lookup = params['id']
      end

      def block
        klass = self.class
        Proc.new do |name|
          klass.new(name, params).result
        end
      end

      def result
        model_class.find(@lookup)
      end

      # Rely on ActiveSupport methods
      def model_class
        name.classify.constantize
      end
    end
  end
end

The only method that BasicAssumption depends on in the interface of custom default classes is the block method. Note the hoops that have to be jumped through inside the implementation of block in this example. Keep in mind the implications of evaluating the Proc returned by block using instance_eval (or instance_exec).

Configuration

There are a couple of configuration settings that can be set inside of a configuration block that can be used in places such as Rails initializer blocks. #emulate_exposure! will alias assume and default_assumption to expose and default_exposure, respectively. You can also set the app-wide default behavior. For more information, see BasicAssumption::Configuration.

Issues to note

Memoization

Methods that are created by BasicAssumption#assume memoize the result of the block the invoke when they’re called. Because of that, the block is only evaluated once during the lifespan of each object of the class that used assume. This means that a method created by assuming can be used multiple times inside of a Rails controller object and associated view(s) without invoking the associated block multiple times, but it also means that any behavior of the block that is meant to vary over multiple invocations will not be observed.

Exceptions

Using BasicAssumption may change the exception handling strategy inside your classes. In Rails, the rescue_from method may be useful.

Hacking/running specs

There is nothing special about running the specs, aside from ensuring the RUBYOPT environment variable is set to your preferred Ruby dependency manager. For example, if that’s RubyGems:

export RUBYOPT=rubygems

If you’re unfamiliar with why this is being done, take a look here for a start. Other than that, fork away and send back pull requests! Thanks.

But should I use it?

Sure! Absolutely. I think it’s a cool idea that lets you cut down on line noise, particularly in your Rails controllers. You may want to consider going with DecentExposure instead, simply because it’s used by much of Hashrocket. But so far as I know, BasicAssumption is pretty solid. Feel free to let me know if you use it! Email mby [at] mattyoho [dot] com with questions, comments, or non-sequiters.