Today's Communique

During work today, I made more progress in unifying the ActionController=>ActionView API:

The API into ActionView is now ActionView::Base#_render_for_template. This is where the refactor was going all along, and with Josh's help, we were able to really solidify how this should work.

At the moment, there are a few stragglers that may or may not be cleaned up: AV::Base#_render_text and AV::Base#_render_inline. Using _render_inline in particular ensures that render :inline does the same thing in both ActionController and ActionView. This is the code in ActionController that delegates to ActionView for :inline:

if inline = options[:inline]
  render_for_text(@template._render_inline(inline, _pick_layout(layout_name), options))

This is the code in ActionView for rendering inline:

elsif inline = options[:inline]
  _render_inline(inline, layout, options)

As you can see, having a single method allows us to be sure that the code does the same thing in both places. You can compare with the code from before the refactor. ActionController:

elsif inline = options[:inline]
  render_for_text(@template.render(options.merge(:layout => layout)), options[:status])

ActionView:

elsif options[:inline]
  InlineTemplate.new(options[:inline], options[:type]).render(self, options[:locals])

Not particularly terrible, but the mechanism is a bit more obscure, and it's less obvious that both pieces of code do the same thing. Before the refactor, you can see that ActionController delegated to ActionView's render method, after twiddling with the options and passing them through uncritically.

Another interesting item in today's refactor: previously, layout selection happened at the beginning of the render method, before the options were examined. As a result, it was necessary for the template chooser to scan through all the options to determine whether it should fall back to the default layout if no layout was specified. For instance, render :action assumes that a layout should be used by default, while render :text assumes that a layout should not be used. Of course, :layout => "..." can be specified, but the default behavior is different.

In the refactored version, the _pick_layout method is called with information about whether it should fall back to the default, so it no longer has to scan the options. It also means that a layout is never selected if it is not required (render :nothing or RJS, for instance).

After work, I hung out with a friend from New York, but when I got back I decided to do a bit more work on Rails.

Some interesting stuff: I'm experimenting with ConcurrentHash and SafelyMemoizable, which exploits the fact that Ruby implicitly locks instance variable mutations to create a cache that should both be threadsafe and not require locks for almost all cases. This also allows a very simple threadsafe memoization technique for global caches. Again, this is a pretty early experiment, so the details may change some. Note that it's not as expensive as it looks, because Hash#dup is a shallow clone, so the memory-expensive values will exist only once in memory.

I also cleaned up layouts a bit more, including replacing a somewhat complex mechanism for dealing with default layouts that was implemented via an inherited hook that involved the rules for whether or not a missing layout should raise an error. It still is fairly arcane stuff, but I'm pretty pleased with the revised code. I needed to add some tests for layout :except and layout :only, as there were no tests for that functionality.

The very last thing tonight was to put the final piece in place to have a single place for determining whether a response is exempt from layout. Now that all options that involve rendering templates go through the same method, all of the logic for skipping layout is encapsulated in layout = _pick_layout(*layout) unless tmp.exempt_from_layout?. Putting this piece in place was very personally satisfying.

At this point, I'm pretty sure I'm posting simply to create a personal record of my work. If some of you are enjoying it, that's a bonus!