Deep in Rails: ActionMailer#deliver Part III

Posted by admin 29/11/2009 at 10h47

When we last left off we were just about to jump into Renderable#render. Thatâ??s where we are going next:

# (line 193 action_pack/action_view/renderable.rb)
def render(view, local_assigns = {})
  compile(local_assigns)

  view.with_template self do
    view.send(:_evaluate_assigns_and_ivars)
    view.send(:_set_controller_content_type, mime_type) if respond_to?(:mime_type)

    view.send(method_name(local_assigns), local_assigns) do |*names|
      ivar = :@_proc_for_layout
      if !view.instance_variable_defined?(:"@content_for_#{names.first}") && view.instance_variable_defined?(ivar) && (proc = view.instance_variable_get(ivar))
        view.capture(*names, &proc)
      elsif view.instance_variable_defined?(ivar = :"@content_for_#{names.first || :layout}")
        view.instance_variable_get(ivar)
      end
    end
  end
end

Alright, this is where we get real deep into Rails. This particular method does the vast majority of the rendering work in Rails. Letâ??s see whatâ??s going on:

compile(local_assigns)

This is the first line and it sets up a series of methods that we are going to run through. compile is a private method in Renderable:

# (line 193 action_pack/action_view/renderable.rb)
def compile(local_assigns)
  render_symbol = method_name(local_assigns)

  if !Base::CompiledTemplates.method_defined?(render_symbol) || recompile?
    compile!(render_symbol, local_assigns)
  end
end

which starts out creating a method symbol for this renderable object by calling another private method aptly named method_name. This is the definition for method_name:

# (line 45 action_pack/action_view/renderable.rb)
def method_name(local_assigns)
  if local_assigns && local_assigns.any?
    method_name = method_name_without_locals.dup
    method_name << "_locals_#{local_assigns.keys.map { |k| k.to_s }.sort.join('_')}"
  else
    method_name = method_name_without_locals
  end
  method_name.to_sym
end

Here are the params for our example:

local_assigns => {}

Since our local_assigns is empty, we end up executing the else part of the conditional which sets method_name to the result method_name_without_locals which is yet another private method in Renderable. Here it is:

# (line 22 action_pack/action_view/renderable.rb)
def method_name_without_locals
  ['_run', extension, method_segment].compact.join('_')
end

This method gives us unique method name to use for the definition of the compiled rendered template. You may have seen something like the following in your Rails development logs:

Account Columns (251.9ms)   SHOW FIELDS FROM `accounts`
AccountSettings Columns (4.2ms)   SHOW FIELDS FROM `account_settings`
Account Load (0.5ms)   SELECT * FROM `accounts` WHERE (`accounts`.`id` = 47715) 
  app/controllers/application.rb:144:in `current_user'
  app/views/layouts/bare.rhtml:21:in `_run_rhtml_app47views47layouts47bare46rhtml'
  app/controllers/administration_controller.rb:10:in `index'
  vendor/plugins/mongrel_proctitle/lib/mongrel_proctitle.rb:114:in `process_client'
  vendor/plugins/mongrel_proctitle/lib/mongrel_proctitle.rb:33:in `request'
  vendor/plugins/mongrel_proctitle/lib/mongrel_proctitle.rb:112:in `process_client'
  vendor/plugins/mongrel_proctitle/lib/mongrel_proctitle.rb:102:in `run'

You see that line:

app/views/layouts/bare.rhtml:21:in `_run_rhtml_app47views47layouts47bare46rhtml'

the _run_rhtml_app47views47layouts47bare46rhtml is actually the name of the method that is given to your template. This is done because Rails executes your template, just like any piece of Ruby code. To do that, it needs a method name, and that is what we are doing in the method_name_without_locals method, creating a unique method name for this template we are about to run. method_segment is a method that is included in from ActionView::Template and extension is an attribute in ActionView::Template. Here is method_segment and extension:

# (line 22 action_pack/action_view/template.rb)
def method_segment
  relative_path.to_s.gsub(/([^a-zA-Z0-9_])/) { $1.ord }
end

extension => "erb"

which calls another method in Template called relative_path displayed here:

