mime_responds.rb 16.2 KB
Newer Older
1 2
require 'abstract_controller/collector'

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

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

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

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

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

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

59 60 61 62
    # Without web-service support, an action which collects the data for displaying a list of people
    # might look something like this:
    #
    #   def index
63
    #     @people = Person.all
64 65 66 67 68
    #   end
    #
    # Here's the same action, with web-service support baked in:
    #
    #   def index
69
    #     @people = Person.all
70 71 72
    #
    #     respond_to do |format|
    #       format.html
A
AvnerCohen 已提交
73
    #       format.xml { render xml: @people }
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
    #     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
    #     @company = Company.find_or_create_by_name(params[:company][:name])
    #     @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)
    #     @company = Company.find_or_create_by_name(company[:name])
    #     @person  = @company.people.create(params[:person])
    #
    #     respond_to do |format|
    #       format.html { redirect_to(person_list_url) }
    #       format.js
A
AvnerCohen 已提交
101
    #       format.xml  { render xml: @person.to_xml(include: @company) }
102 103 104
    #     end
    #   end
    #
X
Xavier Noria 已提交
105 106
    # 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.
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
    # 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)
    #   @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):
    #
    #   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
148
    # config/initializers/mime_types.rb as follows.
149 150
    #
    #   Mime::Type.register "image/jpg", :jpg
J
José Valim 已提交
151 152 153 154
    #
    # Respond to also allows you to specify a common block for different formats by using any:
    #
    #   def index
155
    #     @people = Person.all
J
José Valim 已提交
156 157 158 159 160 161 162 163 164
    #
    #     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 已提交
165
    #   render xml: @people
J
José Valim 已提交
166 167 168
    #
    # Or if the format is json:
    #
A
AvnerCohen 已提交
169
    #   render json: @people
J
José Valim 已提交
170 171 172 173 174 175 176 177
    #
    # 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
178
    #       @people = Person.all
V
Vijay Dev 已提交
179
    #       respond_with(@people)
J
José Valim 已提交
180 181 182
    #     end
    #   end
    #
X
Xavier Noria 已提交
183 184
    # Be sure to check the documentation of +respond_with+ and
    # <tt>ActionController::MimeResponds.respond_to</tt> for more examples.
185
    def respond_to(*mimes, &block)
186
      raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
187

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

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

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

  protected

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

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

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

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

365
      if format
366
        self.content_type ||= format.to_s
367
        lookup_context.formats = [format.to_sym]
368
        lookup_context.rendered_format = lookup_context.formats.first
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 422 423 424 425 426
      def response
        @responses[format] || @responses[Mime::ALL]
      end

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