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.