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!()
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. = current_user..find(params[:widget_id])
render :action => 'show'
end
def destroy
.destroy if .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.