mime_responds.rb 20.0 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
    # Formats can have different variants.
    #
    # The request variant is a specialization of the request format, like <tt>:tablet</tt>,
187
    # <tt>:phone</tt>, or <tt>:desktop</tt>.
Ł
Łukasz Strzałkowski 已提交
188 189 190 191 192 193
    #
    # 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
    # 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
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
    # 
    # Variants also support common `any`/`all` block that formats have.
    #
    # It works for both inline:
    #
    #   respond_to do |format|
    #     format.html.any   { render text: "any"   }
    #     format.html.phone { render text: "phone" }
    #   end
    #
    # and block syntax:
    #
    #   respond_to do |format|
    #     format.html do |variant|
    #       variant.any(:tablet, :phablet){ render text: "any" }
    #       variant.phone { render text: "phone" }
    #     end
    #   end
238
    #
L
Lukasz Strzalkowski 已提交
239 240 241 242 243 244 245 246 247 248 249 250
    # You can also set an array of variants:
    #
    #   request.variant = [:tablet, :phone]
    #
    # 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
    #
X
Xavier Noria 已提交
251 252
    # Be sure to check the documentation of +respond_with+ and
    # <tt>ActionController::MimeResponds.respond_to</tt> for more examples.
253
    def respond_to(*mimes, &block)
254
      raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
255

256
      if collector = retrieve_collector_from_mimes(mimes, &block)
257
        response = collector.response
258
        response ? response.call : render({})
259
      end
J
José Valim 已提交
260
    end
261

262 263
    # For a given controller action, respond_with generates an appropriate
    # response based on the mime-type requested by the client.
264
    #
265
    # If the method is called with just a resource, as in this example -
266
    #
267 268 269 270 271 272 273
    #   class PeopleController < ApplicationController
    #     respond_to :html, :xml, :json
    #
    #     def index
    #       @people = Person.all
    #       respond_with @people
    #     end
J
José Valim 已提交
274 275
    #   end
    #
M
Mark Thomson 已提交
276
    # then the mime-type of the response is typically selected based on the
277
    # request's Accept header and the set of available formats declared
M
Mark Thomson 已提交
278 279 280
    # 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.
281
    #
282
    # If an acceptable format is not identified, the application returns a
M
Mark Thomson 已提交
283
    # '406 - not acceptable' status. Otherwise, the default response is to render
284 285 286 287
    # 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 已提交
288 289
    # * for an html response - if the request method is +get+, an exception
    #   is raised but for other requests such as +post+ the response
290 291 292
    #   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 已提交
293 294
    #   1. If there are no errors, i.e. the resource
    #      was saved successfully, the response +redirect+'s to the resource
295 296
    #      i.e. its +show+ action.
    #   2. If there are validation errors, the response
M
Mark Thomson 已提交
297
    #      renders a default action, which is <tt>:new</tt> for a
A
Akira Matsuda 已提交
298
    #      +post+ request or <tt>:edit</tt> for +patch+ or +put+.
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
    #   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 已提交
317
    #           format.xml { render xml: @user }
318
    #         else
A
AvnerCohen 已提交
319 320
    #           format.html { render action: "new" }
    #           format.xml { render xml: @user }
321 322 323 324 325 326 327
    #         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 已提交
328 329
    #   the resource passed to +respond_with+ responds to <code>to_<format></code>,
    #   the method attempts to render the resource in the requested format
330
    #   directly, e.g. for an xml request, the response is equivalent to calling
A
AvnerCohen 已提交
331
    #   <code>render xml: resource</code>.
332 333 334 335
    #
    # === Nested resources
    #
    # As outlined above, the +resources+ argument passed to +respond_with+
M
Mark Thomson 已提交
336
    # can play two roles. It can be used to generate the redirect url
337 338 339 340 341
    # 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 已提交
342 343
    # For redirecting successful html requests, +respond_with+ also supports
    # the use of nested resources, which are supplied in the same way as
344 345 346 347 348 349 350 351 352 353 354 355
    # 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 已提交
356
    # one specified that is rendered.
357 358 359 360 361
    #
    # === 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. -
362
    #
363 364
    #   def create
    #     @user = User.new(params[:user])
365
    #     flash[:notice] = "User was successfully created." if @user.save
366
    #
367 368 369
    #     respond_with(@user) do |format|
    #       format.html { render }
    #     end
370 371
    #   end
    #
372 373 374 375 376 377
    # 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 已提交
378
    # resource(s) is interpreted as a set of options relevant to all
379
    # formats. Any option accepted by +render+ can be used, e.g.
A
AvnerCohen 已提交
380
    #   respond_with @people, status: 200
