mime_responds.rb 18.6 KB
Newer Older
1
require 'active_support/core_ext/array/extract_options'
2 3
require 'abstract_controller/collector'

4
module ActionController #:nodoc:
5
  module MimeResponds
6 7 8
    extend ActiveSupport::Concern

    included do
J
Jeremy Kemper 已提交
9
      class_attribute :responder, :mimes_for_respond_to
10
      self.responder = ActionController::Responder
11
      clear_respond_to
12 13 14
    end

    module ClassMethods
15 16
      # Defines mime types that are rendered by default when invoking
      # <tt>respond_with</tt>.
17 18 19
      #
      #   respond_to :html, :xml, :json
      #
20 21
      # Specifies that all actions in the controller respond to requests
      # for <tt>:html</tt>, <tt>:xml</tt> and <tt>:json</tt>.
22
      #
23 24
      # To specify on per-action basis, use <tt>:only</tt> and
      # <tt>:except</tt> with an array of actions or a single action:
25 26
      #
      #   respond_to :html
A
AvnerCohen 已提交
27
      #   respond_to :xml, :json, except: [ :edit ]
28
      #
29 30 31
      # 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>.
32
      #
A
AvnerCohen 已提交
33
      #   respond_to :json, only: :create
34
      #
35
      # This specifies that the <tt>:create</tt> action and no other responds
X
Xavier Noria 已提交
36
      # to <tt>:json</tt>.
37 38 39
      def respond_to(*mimes)
        options = mimes.extract_options!

40 41
        only_actions   = Array(options.delete(:only)).map(&:to_s)
        except_actions = Array(options.delete(:except)).map(&:to_s)
42

J
Jeremy Kemper 已提交
43
        new = mimes_for_respond_to.dup
44 45
        mimes.each do |mime|
          mime = mime.to_sym
J
Jeremy Kemper 已提交
46 47 48
          new[mime]          = {}
          new[mime][:only]   = only_actions   unless only_actions.empty?
          new[mime][:except] = except_actions unless except_actions.empty?
49
        end
J
Jeremy Kemper 已提交
50
        self.mimes_for_respond_to = new.freeze
51 52
      end

53
      # Clear all mime types in <tt>respond_to</tt>.
54
      #
55
      def clear_respond_to
V
Vishnu Atrai 已提交
56
        self.mimes_for_respond_to = Hash.new.freeze
57 58
      end
    end
59

60 61 62 63
    # Without web-service support, an action which collects the data for displaying a list of people
    # might look something like this:
    #
    #   def index
64
    #     @people = Person.all
65 66 67 68 69
    #   end
    #
    # Here's the same action, with web-service support baked in:
    #
    #   def index
70
    #     @people = Person.all
71 72 73
    #
    #     respond_to do |format|
    #       format.html
A
AvnerCohen 已提交
74
    #       format.xml { render xml: @people }
75 76 77 78 79 80 81 82 83 84 85
    #     end
    #   end
    #
    # What that says is, "if the client wants HTML in response to this action, just respond as we
    # would have before, but if the client wants XML, return them the list of people in XML format."
    # (Rails determines the desired response format from the HTTP Accept header submitted by the client.)
    #
    # Supposing you have an action that adds a new person, optionally creating their company
    # (by name) if it does not already exist, without web-services, it might look like this:
    #
    #   def create
86
    #     @company = Company.find_or_create_by(name: params[:company][:name])
87 88 89 90 91 92 93 94 95
    #     @person  = @company.people.create(params[:person])
    #
    #     redirect_to(person_list_url)
    #   end
    #
    # Here's the same action, with web-service support baked in:
    #
    #   def create
    #     company  = params[:person].delete(:company)
96
    #     @company = Company.find_or_create_by(name: company[:name])
97 98 99 100 101
    #     @person  = @company.people.create(params[:person])
    #
    #     respond_to do |format|
    #       format.html { redirect_to(person_list_url) }
    #       format.js
A
AvnerCohen 已提交
102
    #       format.xml  { render xml: @person.to_xml(include: @company) }
103 104 105
    #     end
    #   end
    #
