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.
It’s a gem, so do the usual:
[sudo] gem install basic_assumption
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.
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.
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.
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.
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.
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
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, 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.
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.
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
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.
Using BasicAssumption may change the exception handling strategy inside your classes. In Rails, the rescue_from
method may be useful.
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.
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.