mime_responds.rb 17.5 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 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
    # 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+:
    #
    #     request.variant = :tablet if request.user_agent =~ /iPad/
    #
    # Respond to variants in the action just like you respond to formats:
    #
    #   respond_to do |format|
    #     format.html do |html|
    #       html.tablet # renders app/views/projects/show.html+tablet.erb
    #       html.phone { extra_setup; render ... }
    #     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
    #
X
Xavier Noria 已提交
211 212
    # Be sure to check the documentation of +respond_with+ and
    # <tt>ActionController::MimeResponds.respond_to</tt> for more examples.
213
    def respond_to(*mimes, &block)
214
      raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
215

216
      if collector = retrieve_collector_from_mimes(mimes, &block)
Ł
Łukasz Strzałkowski 已提交
217
        response = collector.response(request.variant)
218
        response ? response.call : render({})
219
      end
J
José Valim 已提交
220
    end
221

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

354
      if collector = retrieve_collector_from_mimes(&block)
355
        options = resources.size == 1 ? {} : resources.extract_options!
356
        options = options.clone
Ł
Łukasz Strzałkowski 已提交
357
        options[:default_response] = collector.response(request.variant)
358
        (options.delete(:responder) || self.class.responder).call(self, resources, options)
359
      end
360
    end
361 362 363

  protected

J
José Valim 已提交
364 365 366
    # Collect mimes declared in the class method respond_to valid for the
    # current action.
    def collect_mimes_from_class_level #:nodoc:
367
      action = action_name.to_s
368

J
Jeremy Kemper 已提交
369 370
      self.class.mimes_for_respond_to.keys.select do |mime|
        config = self.class.mimes_for_respond_to[mime]
J
José Valim 已提交
371 372

        if config[:except]
373
          !config[:except].include?(action)
J
José Valim 已提交
374
        elsif config[:only]
375
          config[:only].include?(action)
J
José Valim 已提交
376 377
        else
          true
378 379
        end
      end
J
José Valim 已提交
380
    end
381

382 383 384 385 386 387
    # 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.
388
    def retrieve_collector_from_mimes(mimes=nil, &block) #:nodoc:
389
      mimes ||= collect_mimes_from_class_level
390
      collector = Collector.new(mimes)
391
      block.call(collector) if block_given?
392
      format = collector.negotiate_format(request)
393

394
      if format
395
        _process_format(format)
396
        collector
397
      else
398
        raise ActionController::UnknownFormat
399 400 401
      end
    end

402 403
    # A container for responses available from the current controller for
    # requests for different mime-types sent to a particular action.
404 405 406 407 408 409 410
    #
    # 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 已提交
411
    #     format.xml { render xml: @people }
412 413 414 415 416 417 418 419 420 421 422 423 424
    #   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
425
      include AbstractController::Collector
426
      attr_accessor :format
427

428
      def initialize(mimes)
429
        @responses = {}
S
Santiago Pastorino 已提交
430
        mimes.each { |mime| send(mime) }
431
      end
432 433

      def any(*args, &block)
434 435 436
        if args.any?
          args.each { |type| send(type, &block) }
        else
437
          custom(Mime::ALL, &block)
438
        end
439
      end
J
José Valim 已提交
440
      alias :all :any
441 442

      def custom(mime_type, &block)
443
        mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type)
444
        @responses[mime_type] ||= block
445
      end
446

Ł
Łukasz Strzałkowski 已提交
447 448 449 450 451 452 453
      def response(variant)
        response = @responses.fetch(format, @responses[Mime::ALL])
        if response.nil? || response.arity == 0
          response
        else
          lambda { response.call VariantFilter.new(variant) }
        end
454 455 456
      end

      def negotiate_format(request)
457
        @format = request.negotiate_mime(@responses.keys)
458
      end
Ł
Łukasz Strzałkowski 已提交
459 460 461 462 463 464 465 466 467 468

      class VariantFilter
        def initialize(variant)
          @variant = variant
        end

        def method_missing(name)
          yield if name == @variant
        end
      end
469 470
    end
  end
471
end