X
Xavier Noria 已提交
106 107
    # If the client wants HTML, we just redirect them back to the person list. If they want JavaScript,
    # then it is an Ajax request and we render the JavaScript template associated with this action.
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
    # Lastly, if the client wants XML, we render the created person as XML, but with a twist: we also
    # include the person's company in the rendered XML, so you get something like this:
    #
    #   <person>
    #     <id>...</id>
    #     ...
    #     <company>
    #       <id>...</id>
    #       <name>...</name>
    #       ...
    #     </company>
    #   </person>
    #
    # Note, however, the extra bit at the top of that action:
    #
    #   company  = params[:person].delete(:company)
124
    #   @company = Company.find_or_create_by(name: company[:name])
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
    #
    # 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):
    #
    #   person[name]=...&person[company][name]=...&...
    #
    # And, like this (xml-encoded):
    #
    #   <person>
    #     <name>...</name>
    #     <company>
    #       <name>...</name>
    #     </company>
    #   </person>
    #
    # In other words, we make the request so that it operates on a single entity's person. Then, in the action,
    # we extract the company data from the request, find or create the company, and then create the new person
    # with the remaining data.
    #
    # Note that you can define your own XML parameter parser which would allow you to describe multiple entities
    # in a single request (i.e., by wrapping them all in a single root node), but if you just go with the flow
    # and accept Rails' defaults, life will be much easier.
    #
    # If you need to use a MIME type which isn't supported by default, you can register your own handlers in
149
    # config/initializers/mime_types.rb as follows.
150 151
    #
    #   Mime::Type.register "image/jpg", :jpg
J
José Valim 已提交
152 153 154 155
    #
    # Respond to also allows you to specify a common block for different formats by using any:
    #
    #   def index
156
    #     @people = Person.all
J
José Valim 已提交
157 158 159 160 161 162 163 164 165
    #
    #     respond_to do |format|
    #       format.html
    #       format.any(:xml, :json) { render request.format.to_sym => @people }
    #     end
    #   end
    #
    # In the example above, if the format is xml, it will render:
    #
A
AvnerCohen 已提交
166
    #   render xml: @people
J
José Valim 已提交
167 168 169
    #
    # Or if the format is json:
    #
A
AvnerCohen 已提交
170
    #   render json: @people
J
José Valim 已提交
171 172 173 174 175 176 177 178
    #
    # Since this is a common pattern, you can use the class method respond_to
    # with the respond_with method to have the same results:
    #
    #   class PeopleController < ApplicationController
    #     respond_to :html, :xml, :json
    #
    #     def index
179
    #       @people = Person.all
V
Vijay Dev 已提交
180
    #       respond_with(@people)
J
José Valim 已提交
181 182 183
    #     end
    #   end
    #
Ł
Łukasz Strzałkowski 已提交
184 185 186 187 188 189 190 191 192 193
    # Formats can have different variants.
    #
    # The request variant is a specialization of the request format, like <tt>:tablet</tt>,
    # <tt>:phone</tt>, or <tt>:desktop<tt>.
    #
    # We often want to render different html/json/xml templates for phones,
    # tablets, and desktop browsers. Variants make it easy.
    #
    # You can set the variant in a +before_action+:
    #
194
    #   request.variant = :tablet if request.user_agent =~ /iPad/
Ł
Łukasz Strzałkowski 已提交
195 196 197 198
    #
    # Respond to variants in the action just like you respond to formats:
    #
    #   respond_to do |format|
199 200 201 202
    #     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
Ł
Łukasz Strzałkowski 已提交
203 204 205 206 207 208 209 210 211
    #     end
    #   end
    #
    # Provide separate templates for each format and variant:
    #
    #   app/views/projects/show.html.erb
    #   app/views/projects/show.html+tablet.erb
    #   app/views/projects/show.html+phone.erb
    #
212 213 214 215 216 217 218 219 220
    # When you're not sharing any code within the format, you can simplify defining variants
    # using the inline syntax:
    #
    #   respond_to do |format|
    #     format.js         { render "trash" }
    #     format.html.phone { redirect_to progress_path }
    #     format.html.none  { render "trash" }
    #   end
    #
X
Xavier Noria 已提交
221 222
    # Be sure to check the documentation of +respond_with+ and
    # <tt>ActionController::MimeResponds.respond_to</tt> for more examples.
223
    def respond_to(*mimes, &block)
224
      raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
225

226
      if collector = retrieve_collector_from_mimes(mimes, &block)
227
        response = collector.response(request.variant)
