Implementing a Rails 3 View Resolver
- rails
- ruby
One of the biggest tasks in our recent upgrade from Rails 2.2.2 to Rails 3.0.45 at Bonanza was the
re-implementation of all of our Rails monkey patches custom initializers to work with Rails 3. This upgrade
presented a pleasant pattern (which we happily took as further validation of our move to Rails 3) of taking large, fragile patches
and re-implementing them as significantly smaller chunks of code - in many cases using public APIs now exposed by Rails.
In the case that spurred this post, we have some functionality implemented by subclassing some of our main controllers and
overriding a small subset of their methods. To alleviate the need to replicate view templates between the parent and child
controllers, we extended view template lookup to walk up the controller inheritance tree (up to ActionController::Base
) until a
particular view template could be found.
To provide an example:
class SearchController < ApplicationController def some_cool_action end end class SpecializedSearchController < SearchController before_filter :some_cool_addition end
We want to be able let SearchController handle /specialized_search/some_cool_action
, but by default Rails wants
to render the view specialized_search/some_cool_action
and will give up completely if it doesn’t exist. This wasn’t too
difficulty to implement in Rails 2.2.2 by rewriting ActionController::Base#default_template_name
, like so:
class ActionController::Base # Overrides the original function to walk up the inheritance chain looking for # the first existant template when a template does not exist in the current # controller's view folder def default_template_name(action_name = self.action_name, klass = self.class) return nil unless klass.respond_to?(:controller_path) # Stop walking up the inheritance chain at the generic controller level return nil if klass == ActionController::Base if action_name && klass == self.class action_name = action_name.to_s if action_name.include?('/') && template_path_includes_controller?(action_name) action_name = strip_out_controller(action_name) end end template_name = "#{klass.controller_path}/#{action_name}" return template_name if template_exists?(template_name) # Only return a template path that can't be found if this is the klass that this # function was originally called on so the missing template error refers to this # controller (instead of this controller's ancestor) default_template_name(action_name, klass.superclass) || ((klass == self.class && template_name) || nil) end end
Not exactly beautiful, nor simple, but it gets the job done with a single method overridden. Oh, and it’s mega fragile - it not only relies on the method actually being there, but replicates some of its contents. Let’s see how we can re-implement this functionality in Rails 3…
A custom ActionView::Resolver
Rails 3 exposes view resolution in a configurable way, but I’ll be damned if I could find much in the way of examples or
documentation online or in the Rails source. resolver.rb
has a couple default implementations of resolvers, and there is some explanation of a custom resolver in a
recently released Rails book - not a lot to go on. Turns
out it is actually pretty simple:
class InheritedViewsResolver < ::ActionView::FileSystemResolver def initialize path super(path) end def find_templates(name, prefix, partial, details) klass = "#{prefix}_controller".camelize.constantize rescue nil return [] unless klass return [] unless klass.ancestors.include? ActionController::Base return [] if klass.ancestors.first == ActionController::Base ancestor = klass.ancestors.second ancestor_prefix = ancestor.name.gsub(/Controller$/, '').underscore templates = super(name, ancestor_prefix, partial, details) return templates if templates.present? find_templates(name, ancestor_prefix, partial, details) end end
In this case, we don’t really want to do anything crazy in terms of where we get our template from, so we’ll go ahead and
inherit from the existing FileSystemResolver
that Rails uses by default. A quick pass through the debugger on Rails’ default
FileSystemResolver
(which inherits from PathResolver
and uses PathResolver
’s find_templates
method) shows us what we’ve
got to work with:
There we go. We know we can expect something like find_templates("some_cool_action", "specialized_search", false, ...)
(pulling from
the initial example) so the majority of the work in InheritedViewsResolver#find_templates
will be the translation of the prefix
argument
to a controller class name and back to a prefix directory name again. The behavior of find_templates
is to return an empty array if no
template is found, so we have no problem determining if the current ancestor_prefix
resulted in a template.
Call me, please
Okay, we’re doing great - we’ve got our resolver all ready to go, but now we need it to actually get called when Rails does a template lookup. Again, not too bad - we can use the following initializer to add it in:
ActiveSupport.on_load(:action_controller) do view_paths.each do |path| append_view_path(InheritedViewsResolver.new(path.to_s)) end end
This one is a little tricky, so I’ll explain it in a bit more detail. First of all, notice that the InheritedViewsResolver#initialize
method takes a path
argument - the key here is that each FileSystemResolver
is responsible for resolving one root
views path - these are your app/views/
and vendor/plugins/*/app/views
paths, and you’ll have one view_paths
entry for
each one when ActionController
has been loaded up. We want our inherited views to work in all of these paths, so we add
a whole additional set of InheritedViewsResolver
s - one for each view path.
Now that we have everything hooked up, we can fire up the debugger and see what’s what:
Everything looks in order: we found our inherited template (in the templates
array); our arguments are as expected; and as an
added bonus, our InheritedViewsResolver
only gets called when all other resolvers fail to find a template, so we aren’t
adding significant overhead (except in cases where no template is found, that is).
Thanks for being a pal Rails 3
View resolvers are an easy way to take control of view resolution. There’s a clear pattern when re-implementing Rails 2.2.2 additions in Rails 3 - simple APIs; less fragile code; less duplicated code; and frequently - though not as clearly in this case - fewer lines of code.