Skip to content

An implementation of the idiom introduced by decent_exposure for concisely declaring resources in Rails controllers and views.

License

Notifications You must be signed in to change notification settings

mattyoho/basic_assumption

Repository files navigation

BasicAssumption

BasicAssumption is a gem that lets you declare resources inside of a class in a concise manner. It implements an idiom for writing certain kinds of code in a declarative way. In particular, it’s meant to make Rails controllers and views cleaner.

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'

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 returned by 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.

Using an alternative model name

BasicAssumption tends to assume a lot of things, including the name of the model class a default assume call should try to load. If you want the name given to assume to differ from the name of the model, use the optional context hash to pass an as option:

class WidgetController < ApplicationController
  assume :sprocket, :as => :widget
end

This will create a sprocket method in your actions and views that will use the Widget model for its lookup.

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.

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])
  end
end

would become this:

class RecordController < ActionController::Base
  assume :record
end

and would provide the added benefit of not tossing instance variables around. 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.

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. 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

Passing context to defaults

BasicAssumption supports passing a hash of arbitrary context information when assume is called without a block. This allows configuration or optional data to be made available in default blocks. The built-in Rails defaults use this to override the name of the model that is being worked with via the :as option.

Here is an example:

class Widget < ActiveRecord::Base
  named_scope :shiny, where(:glossy => true)
end

class WidgetController < ActionController::Base
  default_assumption do |name, context|
    name.to_s.classify.constantize.send(context[:type]).find(params[:id])
  end

  assume :widget, :type => :shiny
end

In this case, the lookups for widget are scoped to ones that are shiny.

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. Though it could be considered a bit more dangerous to do, this standard Rails default will accept an option :find_on_id, that will find on params as well as params. Enable that for one of your controllers like so:

class FilmController < ActionController::Base
  assume :film, :find_on_id => true
end

It’s also possible to have this behavior turned on by default via a configuration setting, which may be convenient for backwards compatibility with versions of BasicAssumption prior to 0.5.0. Similarly, there is a raise_error setting that will cause any errors that result from the attempt to find the record to bubble up; otherwise, they will be swallowed and the assumption method will return nil.

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, context|
          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. It should return a Proc that accepts a a symbol/string name and a context hash. 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), and enclose any data the block may need at runtime.

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. #alias_assume_to will alias assume to other names. The example below would alias it to expose and reveal. You can also set the app-wide default behavior. For more information, see BasicAssumption::Configuration.

BasicAssumption::Configuration.configure do |conf|
  conf.default_assumption = Proc.new { "I <3 GitHub." }

  conf.alias_assume_to :expose, :reveal
end

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.

There is also a Cucumber suite that can be run to check BasicAssumption against an actual Rails app.

There is an .rvmrc file in the repository that will require a basic_assumption gemset if you’re using RVM, which will help to manage the gem dependencies.

The test suites are dependent on the Bundler gem.

gem install bundler

It is highly recommended to use RVM to manage development BasicAssumption against various Rails and Ruby versions. Run the following command to create an appropriate RVM gemset and receive a command to run manually that selects that gemset:

rake rvm:gemset

To run the Cucumber and spec suites for the first time, use these Rake tasks:

rake init:rails2 #or rake init:rails3
rake

Note that the init task will bundle install the development dependencies, which includes basic_assumption itself. Using the RVM gemset is recommended.

This will create an example Rails app in ./tmp and run the suites against it. Use rake spec to run BasicAssumption’s specs, rake cucumber to run the cukes, or rake to run specs and cukes.

Feel free to fork away and send back pull requests, including specs! 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 also want to look at DecentExposure, the project BasicAssumption is based on, written by Stephen Caudill of Hashrocket. Feel free to let me know if you use it! Email mby [at] mattyoho [dot] com with questions, comments, or non-sequiters.

About

An implementation of the idiom introduced by decent_exposure for concisely declaring resources in Rails controllers and views.

Resources

License

Stars

Watchers

Forks

Packages

No packages published