228
        response ? response.call : render({})
229
      end
J
José Valim 已提交
230
    end
231

232 233
    # For a given controller action, respond_with generates an appropriate
    # response based on the mime-type requested by the client.
234
    #
235
    # If the method is called with just a resource, as in this example -
236
    #
237 238 239 240 241 242 243
    #   class PeopleController < ApplicationController
    #     respond_to :html, :xml, :json
    #
    #     def index
    #       @people = Person.all
    #       respond_with @people
    #     end
J
José Valim 已提交
244 245
    #   end
    #
M
Mark Thomson 已提交
246
    # then the mime-type of the response is typically selected based on the
247
    # request's Accept header and the set of available formats declared
M
Mark Thomson 已提交
248 249 250
    # 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.
251
    #
252
    # If an acceptable format is not identified, the application returns a
M
Mark Thomson 已提交
253
    # '406 - not acceptable' status. Otherwise, the default response is to render
254 255 256 257
    # 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:
    #
M
Mark Thomson 已提交
258 259
    # * for an html response - if the request method is +get+, an exception
    #   is raised but for other requests such as +post+ the response
260 261 262
    #   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) -
M
Mark Thomson 已提交
263 264
    #   1. If there are no errors, i.e. the resource
    #      was saved successfully, the response +redirect+'s to the resource
265 266
    #      i.e. its +show+ action.
    #   2. If there are validation errors, the response
M
Mark Thomson 已提交
267
    #      renders a default action, which is <tt>:new</tt> for a
A
Akira Matsuda 已提交
268
    #      +post+ request or <tt>:edit</tt> for +patch+ or +put+.
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
    #   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
    #
    #   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) }
A
AvnerCohen 已提交
287
    #           format.xml { render xml: @user }
288
    #         else
A
AvnerCohen 已提交
289 290
    #           format.html { render action: "new" }
    #           format.xml { render xml: @user }
291 292 293 294 295 296 297
    #         end
    #       end
    #     end
    #
    # * 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
M
Mark Thomson 已提交
298 299
    #   the resource passed to +respond_with+ responds to <code>to_<format></code>,
    #   the method attempts to render the resource in the requested format
300
    #   directly, e.g. for an xml request, the response is equivalent to calling
A
AvnerCohen 已提交
301
    #   <code>render xml: resource</code>.
302 303 304 305
    #
    # === Nested resources
    #
    # As outlined above, the +resources+ argument passed to +respond_with+
M
Mark Thomson 已提交
306
    # can play two roles. It can be used to generate the redirect url
307 308 309 310 311
    # 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).
    #
M
Mark Thomson 已提交
312 313
    # For redirecting successful html requests, +respond_with+ also supports
    # the use of nested resources, which are supplied in the same way as
314 315 316 317 318 319 320 321 322 323 324 325
    # in <code>form_for</code> and <code>polymorphic_url</code>. For example -
    #
    #   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)
    #   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
M
Mark Thomson 已提交
326
    # one specified that is rendered.
327 328 329 330 331
    #
    # === Customizing response behavior
    #
    # 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. -
332
    #
333 334
    #   def create
    #     @user = User.new(params[:user])
335
    #     flash[:notice] = "User was successfully created." if @user.save
336
    #
337 338 339
    #     respond_with(@user) do |format|
    #       format.html { render }
    #     end
340 341
    #   end
    #
342 343 344 345 346 347
    # 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
M
Mark Thomson 已提交
348
    # resource(s) is interpreted as a set of options relevant to all
349
    # formats. Any option accepted by +render+ can be used, e.g.
A
AvnerCohen 已提交
350
    #   respond_with @people, status: 200
351 352 353 354 355 356 357 358 359
    # 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.
360
    def respond_with(*resources, &block)
361 362 363 364
      if self.class.mimes_for_respond_to.empty?
        raise "In order to use respond_with, first you need to declare the " \
          "formats your controller responds to in the class level."
      end
365

366
      if collector = retrieve_collector_from_mimes(&block)
367
        options = resources.size == 1 ? {} : resources.extract_options!
368
        options = options.clone
369
        options[:default_response] = collector.response(request.variant)
370
        (options.delete(:responder) || self.class.responder).call(self, resources, options)
371
      end
372
    end
373 374 375

  protected