381 382 383 384 385 386 387 388 389
    # 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.
390
    def respond_with(*resources, &block)
391 392 393 394
      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
395

396
      if collector = retrieve_collector_from_mimes(&block)
397
        options = resources.size == 1 ? {} : resources.extract_options!
398
        options = options.clone
399
        options[:default_response] = collector.response
400
        (options.delete(:responder) || self.class.responder).call(self, resources, options)
401
      end
402
    end
403 404 405

  protected

J
José Valim 已提交
406 407 408
    # Collect mimes declared in the class method respond_to valid for the
    # current action.
    def collect_mimes_from_class_level #:nodoc:
409
      action = action_name.to_s
410

J
Jeremy Kemper 已提交
411 412
      self.class.mimes_for_respond_to.keys.select do |mime|
        config = self.class.mimes_for_respond_to[mime]
J
José Valim 已提交
413 414

        if config[:except]
415
          !config[:except].include?(action)
J
José Valim 已提交
416
        elsif config[:only]
417
          config[:only].include?(action)
J
José Valim 已提交
418 419
        else
          true
420 421
        end
      end
J
José Valim 已提交
422
    end
423

424 425 426 427 428 429
    # 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.
430
    def retrieve_collector_from_mimes(mimes=nil, &block) #:nodoc:
431
      mimes ||= collect_mimes_from_class_level
432
      collector = Collector.new(mimes, request.variant)
433
      block.call(collector) if block_given?
434
      format = collector.negotiate_format(request)
435

436
      if format
437
        _process_format(format)
438
        collector
439
      else
440
        raise ActionController::UnknownFormat
441 442 443
      end
    end

444 445
    # A container for responses available from the current controller for
    # requests for different mime-types sent to a particular action.
446 447 448 449 450 451 452
    #
    # 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 已提交
453
    #     format.xml { render xml: @people }
454 455 456 457 458 459 460 461 462 463 464 465 466
    #   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
467
      include AbstractController::Collector
468
      attr_accessor :format
469

470
      def initialize(mimes, variant = nil)
471
        @responses = {}
472
        @variant = variant
473 474

        mimes.each { |mime| @responses["Mime::#{mime.upcase}".constantize] = nil }
475
      end
476 477

      def any(*args, &block)
478 479 480
        if args.any?
          args.each { |type| send(type, &block) }
        else
481
          custom(Mime::ALL, &block)
482
        end
483
      end
J
José Valim 已提交
484
      alias :all :any
485 486

      def custom(mime_type, &block)
487
        mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type)
Ł
Łukasz Strzałkowski 已提交
488 489 490
        @responses[mime_type] ||= if block_given?
          block
        else
491
          VariantCollector.new(@variant)
Ł
Łukasz Strzałkowski 已提交
492
        end
493
      end
494

495
      def response
Ł
Łukasz Strzałkowski 已提交
496
        response = @responses.fetch(format, @responses[Mime::ALL])
497 498 499
        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
Ł
Łukasz Strzałkowski 已提交
500
          response
501 502
        else # `format.html{ |variant| variant.phone }` - variant block syntax
          variant_collector = VariantCollector.new(@variant)
L
Lukasz Strzalkowski 已提交
503
          response.call(variant_collector) # call format block with variants collector
504
          variant_collector.variant
Ł
Łukasz Strzałkowski 已提交
505
        end
506 507 508
      end

      def negotiate_format(request)
509
        @format = request.negotiate_mime(@responses.keys)
510
      end
Ł
Łukasz Strzałkowski 已提交
511

Ł
Łukasz Strzałkowski 已提交
512
      class VariantCollector #:nodoc:
513 514
        def initialize(variant = nil)
          @variant = variant
Ł
Łukasz Strzałkowski 已提交
515 516 517
          @variants = {}
        end

518 519 520 521 522 523 524 525
        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
Ł
Łukasz Strzałkowski 已提交
526
        end
527
        alias :all :any
Ł
Łukasz Strzałkowski 已提交
528

529 530
        def method_missing(name, *args, &block)
          @variants[name] = block if block_given?
Ł
Łukasz Strzałkowski 已提交
531 532
        end

533
        def variant
L
Lukasz Strzalkowski 已提交
534 535 536 537 538 539
          if @variant.nil?
            @variants[:none]
          elsif (@variants.keys & @variant).any?
            @variant.each do |v|
              return @variants[v] if @variants.key?(v)
            end
540
          else
L
Lukasz Strzalkowski 已提交
541
            @variants[:any]
542
          end
Ł
Łukasz Strzałkowski 已提交
543 544
        end
      end
545 546
    end
  end
547
end