mime_responds.rb 16.2 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
    #
X
Xavier Noria 已提交
184 185
    # Be sure to check the documentation of +respond_with+ and
    # <tt>ActionController::MimeResponds.respond_to</tt> for more examples.
186
    def respond_to(*mimes, &block)
187
      raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
188

189
      if collector = retrieve_collector_from_mimes(mimes, &block)
190
        response = collector.response
191
        response ? response.call : render({})
192
      end
J
José Valim 已提交
193
    end
194

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

327
      if collector = retrieve_collector_from_mimes(&block)
328
        options = resources.size == 1 ? {} : resources.extract_options!
329
        options = options.clone
330 331
        options[:default_response] = collector.response
        (options.delete(:responder) || self.class.responder).call(self, resources, options)
332
      end
333
    end
334 335 336

  protected

J
José Valim 已提交
337 338 339
    # Collect mimes declared in the class method respond_to valid for the
    # current action.
    def collect_mimes_from_class_level #:nodoc:
340
      action = action_name.to_s
341

J
Jeremy Kemper 已提交
342 343
      self.class.mimes_for_respond_to.keys.select do |mime|
        config = self.class.mimes_for_respond_to[mime]
J
José Valim 已提交
344 345

        if config[:except]
346
          !config[:except].include?(action)
J
José Valim 已提交
347
        elsif config[:only]
348
          config[:only].include?(action)
J
José Valim 已提交
349 350
        else
          true
351 352
        end
      end
J
José Valim 已提交
353
    end
354

355 356 357 358 359 360
    # 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.
361
    def retrieve_collector_from_mimes(mimes=nil, &block) #:nodoc:
362
      mimes ||= collect_mimes_from_class_level
363
      collector = Collector.new(mimes)
364
      block.call(collector) if block_given?
365
      format = collector.negotiate_format(request)
366

367
      if format
368
        _process_format(format)
369
        collector
370
      else
371
        raise ActionController::UnknownFormat
372 373 374
      end
    end

375 376
    # A container for responses available from the current controller for
    # requests for different mime-types sent to a particular action.
377 378 379 380 381 382 383
    #
    # 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 已提交
384
    #     format.xml { render xml: @people }
385 386 387 388 389 390 391 392 393 394 395 396 397
    #   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
398
      include AbstractController::Collector
399
      attr_accessor :order, :format
400

401 402
      def initialize(mimes)
        @order, @responses = [], {}
S
Santiago Pastorino 已提交
403
        mimes.each { |mime| send(mime) }
404
      end
405 406

      def any(*args, &block)
407 408 409
        if args.any?
          args.each { |type| send(type, &block) }
        else
410
          custom(Mime::ALL, &block)
411
        end
412
      end
J
José Valim 已提交
413
      alias :all :any
414 415

      def custom(mime_type, &block)
416
        mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type)
417 418
        @order << mime_type
        @responses[mime_type] ||= block
419
      end
420

421
      def response
422
        @responses.fetch(format, @responses[Mime::ALL])
423 424 425 426
      end

      def negotiate_format(request)
        @format = request.negotiate_mime(order)
427
      end
428 429
    end
  end
430
end