diff options
Diffstat (limited to 'actionpack/lib/action_controller/metal/mime_responds.rb')
-rw-r--r-- | actionpack/lib/action_controller/metal/mime_responds.rb | 363 |
1 files changed, 129 insertions, 234 deletions
diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 6bf306ac5b..ac1f209232 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -1,59 +1,26 @@ +require 'active_support/core_ext/array/extract_options' require 'abstract_controller/collector' module ActionController #:nodoc: module MimeResponds extend ActiveSupport::Concern - included do - class_attribute :responder, :mimes_for_respond_to - self.responder = ActionController::Responder - clear_respond_to - end - module ClassMethods - # Defines mime types that are rendered by default when invoking - # <tt>respond_with</tt>. - # - # respond_to :html, :xml, :json - # - # Specifies that all actions in the controller respond to requests - # for <tt>:html</tt>, <tt>:xml</tt> and <tt>:json</tt>. - # - # To specify on per-action basis, use <tt>:only</tt> and - # <tt>:except</tt> with an array of actions or a single action: - # - # respond_to :html - # respond_to :xml, :json, except: [ :edit ] - # - # This specifies that all actions respond to <tt>:html</tt> - # and all actions except <tt>:edit</tt> respond to <tt>:xml</tt> and - # <tt>:json</tt>. - # - # respond_to :json, only: :create - # - # This specifies that the <tt>:create</tt> action and no other responds - # to <tt>:json</tt>. - def respond_to(*mimes) - options = mimes.extract_options! - - only_actions = Array(options.delete(:only)).map(&:to_s) - except_actions = Array(options.delete(:except)).map(&:to_s) - - new = mimes_for_respond_to.dup - mimes.each do |mime| - mime = mime.to_sym - new[mime] = {} - new[mime][:only] = only_actions unless only_actions.empty? - new[mime][:except] = except_actions unless except_actions.empty? - end - self.mimes_for_respond_to = new.freeze + def respond_to(*) + raise NoMethodError, "The controller-level `respond_to' feature has " \ + "been extracted to the `responders` gem. Add it to your Gemfile to " \ + "continue using this feature:\n" \ + " gem 'responders', '~> 2.0'\n" \ + "Consult the Rails upgrade guide for details." end + end - # Clear all mime types in <tt>respond_to</tt>. - # - def clear_respond_to - self.mimes_for_respond_to = Hash.new.freeze - end + def respond_with(*) + raise NoMethodError, "The `respond_with' feature has been extracted " \ + "to the `responders` gem. Add it to your Gemfile to continue using " \ + "this feature:\n" \ + " gem 'responders', '~> 2.0'\n" \ + "Consult the Rails upgrade guide for details." end # Without web-service support, an action which collects the data for displaying a list of people @@ -82,7 +49,7 @@ module ActionController #:nodoc: # (by name) if it does not already exist, without web-services, it might look like this: # # def create - # @company = Company.find_or_create_by_name(params[:company][:name]) + # @company = Company.find_or_create_by(name: params[:company][:name]) # @person = @company.people.create(params[:person]) # # redirect_to(person_list_url) @@ -92,7 +59,7 @@ module ActionController #:nodoc: # # def create # company = params[:person].delete(:company) - # @company = Company.find_or_create_by_name(company[:name]) + # @company = Company.find_or_create_by(name: company[:name]) # @person = @company.people.create(params[:person]) # # respond_to do |format| @@ -120,7 +87,7 @@ module ActionController #:nodoc: # Note, however, the extra bit at the top of that action: # # company = params[:person].delete(:company) - # @company = Company.find_or_create_by_name(company[:name]) + # @company = Company.find_or_create_by(name: company[:name]) # # This is because the incoming XML document (if a web-service request is in process) can only contain a # single root-node. So, we have to rearrange things so that the request looks like this (url-encoded): @@ -168,205 +135,85 @@ module ActionController #:nodoc: # # render json: @people # - # Since this is a common pattern, you can use the class method respond_to - # with the respond_with method to have the same results: + # Formats can have different variants. # - # class PeopleController < ApplicationController - # respond_to :html, :xml, :json + # The request variant is a specialization of the request format, like <tt>:tablet</tt>, + # <tt>:phone</tt>, or <tt>:desktop</tt>. # - # def index - # @people = Person.all - # respond_with(@people) - # end - # end + # We often want to render different html/json/xml templates for phones, + # tablets, and desktop browsers. Variants make it easy. # - # Be sure to check the documentation of +respond_with+ and - # <tt>ActionController::MimeResponds.respond_to</tt> for more examples. - def respond_to(*mimes, &block) - raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given? - - if collector = retrieve_collector_from_mimes(mimes, &block) - response = collector.response - response ? response.call : render({}) - end - end - - # For a given controller action, respond_with generates an appropriate - # response based on the mime-type requested by the client. + # You can set the variant in a +before_action+: # - # If the method is called with just a resource, as in this example - + # request.variant = :tablet if request.user_agent =~ /iPad/ # - # class PeopleController < ApplicationController - # respond_to :html, :xml, :json + # Respond to variants in the action just like you respond to formats: # - # def index - # @people = Person.all - # respond_with @people + # respond_to do |format| + # format.html do |variant| + # variant.tablet # renders app/views/projects/show.html+tablet.erb + # variant.phone { extra_setup; render ... } + # variant.none { special_setup } # executed only if there is no variant set # end # end # - # then the mime-type of the response is typically selected based on the - # request's Accept header and the set of available formats declared - # by previous calls to the controller's class method +respond_to+. Alternatively - # the mime-type can be selected by explicitly setting <tt>request.format</tt> in - # the controller. - # - # If an acceptable format is not identified, the application returns a - # '406 - not acceptable' status. Otherwise, the default response is to render - # a template named after the current action and the selected format, - # e.g. <tt>index.html.erb</tt>. If no template is available, the behavior - # depends on the selected format: - # - # * for an html response - if the request method is +get+, an exception - # is raised but for other requests such as +post+ the response - # depends on whether the resource has any validation errors (i.e. - # assuming that an attempt has been made to save the resource, - # e.g. by a +create+ action) - - # 1. If there are no errors, i.e. the resource - # was saved successfully, the response +redirect+'s to the resource - # i.e. its +show+ action. - # 2. If there are validation errors, the response - # renders a default action, which is <tt>:new</tt> for a - # +post+ request or <tt>:edit</tt> for +put+. - # Thus an example like this - - # - # respond_to :html, :xml - # - # def create - # @user = User.new(params[:user]) - # flash[:notice] = 'User was successfully created.' if @user.save - # respond_with(@user) - # end + # Provide separate templates for each format and variant: # - # is equivalent, in the absence of <tt>create.html.erb</tt>, to - - # - # def create - # @user = User.new(params[:user]) - # respond_to do |format| - # if @user.save - # flash[:notice] = 'User was successfully created.' - # format.html { redirect_to(@user) } - # format.xml { render xml: @user } - # else - # format.html { render action: "new" } - # format.xml { render xml: @user } - # end - # end - # end + # app/views/projects/show.html.erb + # app/views/projects/show.html+tablet.erb + # app/views/projects/show.html+phone.erb # - # * for a javascript request - if the template isn't found, an exception is - # raised. - # * for other requests - i.e. data formats such as xml, json, csv etc, if - # the resource passed to +respond_with+ responds to <code>to_<format></code>, - # the method attempts to render the resource in the requested format - # directly, e.g. for an xml request, the response is equivalent to calling - # <code>render xml: resource</code>. + # When you're not sharing any code within the format, you can simplify defining variants + # using the inline syntax: # - # === Nested resources + # respond_to do |format| + # format.js { render "trash" } + # format.html.phone { redirect_to progress_path } + # format.html.none { render "trash" } + # end # - # As outlined above, the +resources+ argument passed to +respond_with+ - # can play two roles. It can be used to generate the redirect url - # for successful html requests (e.g. for +create+ actions when - # no template exists), while for formats other than html and javascript - # it is the object that gets rendered, by being converted directly to the - # required format (again assuming no template exists). + # Variants also support common `any`/`all` block that formats have. # - # For redirecting successful html requests, +respond_with+ also supports - # the use of nested resources, which are supplied in the same way as - # in <code>form_for</code> and <code>polymorphic_url</code>. For example - + # It works for both inline: # - # def create - # @project = Project.find(params[:project_id]) - # @task = @project.comments.build(params[:task]) - # flash[:notice] = 'Task was successfully created.' if @task.save - # respond_with(@project, @task) + # respond_to do |format| + # format.html.any { render text: "any" } + # format.html.phone { render text: "phone" } # end # - # This would cause +respond_with+ to redirect to <code>project_task_url</code> - # instead of <code>task_url</code>. For request formats other than html or - # javascript, if multiple resources are passed in this way, it is the last - # one specified that is rendered. + # and block syntax: # - # === Customizing response behavior + # respond_to do |format| + # format.html do |variant| + # variant.any(:tablet, :phablet){ render text: "any" } + # variant.phone { render text: "phone" } + # end + # end # - # Like +respond_to+, +respond_with+ may also be called with a block that - # can be used to overwrite any of the default responses, e.g. - + # You can also set an array of variants: # - # def create - # @user = User.new(params[:user]) - # flash[:notice] = "User was successfully created." if @user.save + # request.variant = [:tablet, :phone] # - # respond_with(@user) do |format| - # format.html { render } - # end + # which will work similarly to formats and MIME types negotiation. If there will be no + # :tablet variant declared, :phone variant will be picked: + # + # respond_to do |format| + # format.html.none + # format.html.phone # this gets rendered # end # - # The argument passed to the block is an ActionController::MimeResponds::Collector - # object which stores the responses for the formats defined within the - # block. Note that formats with responses defined explicitly in this way - # do not have to first be declared using the class method +respond_to+. - # - # Also, a hash passed to +respond_with+ immediately after the specified - # resource(s) is interpreted as a set of options relevant to all - # formats. Any option accepted by +render+ can be used, e.g. - # respond_with @people, status: 200 - # However, note that these options are ignored after an unsuccessful attempt - # to save a resource, e.g. when automatically rendering <tt>:new</tt> - # after a post request. - # - # Two additional options are relevant specifically to +respond_with+ - - # 1. <tt>:location</tt> - overwrites the default redirect location used after - # a successful html +post+ request. - # 2. <tt>:action</tt> - overwrites the default render action used after an - # unsuccessful html +post+ request. - def respond_with(*resources, &block) - raise "In order to use respond_with, first you need to declare the formats your " << - "controller responds to in the class level" if self.class.mimes_for_respond_to.empty? - - if collector = retrieve_collector_from_mimes(&block) - options = resources.size == 1 ? {} : resources.extract_options! - options[:default_response] = collector.response - (options.delete(:responder) || self.class.responder).call(self, resources, options) - end - end - - protected - - # Collect mimes declared in the class method respond_to valid for the - # current action. - def collect_mimes_from_class_level #:nodoc: - action = action_name.to_s - - self.class.mimes_for_respond_to.keys.select do |mime| - config = self.class.mimes_for_respond_to[mime] - - if config[:except] - !config[:except].include?(action) - elsif config[:only] - config[:only].include?(action) - else - true - end - end - end + # Be sure to check the documentation of <tt>ActionController::MimeResponds.respond_to</tt> + # for more examples. + def respond_to(*mimes) + raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given? - # Returns a Collector object containing the appropriate mime-type response - # for the current request, based on the available responses defined by a block. - # In typical usage this is the block passed to +respond_with+ or +respond_to+. - # - # Sends :not_acceptable to the client and returns nil if no suitable format - # is available. - def retrieve_collector_from_mimes(mimes=nil, &block) #:nodoc: - mimes ||= collect_mimes_from_class_level - collector = Collector.new(mimes) - block.call(collector) if block_given? - format = collector.negotiate_format(request) + collector = Collector.new(mimes, request.variant) + yield collector if block_given? - if format - self.content_type ||= format.to_s - lookup_context.formats = [format.to_sym] - lookup_context.rendered_format = lookup_context.formats.first - collector + if format = collector.negotiate_format(request) + _process_format(format) + response = collector.response + response ? response.call : render({}) else raise ActionController::UnknownFormat end @@ -375,8 +222,8 @@ module ActionController #:nodoc: # A container for responses available from the current controller for # requests for different mime-types sent to a particular action. # - # The public controller methods +respond_with+ and +respond_to+ may be called - # with a block that is used to define responses to different mime-types, e.g. + # The public controller methods +respond_to+ may be called with a block + # that is used to define responses to different mime-types, e.g. # for +respond_to+ : # # respond_to do |format| @@ -396,11 +243,13 @@ module ActionController #:nodoc: # request, with this response then being accessible by calling #response. class Collector include AbstractController::Collector - attr_accessor :order, :format + attr_accessor :format + + def initialize(mimes, variant = nil) + @responses = {} + @variant = variant - def initialize(mimes) - @order, @responses = [], {} - mimes.each { |mime| send(mime) } + mimes.each { |mime| @responses["Mime::#{mime.upcase}".constantize] = nil } end def any(*args, &block) @@ -414,16 +263,62 @@ module ActionController #:nodoc: def custom(mime_type, &block) mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type) - @order << mime_type - @responses[mime_type] ||= block + @responses[mime_type] ||= if block_given? + block + else + VariantCollector.new(@variant) + end end def response - @responses[format] || @responses[Mime::ALL] + response = @responses.fetch(format, @responses[Mime::ALL]) + if response.is_a?(VariantCollector) # `format.html.phone` - variant inline syntax + response.variant + elsif response.nil? || response.arity == 0 # `format.html` - just a format, call its block + response + else # `format.html{ |variant| variant.phone }` - variant block syntax + variant_collector = VariantCollector.new(@variant) + response.call(variant_collector) # call format block with variants collector + variant_collector.variant + end end def negotiate_format(request) - @format = request.negotiate_mime(order) + @format = request.negotiate_mime(@responses.keys) + end + + class VariantCollector #:nodoc: + def initialize(variant = nil) + @variant = variant + @variants = {} + end + + def any(*args, &block) + if block_given? + if args.any? && args.none?{ |a| a == @variant } + args.each{ |v| @variants[v] = block } + else + @variants[:any] = block + end + end + end + alias :all :any + + def method_missing(name, *args, &block) + @variants[name] = block if block_given? + end + + def variant + if @variant.nil? + @variants[:none] || @variants[:any] + elsif (@variants.keys & @variant).any? + @variant.each do |v| + return @variants[v] if @variants.key?(v) + end + else + @variants[:any] + end + end end end end |