J
José Valim 已提交
376 377 378
    # Collect mimes declared in the class method respond_to valid for the
    # current action.
    def collect_mimes_from_class_level #:nodoc:
379
      action = action_name.to_s
380

J
Jeremy Kemper 已提交
381 382
      self.class.mimes_for_respond_to.keys.select do |mime|
        config = self.class.mimes_for_respond_to[mime]
J
José Valim 已提交
383 384

        if config[:except]
385
          !config[:except].include?(action)
J
José Valim 已提交
386
        elsif config[:only]
387
          config[:only].include?(action)
J
José Valim 已提交
388 389
        else
          true
390 391
        end
      end
J
José Valim 已提交
392
    end
393

394 395 396 397 398 399
    # 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.
400
    def retrieve_collector_from_mimes(mimes=nil, &block) #:nodoc:
401
      mimes ||= collect_mimes_from_class_level
402
      collector = Collector.new(mimes)
403
      block.call(collector) if block_given?
404
      format = collector.negotiate_format(request)
405

406
      if format
407
        _process_format(format)
408
        collector
409
      else
410
        raise ActionController::UnknownFormat
411 412 413
      end
    end

414 415
    # A container for responses available from the current controller for
    # requests for different mime-types sent to a particular action.
416 417 418 419 420 421 422
    #
    # 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.
    # for +respond_to+ :
    #
    #   respond_to do |format|
    #     format.html
A
AvnerCohen 已提交
423
    #     format.xml { render xml: @people }
424 425 426 427 428 429 430 431 432 433 434 435 436
    #   end
    #
    # In this usage, the argument passed to the block (+format+ above) is an
    # instance of the ActionController::MimeResponds::Collector class. This
    # object serves as a container in which available responses can be stored by
    # calling any of the dynamically generated, mime-type-specific methods such
    # as +html+, +xml+ etc on the Collector. Each response is represented by a
    # corresponding block if present.
    #
    # A subsequent call to #negotiate_format(request) will enable the Collector
    # to determine which specific mime-type it should respond with for the current
    # request, with this response then being accessible by calling #response.
    class Collector
437
      include AbstractController::Collector
438
      attr_accessor :format
439

440
      def initialize(mimes)
441
        @responses = {}
442 443

        mimes.each { |mime| @responses["Mime::#{mime.upcase}".constantize] = nil }
444
      end
445 446

      def any(*args, &block)
447 448 449
        if args.any?
          args.each { |type| send(type, &block) }
        else
450
          custom(Mime::ALL, &block)
451
        end
452
      end
J
José Valim 已提交
453
      alias :all :any
454 455

      def custom(mime_type, &block)
456
        mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type)
Ł
Łukasz Strzałkowski 已提交
457 458 459 460 461
        @responses[mime_type] ||= if block_given?
          block
        else
          VariantCollector.new
        end
462
      end
463

464
      def response(variant)
Ł
Łukasz Strzałkowski 已提交
465
        response = @responses.fetch(format, @responses[Mime::ALL])
Ł
Łukasz Strzałkowski 已提交
466 467 468
        if response.is_a?(VariantCollector)
          response.variant(variant)
        elsif response.nil? || response.arity == 0
Ł
Łukasz Strzałkowski 已提交
469 470
          response
        else
471
          lambda { response.call VariantFilter.new(variant) }
Ł
Łukasz Strzałkowski 已提交
472
        end
473 474 475
      end

      def negotiate_format(request)
476
        @format = request.negotiate_mime(@responses.keys)
477
      end
Ł
Łukasz Strzałkowski 已提交
478

Ł
Łukasz Strzałkowski 已提交
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494
      #Used for inline syntax
      class VariantCollector #:nodoc:
        def initialize
          @variants = {}
        end

        def method_missing(name, *args, &block)
          @variants[name] = block if block_given?
        end

        def variant(name)
          @variants[name.nil? ? :none : name]
        end
      end

      #Used for nested block syntax
495
      class VariantFilter #:nodoc:
Ł
Łukasz Strzałkowski 已提交
496 497 498 499 500
        def initialize(variant)
          @variant = variant
        end

        def method_missing(name)
501 502 503
          if block_given?
            yield if name == @variant || (name == :none && @variant.nil?)
          end
Ł
Łukasz Strzałkowski 已提交
504 505
        end
      end
506 507
    end
  end
508
end