THIRD COPY This guide is available on Github
A Refresher
respond_to
is a method that defines different formats that your actions, well, respond to. For those who are unfamiliar with respond_to
or simply forgot about it, here’s a refresher.
Take this example:
class BlogsController < ApplicationController def index @blogs = Blog.all(:limit => 10) respond_to do |format| format.html format.xml end end end
It’s an index action in a controller called BlogsController
and we have a collection of Blog
objects stored in @blogs
. We then call respond_to
and specify the block with a single argument of format
. On format
we call html
and xml
which will render app/views/blogs/index.html.erb and app/views/blogs/index.xml.erb respectively. In those templates we can iterate over the @blogs
collection and do whatever with it that we fancy. There’s a shorter way to write the respond_to
for this:
respond_to :html, :xml
If we want some formats to respond one way, and some others to respond another way we can do this:
respond_to do |format| format.html { @blogs = Blog.all(:limit => 10) } format.any(:atom, :rss) { @blogs = Blog.all } end
In this example index.html.erb’s @blogs
will contain 10 objects, where index.atom.erb’s and index.rss.erb’s @blogs
will contain all the blogs record.
Diving In
My first post in this series was my Timezone Overview for Rails 2.2. Today I would like to cover how mime types and respond_to
work in Rails 2.3. The reason I choose both of these instead of just respond_to
is because they tie in close together.
I’ve tried to make all the methods in this guide clickable with links to the source on Github.
Firstly I’ll talk about MIME types in Rails.
The default MIME types are loaded when Rails starts up by the final line in actionpack/lib/action_controller/mime_type.rb:
require 'action_controller/mime_types'
This loads the actionpack/lib/action_controller/mime_types.rb file and registers the default MIME types:
Mime::Type.register "*/*", :all Mime::Type.register "text/plain", :text, [], %w(txt) Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml ) Mime::Type.register "text/javascript", :js, %w( application/javascript application/x-javascript ) Mime::Type.register "text/css", :css Mime::Type.register "text/calendar", :ics Mime::Type.register "text/csv", :csv Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml ) Mime::Type.register "application/rss+xml", :rss Mime::Type.register "application/atom+xml", :atom Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml ) Mime::Type.register "multipart/form-data", :multipart_form Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form # http://www.ietf.org/rfc/rfc4627.txt # http://www.json.org/JSONRequest.html Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest )
You may recognise the syntax used in mime_types.rb from your app’s config/initializers/mime_types.rb. This file is used to define new MIME types for your application and is loaded on initialization of your application by this line in initializer.rb
load_application_initializers
and the corresponding load_application_initializers
method:
def load_application_initializers if gems_dependencies_loaded Dir["#{configuration.root_path}/config/initializers/**/*.rb"].sort.each do |initializer| load(initializer) end end end
which is responsible for loading all your application’s initializers, including config/initializers/mime_types.rb.
The MIME types defined by Rails and your initializers are the methods that you call on the block object format
or the symbols that you pass to respond_to. You may recognise these from the symbols passed to the Mime::Type.register
calls earlier.
respond_to
’s code may look a bit intimidating at first but it’s not really all that bad:
def respond_to(*types, &block) raise ArgumentError, "respond_to takes either types or a block, never both" unless types.any? ^ block block ||= lambda { |responder| types.each { |type| responder.send(type) } } responder = Responder.new(self) block.call(responder) responder.respond end
If we use the block syntax this will just call
the block
object with the argument of responder
which is defined as Responder.new
.
If we use the single line syntax. this will just pass in the types as an array. This method then defines a lambda
which is a Proc
object just like a usual block. This takes one argument, called responder
and then calls the types on the responder object.
The ||=
on the block definition means “set this variable only if it hasn’t been set already”. The next line is defining the responder
object which triggers initialize
method for Responder
:
class Responder def initialize(controller) @controller = controller @request = controller.request @response = controller.response if ActionController::Base.use_accept_header @mime_type_priority = Array(Mime::Type.lookup_by_extension(@request.parameters[:format]) || @request.accepts) else @mime_type_priority = [@request.format] end @order = [] @responses = {} end ... end
This defines a couple of key variables, namely @mime_type_priority
and @responses
.
The first, @mime_type_priorty
calls Mime::Type.lookup_by_extension(@request.parameters[:format])
which looks like:
def lookup_by_extension(extension) EXTENSION_LOOKUP[extension] end
EXTENSION_LOOKUP
is defined on line #5 of mime_type.rb as a hash:
EXTENSION_LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? }
What happens when a key is looked for on this hash it calls Mime::Type.new(key)
which calls the initialize
method for Mime::Type
:
def initialize(string, symbol = nil, synonyms = []) @symbol, @synonyms = symbol, synonyms @string = string end
We eventually understand that all our original @mime_type_priority
is simply a Mime::Type
object. If we requested HTML, the MIME type would be: #<Mime::Type:0x2624380 @synonyms=["application/xhtml+xml"], @symbol=:html, @string="text/html">
. This element is made into an array and then used in the respond
method.
The second variable, @responses
is defined as an empty hash. The magic happens in the respond
method:
def respond for priority in @mime_type_priority if priority == Mime::ALL @responses[@order.first].call return else if @responses[priority] @responses[priority].call return # mime type match found, be happy and return end end end if @order.include?(Mime::ALL) @responses[Mime::ALL].call else @controller.send :head, :not_acceptable end end
This method iterates over @mime_type_priority
and then checks firstly if the element is a Mime::ALL
This can be achieved by making the format of the URL “all”, such as http://localhost:3000/blogs.all. If the element is not Mime::ALL
this continues iterating through until it finds a format that is defined. It does this by checking if there is a key in the @responses
hash… but it’s empty. So I first thought, too. Buried just over halfway is this method which is called when the file is loaded and this calls generate_method_for_mime
which does some funky class_eval
‘ing:
def self.generate_method_for_mime(mime) sym = mime.is_a?(Symbol) ? mime : mime.to_sym const = sym.to_s.upcase class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{sym}(&block) # def html(&block) custom(Mime::#{const}, &block) # custom(Mime::HTML, &block) end # end RUBY end
Remember back when we were calling responder.send(type)
? This is what it calls, this generated method.
The generated methods take an optional block, as shown by the code for custom
:
def custom(mime_type, &block) mime_type = mime_type.is_a?(Mime::Type) ? mime_type : Mime::Type.lookup(mime_type.to_s) @order << mime_type @responses[mime_type] ||= Proc.new do @response.template.template_format = mime_type.to_sym @response.content_type = mime_type.to_s block_given? ? block.call : @controller.send(:render, :action => @controller.action_name) end end
If the first argument given to custom is not a Mime::Type
object then it will do a lookup and find it. The method will then concatenate into @order
mime_type
. This variable is used for when Mime::ALL
is specified as the mime type, it will use the first one in this list, which will be the first one you’ve called in your respond_to
. If you’ve define your respond_to like this:
def index @blogs = Blog.all(:limit => 10) respond_to do |format| format.html format.json { render :json => @blogs } format.any(:atom, :xml) { @blogs = Blog.all } end end
The all
will call html
because it was defined first. Following on with this example, the format.html
method isn’t passed a block, but the format.xml
is. This determines what this line does in custom
:
block_given? ? block.call : @controller.send(:render, :action => @controller.action_name)
When we don’t pass a block for the format.html
it will render the action, looking for index.html.erb which, in the default scaffold, will list all the blogs. Rails does this by calling:
@controller.send(:render, :action => @controller.action_name)
It calls send
because render
is a protected method. This passes a single argument to render, { :action => @controller.action_name }
which in this example is “index”. render
is a pretty heavy method, weighing in at close to 100 lines, so I won’t bore you with all the details of this method in this post. What I will show you is the part where it processes our :action
option:
elsif action_name = options[:action] render_for_file(default_template(action_name.to_s), options[:status], layout)
The method we’re interested here is not render_for_file
but default_template
:
def default_template(action_name = self.action_name) self.view_paths.find_template(default_template_name(action_name), default_template_format) end
This calls a number of methods, but what we’re interested in here firsty is the default_template_format
method. This method looks like this:
def default_template_format response.template.template_format end
and template_format
looks like:
def template_format if defined? @template_format @template_format elsif controller && controller.respond_to?(:request) @template_format = controller.request.template_format.to_sym else @template_format = :html end end
Our request will match the elsif
in this code which will call another template_format
method, this time on the request
object of our controller. This method looks like this:
def template_format parameter_format = parameters[:format] if parameter_format parameter_format elsif xhr? :js else :html end end
Yes! After all this time it does call parameters[:format]
! This is also known as params[:format]
and, to the well trained "html"
. So way back in our initial default_template
call:
def default_template(action_name = self.action_name) self.view_paths.find_template(default_template_name(action_name), default_template_format) end
We were firstly interested in default_template_format
which is what we just covered. Now we’re interested in find_template
:
def find_template(original_template_path, format = nil, html_fallback = true) return original_template_path if original_template_path.respond_to?(:render) template_path = original_template_path.sub(/^\//, '') each do |load_path| if format && (template = load_path["#{template_path}.#{I18n.locale}.#{format}"]) return template elsif format && (template = load_path["#{template_path}.#{format}"]) return template elsif template = load_path["#{template_path}.#{I18n.locale}"] return template elsif template = load_path[template_path] return template # Try to find html version if the format is javascript elsif format == :js && html_fallback && template = load_path["#{template_path}.#{I18n.locale}.html"] return template elsif format == :js && html_fallback && template = load_path["#{template_path}.html"] return template end end return Template.new(original_template_path) if File.file?(original_template_path) raise MissingTemplate.new(self, original_template_path, format) end
Nothing inside the each
block will match, so it will not return anything. Instead, what is returned is the line after the each block:
return Template.new(original_template_path) if File.file?(original_template_path)
This returns a Template object like this:
#<ActionView::Template:0x2193258 @format=nil, @base_path=nil, @template_path="blogs/", @extension=nil, @locale=nil, @name=nil, @load_path=nil>
render_for_file
takes this object and uses it to render the appropriate template that the user requested.
When we do pass a block to our format.xml
, this is much simpler in the call to render
:
elsif xml = options[:xml] response.content_type ||= Mime::XML render_for_text(xml.respond_to?(:to_xml) ? xml.to_xml : xml, options[:status]) end
This code calls to_xml
on our collection and converts it to an XML document.
Finally, the any
is another method defined on the Responder
object. You can pass to this a collection of mime types and for these mime types it will perform the block. In this example we’ve used:
format.any(:atom, :xml) { @blogs = Blog.all }
but you can also do:
format.any { @blogs = Blog.all }
which will define the response for all currently undefined mime types.
any
goes a bit like this:
def any(*args, &block)
if args.any?
args.each { |type| send(type, &block) }
else
custom(@mime_type_priority.first, &block)
end
end
We’ve passed args to ours, so it’ll just call the methods atom
and xml
on the Responder
object and then render the appropriate views for these.
If we don’t pass arguments to any
, any MIME type that we don’t have a respond_to
line for will call the any
block instead. Take for example, if we had this:
def index @blogs = Blog.all(:limit => 10) respond_to do |format| format.html format.json { render :json => @blogs } format.any { @blogs = Blog.all } end end
And we requested an XML page, http://localhost:3000/blogs.xml, this will trigger the code in the any
block to be ran.
So that wraps up this guide on respond_to. Thanks for reading!