Home

Mar 11

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:

Debugging PathResolver#find_templates

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 InheritedViewsResolvers - one for each view path.

Now that we have everything hooked up, we can fire up the debugger and see what’s what:

Debugging InheritedViewsResolver#find_templates

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.

blog comments powered by Disqus