mime_responds.rb 9.9 KB
Newer Older
1 2
module ActionController #:nodoc:
  module MimeResponds #:nodoc:
3 4 5 6
    extend ActiveSupport::Concern

    included do
      class_inheritable_reader :mimes_for_respond_to
7
      clear_respond_to
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
    end

    module ClassMethods
      # Defines mimes that are rendered by default when invoking respond_with.
      #
      # Examples:
      #
      #   respond_to :html, :xml, :json
      #
      # All actions on your controller will respond to :html, :xml and :json.
      #
      # But if you want to specify it based on your actions, you can use only and
      # except:
      #
      #   respond_to :html
      #   respond_to :xml, :json, :except => [ :edit ]
      #
      # The definition above explicits that all actions respond to :html. And all
      # actions except :edit respond to :xml and :json.
      #
      # You can specify also only parameters:
      #
      #   respond_to :rjs, :only => :create
      #
      def respond_to(*mimes)
        options = mimes.extract_options!

        only_actions   = Array(options.delete(:only))
        except_actions = Array(options.delete(:except))

        mimes.each do |mime|
          mime = mime.to_sym
40 41 42
          mimes_for_respond_to[mime]          = {}
          mimes_for_respond_to[mime][:only]   = only_actions   unless only_actions.empty?
          mimes_for_respond_to[mime][:except] = except_actions unless except_actions.empty?
43 44 45 46 47
        end
      end

      # Clear all mimes in respond_to.
      #
48 49
      def clear_respond_to
        write_inheritable_attribute(:mimes_for_respond_to, ActiveSupport::OrderedHash.new)
50 51
      end
    end
52

53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 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 101 102 103 104 105 106 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
    # Without web-service support, an action which collects the data for displaying a list of people
    # might look something like this:
    #
    #   def index
    #     @people = Person.find(:all)
    #   end
    #
    # Here's the same action, with web-service support baked in:
    #
    #   def index
    #     @people = Person.find(:all)
    #
    #     respond_to do |format|
    #       format.html
    #       format.xml { render :xml => @people.to_xml }
    #     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
    #
    # If the client wants HTML, we just redirect them back to the person list. If they want Javascript
    # (format.js), then it is an RJS request and we render the RJS template associated with this action.
    # 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
    # environment.rb as follows.
    #
    #   Mime::Type.register "image/jpg", :jpg
J
José Valim 已提交
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
    #
    # Respond to also allows you to specify a common block for different formats by using any:
    #
    #   def index
    #     @people = Person.find(:all)
    #
    #     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
    #       @people = Person.find(:all)
    #       respond_with(@person)
    #     end
    #   end
    #
    # Be sure to check respond_with and respond_to documentation for more examples.
    #
179
    def respond_to(*mimes, &block)
180
      raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
J
José Valim 已提交
181

182
      collector = Collector.new
183
      mimes = collect_mimes_from_class_level if mimes.empty?
184 185
      mimes.each { |mime| collector.send(mime) }
      block.call(collector) if block_given?
186

187
      if format = request.negotiate_mime(collector.order)
188 189
        self.formats = [format.to_sym]

190
        if response = collector.response_for(format)
191 192 193 194
          response.call
        else
          default_render
        end
J
José Valim 已提交
195 196 197 198
      else
        head :not_acceptable
      end
    end
199

200
    # respond_with wraps a resource around a responder for default representation.
201 202
    # First it invokes respond_to, if a response cannot be found (ie. no block
    # for the request was given and template was not available), it instantiates
203
    # an ActionController::Responder with the controller and resource.
204
    #
205
    # ==== Example
206
    #
207 208 209
    #   def index
    #     @users = User.all
    #     respond_with(@users)
J
José Valim 已提交
210 211
    #   end
    #
212 213
    # It also accepts a block to be given. It's used to overwrite a default
    # response:
214 215
    #
    #   def destroy
216 217
    #     @user = User.find(params[:id])
    #     flash[:notice] = "User was successfully created." if @user.save
218
    #
219 220 221
    #     respond_with(@user) do |format|
    #       format.html { render }
    #     end
222 223
    #   end
    #
224 225
    # All options given to respond_with are sent to the underlying responder,
    # except for the option :responder itself. Since the responder interface
226 227
    # is quite simple (it just needs to respond to call), you can even give
    # a proc to it.
228
    #
229
    def respond_with(*resources, &block)
230
      respond_to(&block)
231
    rescue ActionView::MissingTemplate
232 233
      options = resources.extract_options!
      (options.delete(:responder) || responder).call(self, resources, options)
234
    end
235

236 237
    def responder
      ActionController::Responder
238 239
    end

240 241
  protected

J
José Valim 已提交
242 243 244 245 246
    # Collect mimes declared in the class method respond_to valid for the
    # current action.
    #
    def collect_mimes_from_class_level #:nodoc:
      action = action_name.to_sym
247

J
José Valim 已提交
248 249 250 251 252 253 254 255 256
      mimes_for_respond_to.keys.select do |mime|
        config = mimes_for_respond_to[mime]

        if config[:except]
          !config[:except].include?(action)
        elsif config[:only]
          config[:only].include?(action)
        else
          true
257 258
        end
      end
J
José Valim 已提交
259
    end
260

261
    class Collector #:nodoc:
262
      attr_accessor :order
263

264
      def initialize
265
        @order, @responses = [], {}
266
      end
267 268

      def any(*args, &block)
269 270 271
        if args.any?
          args.each { |type| send(type, &block) }
        else
272
          custom(Mime::ALL, &block)
273
        end
274
      end
J
José Valim 已提交
275
      alias :all :any
276 277 278 279 280 281

      def custom(mime_type, &block)
        mime_type = mime_type.is_a?(Mime::Type) ? mime_type : Mime::Type.lookup(mime_type.to_s)

        @order << mime_type
        @responses[mime_type] ||= block
282
      end
283

284
      def response_for(mime)
285
        @responses[mime] || @responses[Mime::ALL]
286 287
      end

288 289 290 291 292 293 294 295
      def self.generate_method_for_mime(mime)
        sym = mime.is_a?(Symbol) ? mime : mime.to_sym
        const = sym.to_s.upcase
        class_eval <<-RUBY, __FILE__, __LINE__ + 1
          def #{sym}(&block)                # def html(&block)
            custom(Mime::#{const}, &block)  #   custom(Mime::HTML, &block)
          end                               # end
        RUBY
296 297
      end

298 299 300
      Mime::SET.each do |mime|
        generate_method_for_mime(mime)
      end
301

302 303
      def method_missing(symbol, &block)
        mime_constant = Mime.const_get(symbol.to_s.upcase)
304

305
        if Mime::SET.include?(mime_constant)
306
          self.class.generate_method_for_mime(mime_constant)
307
          send(symbol, &block)
308 309 310
        else
          super
        end
311
      end
312

313 314
    end
  end
315
end