# (line 172 action_pack/action_view/template.rb)
def relative_path
  path = File.expand_path(filename)
  path.sub!(/^#{Regexp.escape(File.expand_path(RAILS_ROOT))}\//, '') if defined?(RAILS_ROOT)
  path
end

First, we get the full pathname to the current template and then we run sub on the path to remove anything that may look like the full expanded path of RAILS_ROOT if RAILS_ROOT is defined. The templateâ??s filename is stored in the attribute filename. Here are the params:

path => app/views/message_mailer/contact_form.erb
filename => /example_app/app/views/message_mailer/contact_form.erb

As you can see, the main purpose for this method is to give us the relative path to the mailer. Now that we got our path, letâ??s see what method_segment does to it:

relative_path.to_s.gsub(/([^a-zA-Z0-9_])/) { $1.ord }

We call gsub on the path, using the less familiar block usage. This particular usage passes each matched string to the block for processing. In this case, we want to match any character that isnt alphanumeric or an â??_â?. We take each of these matches (which are the individual characters that matched our expression) and call ord on them which returns their ASCII number. So in our case, method_segment would end up returning app47views47message_mailer47contact_form46erb.

Good, now we have our method_segment and we know what our extension is, so letâ??s see what we get from method_name_without_locals for our example:

_run_erb_app47views47message_mailer47contact_form46erb

Whew, now weâ??re back to Renderable#method_name with our method name and we return back from that to compile. Letâ??s refresh our memories real quick:

# (line 193 action_pack/action_view/renderable.rb)
def compile(local_assigns)
  render_symbol = method_name(local_assigns)

  if !Base::CompiledTemplates.method_defined?(render_symbol) || recompile?
    compile!(render_symbol, local_assigns)
  end
end

and we get back for render_symbol:

:_run_erb_app47views47message_mailer47contact_form46erb

The next section is where the super-crazy-secret-sauce magic happens, but first we have to do some checks. First, we check to see if our render_symbol is defined as a method in the module ActionView::Base::CompiledTemplates. Letâ??s take a gander at CompiledTemplates:

# (line 193 action_pack/action_view/renderable.rb)
module CompiledTemplates #:nodoc:
  # holds compiled template code
end

What theâ?¦. Where is the code?

Well, remember that we just created a new method name for our template. Why did we do that? Because Rails needs to execute the template as with everything else. So we have to define the method somehow so that it can be executed, right? Exactly. That is what CompiledTemplates is for. Itâ??s the placeholder module where we will keep these method definitions. Basically, itâ??s a nice container to hold these dynamically created methods so they arent spread out all over ObjectSpace. Plus, it makes clearing the methods cache pretty simple, just undef every method in CompiledTemplates. So first, we check to see if there is a method defined for this particular template. Then we check the private method recompile?, which is so simple, that I think it may be a placeholder for something in the future. Here is recompile?:

# (line 92 action_pack/action_view/renderable.rb)
def recompile?
  false
end

So basically, we are just checking the first part of our condition for the method definition. In our case, we dont have a method defined as such and we go on to compile!, which is a private method in Renderable and looks like this:

# (line 66 action_pack/action_view/renderable.rb)
def compile!(render_symbol, local_assigns)
  locals_code = local_assigns.keys.map { |key| "#{key} = local_assigns[:#{key}];" }.join

  source = <<-end_src
    def #{render_symbol}(local_assigns)
      old_output_buffer = output_buffer;#{locals_code};#{compiled_source}
    ensure
      self.output_buffer = old_output_buffer
    end
  end_src

  begin
    ActionView::Base::CompiledTemplates.module_eval(source, filename, 0)
  rescue Errno::ENOENT => e
    raise e # Missing template file, re-raise for Base to rescue
  rescue Exception => e # errors from template code
    if logger = defined?(ActionController) && Base.logger
      logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
      logger.debug "Function body: #{source}"
      logger.debug "Backtrace: #{e.backtrace.join("\n")}"
    end

    raise ActionView::TemplateError.new(self, {}, e)
  end
end

Here, we start by creating the string that will assign the local variables to their proper values and storing it in the variable locals_code. Then we move on to creating the source code that we want dynamically created and inserted into CompiledTemplates. This code uses the method compiled_source which uses another method named handler. handler is a wrapper method for handler_class_for_extension which is a class method belonging to Template. This method is defined in TemplateHandlers and extended by Template. All of these methods are illustrated below:

# (line 66 action_pack/action_view/renderable.rb)
def handler
  Template.handler_class_for_extension(extension)
end

def compiled_source
  handler.call(self)
end

# (action_pack/action_view/template_handlers_.rb)
module ActionView #:nodoc:
  module TemplateHandlers #:nodoc:
    ...
    def self.extended(base)
      base.register_default_template_handler :erb, TemplateHandlers::ERB
      base.register_template_handler :rjs, TemplateHandlers::RJS
      base.register_template_handler :builder, TemplateHandlers::Builder

      base.register_template_handler :rhtml, TemplateHandlers::ERB
      base.register_template_handler :rxml, TemplateHandlers::Builder
    end
    ...
    def registered_template_handler(extension)
      extension && ``template_handlers[extension.to_sym]
    end
    ...
    def handler_class_for_extension(extension)
      registered_template_handler(extension) || ``default_template_handlers
    end
  end
end

When TemplateHandlers is extended it registers several extension handlers. You are probably familiar with all of them, so we wont go into what each extension is for. handler_class_for_extension returns the result of registered_template_handler or @default_template_handlers. registered_template_handler returns the proper handler (TemplateHandlers::ERB, TemplateHandlers::RJS or TemplateHandlers::MyCustomTemplateHandler) from the class variable @template_handlers which holds the defaults plus any newly registered templates.

So we get the proper handler back (in our case ActionView::TemplateHandlers::ERB) and we execute the call method with our ReloadableTemplate as the parameter. call is a class method in the module TemplateHandlers::Compilable and included into ERB. It looks like this:

# (line 4 action_pack/action_view/template_handlers_.rb)
module Compilable
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def call(template)
      new.compile(template)
    end
  end

  def compile(template)
    raise "Need to implement #{self.class.name}#compile(template)"
  end
end

When we include this module we extend the base class with the module ClassMethods which contains the call method. This method creates a new instance of the base class and calls the compile instance method with the template. The compile in this module raises an error to let you know that it is an abstract method and needs to be defined by the base class, in our case ERB. Letâ??s see what ERB looks like:

# (line 4 action_pack/action_view/template_handlers/erb.rb)
class ERB < TemplateHandler
  include Compilable

  cattr_accessor :erb_trim_mode
  self.erb_trim_mode = '-'
  ...
  def compile(template)
    src = ::ERB.new("<% __in_erb_template=true %>#{template.source}", nil, erb_trim_mode, '@output_buffer').src
    RUBY_VERSION >= '1.9' ? src.sub(/\A#coding:.*\n/, '') : src
  end
end

compile is pretty simple. We create a new instance of ERB parser and pass it the template source and the variable that we want to build the output on (@output_buffer) and returning the src attribute which holds the ruby code generated by the ERB parser. @output_buffer is the name of an attribute inside of ActionView::Base that is used as the output buffer (of course) which will get flushed when we do the final rendering (towards the end of the deliver method). In the case of an AM view, the rendering is pretty simple, but when we get to full controller views with partials we need a place to concat all the different views together which is what we use @output_buffer for. The next line checks for Ruby 1.9 and does some cleaning of the result before returning it. Here is what src outputs for our example:

src => "@output_buffer = '';  __in_erb_template=true ; @output_buffer.concat \"Hi,\\n\"\n@output_buffer.concat \"\\n\"\n@output_buffer.concat \"I am contacting you with this message:\\n\"\n@output_buffer.concat \"\\n\"\n@output_buffer.concat(( `message ).to_s); @output_buffer.concat \"\\n\"\n@output_buffer.concat \"\\n\"\n@output_buffer.concat \"\\n\"\n@output_buffer.concat \"\\n\"\n@output_buffer.concat \"Thanks\\n\"\n@output_buffer.concat \"\\n\"\n@output_buffer.concat \"Jake\"; @output_buffer"

This is one long string that represents the compiled version of the ruby code for our mailer view. Now that we have the compiled_source, letâ??s go back compile!:

# (line 66 action_pack/action_view/renderable.rb)
def compile!(render_symbol, local_assigns)
  locals_code = local_assigns.keys.map { |key| "#{key} = local_assigns[:#{key}];" }.join

  source = <<-end_src
    def #{render_symbol}(local_assigns)
      old_output_buffer = output_buffer;#{locals_code};#{compiled_source}
    ensure
      self.output_buffer = old_output_buffer
    end
  end_src
  ...
end

So now we know what all the pieces mean, letâ??s see what our example becomes:

def _run_erb_app47views47message_mailer47contact_form46erb(local_assigns)
  old_output_buffer = output_buffer;;@output_buffer = '';  __in_erb_template=true ; @output_buffer.concat "Hi,\n"
  @output_buffer.concat "\n"
  @output_buffer.concat "I am contacting you with this message:\n"
  @output_buffer.concat "\n"
  @output_buffer.concat(( `message ).to_s); @output_buffer.concat "\n"
  @output_buffer.concat "\n"
  @output_buffer.concat "\n"
  @output_buffer.concat "\n"@output_buffer.concat "Thanks\n"
  @output_buffer.concat "\n"
  @output_buffer.concat "Jake"; @output_buffer
ensure
  self.output_buffer = old_output_buffer
end

Alright, now we got the code we want evalâ??ed, letâ??s keep going through compile!

# (line 66 action_pack/action_view/renderable.rb)
def compile!(render_symbol, local_assigns)
  ...
  begin
    ActionView::Base::CompiledTemplates.module_eval(source, filename, 0)
  rescue Errno::ENOENT => e
    raise e # Missing template file, re-raise for Base to rescue
  rescue Exception => e # errors from template code
    if logger = defined?(ActionController) && Base.logger
      logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
      logger.debug "Function body: #{source}"
      logger.debug "Backtrace: #{e.backtrace.join("\n")}"
    end

    raise ActionView::TemplateError.new(self, {}, e)
  end
end

Now we open up CompiledTemplates and perform a module_eval with the source we just made and the filename of this template (/example_app/app/views/message_mailer/contact_form.erb) and 0. The filename and the number 0 are used in error reporting during module_eval. Letâ??s say something went wrong with the module_eval, in our case, the error would look something like this:

/example_app/app/views/message_mailer/contact_form.erb:0:in `module_eval': the real problem goes here

Thatâ??s it for this week. Next week we continue with our dynamically compiled method and see what Rails does with it.