# Provides an extension for Rails respond_to by expading MimeResponds::Responder
# and adding respond_to class method and respond_with instance method.
#
module ActionController #:nodoc:
class Base #:nodoc:
protected
# Defines respond_to method to store formats that are rendered by default.
#
# Examples:
#
# respond_to :html, :xml, :json
#
# All actions on your controller will respond to :html, :xml and :json.
# But if you want to specify it based on your actions, you can use only and
# except:
#
# respond_to :html
# respond_to :xml, :json, :except => [ :edit ]
#
# The definition above explicits that all actions respond to :html. And all
# actions except :edit respond to :xml and :json.
#
# You can specify also only parameters:
#
# respond_to :rjs, :only => :create
#
# Which would be the same as:
#
# respond_to :rjs => :create
#
def self.respond_to(*formats)
options = formats.extract_options!
formats_hash = {}
only_actions = Array(options.delete(:only))
except_actions = Array(options.delete(:except))
only_actions.map!{ |a| a.to_sym }
except_actions.map!{ |a| a.to_sym }
formats.each do |format|
formats_hash[format.to_sym] = {}
formats_hash[format.to_sym][:only] = only_actions unless only_actions.empty?
formats_hash[format.to_sym][:except] = except_actions unless except_actions.empty?
end
options.each do |format, actions|
formats_hash[format.to_sym] = {}
next if actions == :all || actions == 'all'
actions = Array(actions)
actions.map!{ |a| a.to_sym }
formats_hash[format.to_sym][:only] = actions unless actions.empty?
end
write_inheritable_hash(:formats_for_respond_to, formats_hash)
end
class_inheritable_reader :formats_for_respond_to
# Define defaults respond_to
respond_to :html
respond_to :xml, :except => [ :edit ]
# Method to clear all respond_to declared until the current controller.
# This is like freeing the controller from the inheritance chain. :)
#
def self.clear_respond_to!
formats = formats_for_respond_to
formats.each { |k,v| formats[k] = { :only => [] } }
write_inheritable_hash(:formats_for_respond_to, formats)
end
# respond_with accepts an object and tries to render a view based in the
# controller and actions that called respond_with. If the view cannot be
# found, it will try to call :to_format in the object.
#
# class ProjectsController < ApplicationController
# respond_to :html, :xml
#
# def show
# @project = Project.find(:id)
# respond_with(@project)
# end
# end
#
# When the client request a xml, we will check first for projects/show.xml
# if it can't be found, we will call :to_xml in the object @project. If the
# object eventually doesn't respond to :to_xml it will render 404.
#
# If you want to overwrite the formats specified in the class, you can
# send your new formats using the options :to.
#
# def show
# @project = Project.find(:id)
# respond_with(@project, :to => :json)
# end
#
# That means that this action will ONLY reply to json requests.
#
# All other options sent will be forwarded to the render method. So you can
# do:
#
# def create
# # ...
# if @project.save
# respond_with(@project, :status => :ok, :location => @project)
# else
# respond_with(@project.errors, :status => :unprocessable_entity)
# end
# end
#
# respond_with does not accept blocks, if you want advanced configurations
# check respond_to method sending :with => @object as option.
#
# Returns true if anything is rendered. Returns false otherwise.
#
def respond_with(object, options = {})
attempt_to_respond = false
# You can also send a responder object as parameter.
#
responder = options.delete(:responder) || Responder.new(self)
# Check for given mime types
#
mime_types = Array(options.delete(:to))
mime_types.map!{ |mime| mime.to_sym }
# If :skip_not_acceptable is sent, it will not render :not_acceptable
# if the mime type sent by the client cannot be found.
#
skip_not_acceptable = options.delete(:skip_not_acceptable)
for priority in responder.mime_type_priority
if priority == Mime::ALL && template_exists?
render options.merge(:action => action_name)
return true
elsif responder.action_respond_to_format?(priority.to_sym, mime_types)
attempt_to_respond = true
response.template.template_format = priority.to_sym
response.content_type = priority.to_s
if template_exists?
render options.merge(:action => action_name)
return true
elsif object.respond_to?(:"to_#{priority.to_sym}")
render options.merge(:text => object.send(:"to_#{priority.to_sym}"))
return true
end
end
end
# If we got here we could not render the object. But if attempted to
# render (this means, the format sent by the client was valid) we should
# render a 404.
#
# If we even didn't attempt to respond, we respond :not_acceptable
# unless is told otherwise.
#
if attempt_to_respond
render :text => '404 Not Found', :status => 404
return true
elsif !skip_not_acceptable
head :not_acceptable
return false
end
return false
end
# Extends respond_to behaviour.
#
# You can now pass objects using the options :with.
#
# respond_to(:html, :xml, :rjs, :with => @project)
#
# If you pass an object and send any block, it's exactly the same as:
#
# respond_with(@project, :to => [:html, :xml, :rjs])
#
# But the main difference of respond_to and respond_with is that the first
# allows further customizations:
#
# respond_to(:html, :with => @project) do |format|
# format.xml { render :xml => @project.errors }
# end
#
# It's the same as:
#
# 1. When responding to html, execute respond_with(@object).
# 2. When accessing a xml, execute the block given.
#
# Formats defined in blocks have precedence to formats sent as arguments.
# In other words, if you pass a format as argument and as block, the block
# will always be executed.
#
# And as in respond_with, all extra options sent will be forwarded to
# the render method:
#
# respond_to(:with => @projects.errors, :status => :unprocessable_entity) do |format|
# format.html { render :template => 'new' }
# end
#
def respond_to(*types, &block)
options = types.extract_options!
object = options.delete(:with)
responder = Responder.new(self)
# This is the default respond_to behaviour, when no object is given.
if object.nil?
block ||= lambda { |responder| types.each { |type| responder.send(type) } }
block.call(responder)
responder.respond
return true # we are done here
else
# If a block is given, it checks if we can perform the requested format.
#
# Even if Mime::ALL is sent by the client, we do not respond_to it now.
# This is done using calling :respond_to_block instead of :respond.
#
# It's worth to remember that responder_to_block does not respond
# :not_acceptable also.
#
if block_given?
block.call(responder)
responder.respond_to_block
return true if responder.responded? || performed?
end
# Let's see if we get lucky rendering with :respond_with.
# At the end, respond_with checks for Mime::ALL if any template exist.
#
# Notice that we are sending the responder (for performance gain) and
# sending :skip_not_acceptable because we don't want to respond
# :not_acceptable yet.
#
if respond_with(object, options.merge(:to => types, :responder => responder, :skip_not_acceptable => true))
return true
# Since respond_with couldn't help us, our last chance is to reply to
# any block given if the user send all as mime type.
#
elsif block_given?
return true if responder.respond_to_all
end
end
# If we get here it means that we could not satisfy our request.
# Now we finally return :not_acceptable.
#
head :not_acceptable
return false
end
private
# Define template_exists? for Rails 2.3
unless ActionController::Base.private_instance_methods.include? 'template_exists?'
def template_exists?
self.view_paths.find_template("#{controller_name}/#{action_name}", response.template.template_format)
rescue ActionView::MissingTemplate
false
end
end
# If ApplicationController is already defined around here, we should call
# inherited_with_inheritable_attributes to insert formats_for_respond_to.
# This usually happens only on Rails 2.3.
#
if defined?(ApplicationController)
self.send(:inherited_with_inheritable_attributes, ApplicationController)
end
end
module MimeResponds #:nodoc:
class Responder #:nodoc:
# Create an attr_reader for @mime_type_priority
attr_reader :mime_type_priority
# Stores if this Responder instance called any block.
def responded?; @responded; end
# Similar as respond but if we can't find a valid mime type,
# we do not send :not_acceptable message as head.
#
# It does not respond to Mime::ALL in priority as well.
#
def respond_to_block
for priority in @mime_type_priority
next if priority == Mime::ALL
if @responses[priority]
@responses[priority].call
return (@responded = true) # mime type match found, be happy and return
end
end
if @order.include?(Mime::ALL)
@responses[Mime::ALL].call
return (@responded = true)
else
return (@responded = false)
end
end
# Respond to the first format given if Mime::ALL is included in the
# mime type priorites. This is the behaviour expected when the client
# sends "*/*" as mime type.
#
def respond_to_all
if @mime_type_priority.include?(Mime::ALL) && first = @responses[@order.first]
first.call
return (@responded = true)
end
end
# Receives an format and checks if the current action responds to
# the given format. If additional mimes are sent, only them are checked.
#
def action_respond_to_format?(format, additional_mimes = [])
if !additional_mimes.blank?
additional_mimes.include?(format.to_sym)
elsif formats = @controller.formats_for_respond_to[format.to_sym]
if formats[:only]
formats[:only].include?(@controller.action_name.to_sym)
elsif formats[:except]
!formats[:except].include?(@controller.action_name.to_sym)
else
true
end
else
false
end
end
end
end
end