mime_responds.rb 16.4 KB
Newer Older
1
require 'abstract_controller/collector'
J
Jeremy Kemper 已提交
2
require 'active_support/core_ext/class/attribute'
3
require 'active_support/core_ext/object/inclusion'
4

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

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

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

43 44
        only_actions   = Array(options.delete(:only)).map(&:to_s)
        except_actions = Array(options.delete(:except)).map(&:to_s)
45

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

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

63 64 65 66
    # Without web-service support, an action which collects the data for displaying a list of people
    # might look something like this:
    #
    #   def index
67
    #     @people = Person.all
68 69 70 71 72
    #   end
    #
    # Here's the same action, with web-service support baked in:
    #
    #   def index
73
    #     @people = Person.all
74 75 76
    #
    #     respond_to do |format|
    #       format.html
77
    #       format.xml { render :xml => @people }
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
    #     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
    #       format.xml  { render :xml => @person.to_xml(:include => @company) }
    #     end
    #   end
    #
X
Xavier Noria 已提交
109 110
    # 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.
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 148 149 150 151
    # 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
152
    # config/initializers/mime_types.rb as follows.
153 154
    #
    #   Mime::Type.register "image/jpg", :jpg
J
José Valim 已提交
155 156 157 158
    #
    # Respond to also allows you to specify a common block for different formats by using any:
    #
    #   def index
159
    #     @people = Person.all
J
José Valim 已提交
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
    #
    #     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:
    #
    #   render :xml => @people
    #
    # Or if the format is json:
    #
    #   render :json => @people
    #
    # 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
182
    #       @people = Person.all
V
Vijay Dev 已提交
183
    #       respond_with(@people)
J
José Valim 已提交
184 185 186 187 188
    #     end
    #   end
    #
    # Be sure to check respond_with and respond_to documentation for more examples.
    #
189
    def respond_to(*mimes, &block)
190
      raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
191

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

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

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

  protected

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

J
Jeremy Kemper 已提交
346 347
      self.class.mimes_for_respond_to.keys.select do |mime|
        config = self.class.mimes_for_respond_to[mime]
J
José Valim 已提交
348 349

        if config[:except]
350
          !action.in?(config[:except])
J
José Valim 已提交
351
        elsif config[:only]
352
          action.in?(config[:only])
J
José Valim 已提交
353 354
        else
          true
355 356
        end
      end
J
José Valim 已提交
357
    end
358

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

372
      if format
373
        self.content_type ||= format.to_s
374
        lookup_context.formats = [format.to_sym]
375
        lookup_context.rendered_format = lookup_context.formats.first
376
        collector
377
      else
378
        raise ActionController::UnknownFormat.new
379 380 381
      end
    end

382 383
    # A container for responses available from the current controller for
    # requests for different mime-types sent to a particular action.
384 385 386 387 388 389 390
    #
    # 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
391
    #     format.xml { render :xml => @people }
392 393 394 395 396 397 398 399 400 401 402 403 404 405
    #   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
406
      include AbstractController::Collector
407
      attr_accessor :order, :format
408

409 410
      def initialize(mimes)
        @order, @responses = [], {}
S
Santiago Pastorino 已提交
411
        mimes.each { |mime| send(mime) }
412
      end
413 414

      def any(*args, &block)
415 416 417
        if args.any?
          args.each { |type| send(type, &block) }
        else
418
          custom(Mime::ALL, &block)
419
        end
420
      end
J
José Valim 已提交
421
      alias :all :any
422 423

      def custom(mime_type, &block)
424
        mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type)
425 426
        @order << mime_type
        @responses[mime_type] ||= block
427
      end
428

429 430 431 432 433 434
      def response
        @responses[format] || @responses[Mime::ALL]
      end

      def negotiate_format(request)
        @format = request.negotiate_mime(order)
435
      end
436 437
    end
  end
438
end