Dissecting the Rails History plugin, or a little metaprogramming by
example
The Rails History
plugin was announced today in the Rails mailing list. It is a simple
plugin that has the virtue of doing its job well and idiomatically. It keeps a
queue of visits per session, except for POST and Ajax requests, that is
available to controllers. Makes for a perfect exercise on studying some source
code.
Let's see the usage. Once you install this plugin, in order to enable
session history is enough to do this:
class ApplicationController < ActionController::Base
history
end
Yeah that's all. Optional parameters for a default URL and the queue size are
provided:
class ApplicationController < ActionController::Base
history :default => "http://example.com/", :max => 10
end
Right, now we have automagically history support in all controllers, who
inherit a few instance methods to use the queue like redirect_back
for example:
def some_action
# do stuff
if some_condition
redirect_back(2) # go two requests back
end
end
The plugin does more things, but essentially that's enough to depict the
techniques it is based upon. We have:
- New class methods defined for ApplicationController
- New instance methods available to controllers
- History housekeeping maintained transparently
Any Rails developer will be familiar with that way of getting new features.
The plugin defines a module History who has a class
Container, and two convenience modules ClassMethods,
and InstanceMethods.
The module ClassMethods contains the class method
history, and the module InstanceMethods groups the
instance methods like redirect_back. How do we add them to
ActionController::Base? Easily with Ruby metaprogramming
features:
module History
def self.append_features(base)
super
base.extend(ClassMethods)
end
# rest of code
end
When a module is included in another Ruby calls
append_features,
passing the receiving module in
base. There we
extend
it with the class methods wrapped in the
ClassMethods
convenience module, which happens to define
history. This top-
level code does the mixin:
ActionController::Base.class_eval do
cattr_accessor :history_container
history_container = History::Container.new
logger.debug("loading history")
include History
end
The Container class implements the history manager itself so
to speak. The method class_eval executes that block of code in the
context of its invocant, which is the class ActionController::Base. So
we are creating a class accessor for history_container, creating an
instance, and including the mixin History. This last step is the one
that makes history available as a class method in
ActionController::Base.
Now, how are the instance methods available. The trick here is done in
the very history method:
def history(options)
logger.debug("history: setting up history")
include History::InstanceMethods
class_eval do
ActionController::Base.history_container = History::Container.new(options)
after_filter :store_location
end
end
As you see that method mixins the instance methods, instantiates a new
container with the received options, and puts the method
store_location as an after filter. That filter does the housekeeping
and that's why you don't notice.