Module: Merb::ResponderMixin

Included in:
Controller
Defined in:
merb-core/lib/merb-core/controller/mixins/responder.rb

Overview

The ResponderMixin adds methods that help you manage what formats your controllers have available, determine what format(s) the client requested and is capable of handling, and perform content negotiation to pick the proper content format to deliver.

If you hear someone say "Use provides" they're talking about the Responder. If you hear someone ask "What happened to respond_to?" it was replaced by provides and the other Responder methods.

A simple example

The best way to understand how all of these pieces fit together is with an example. Here's a simple web-service ready resource that provides a list of all the widgets we know about. The widget list is available in 3 formats: :html (the default), plus :xml and :text.

class Widgets < Application
  provides :html   # This is the default, but you can
                   # be explicit if you like.
  provides :xml, :text

  def index
    @widgets = Widget.fetch
    render @widgets
  end
end

Let's look at some example requests for this list of widgets. We'll assume they're all GET requests, but that's only to make the examples easier; this works for the full set of RESTful methods.

  1. The simplest case, /widgets.html: Since the request includes a specific format (.html) we know what format to return. Since :html is in our list of provided formats, that's what we'll return. #render will look for an index.html.erb (or another template format like index.html.mab; see the documentation on Template engines)

  2. Almost as simple, /widgets.xml: This is very similar. They want :xml, we have :xml, so that's what they get. If #render doesn't find an index.xml.builder or similar template, it will call to_xml on @widgets. This may or may not do something useful, but you can see how it works.

  3. A browser request for /widgets: This time the URL doesn't say what format is being requested, so we'll look to the HTTP Accept: header. If it's '/' (anything), we'll use the first format on our list, :html by default.

If it parses to a list of accepted formats, we'll look through them, in order, until we find one we have available. If we find one, we'll use that. Otherwise, we can't fulfill the request: they asked for a format we don't have. So we raise 406: Not Acceptable.

A more complex example

Sometimes you don't have the same code to handle each available format. Sometimes you need to load different data to serve /widgets.xml versus /widgets.txt. In that case, you can use content_type to determine what format will be delivered.

class Widgets < Application
  def action1
    if content_type == :text
      Widget.load_text_formatted(params[:id])
    else
      render
    end
  end

  def action2
    case content_type
    when :html
      handle_html()
    when :xml
      handle_xml()
    when :text
      handle_text()
    else
      render
    end
  end
end

You can do any standard Ruby flow control using content_type. If you don't call it yourself, it will be called (triggering content negotiation) by #render.

Once content_type has been called, the output format is frozen, and none of the provides methods can be used.

Defined Under Namespace

Modules: ClassMethods Classes: ContentTypeAlreadySet

Constant Summary

TYPES =
Dictionary.new
MIMES =
{}
MIME_MUTEX =
Mutex.new
ACCEPT_RESULTS =
{}

Class Method Summary (collapse)

Instance Method Summary (collapse)

Class Method Details

+ (Object) included(base)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parameters:

  • base (Module)

    The module that ResponderMixin was mixed into



110
111
112
113
114
115
116
117
# File 'merb-core/lib/merb-core/controller/mixins/responder.rb', line 110

def self.included(base)
  base.extend(ClassMethods)
  base.class_eval do
    class_inheritable_accessor :class_provided_formats
    self.class_provided_formats = []
  end
  base.reset_provides
end

Instance Method Details

- (Object) _accept_types



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'merb-core/lib/merb-core/controller/mixins/responder.rb', line 245

def _accept_types
  accept = request.accept
  
  MIME_MUTEX.synchronize do
    return ACCEPT_RESULTS[accept] if ACCEPT_RESULTS[accept]
  end
  
  types = request.accept.split(Merb::Const::ACCEPT_SPLIT).map do |entry|
    entry =~ Merb::Const::MEDIA_RANGE
    media_range, quality = $1, $3
    
    kind, sub_type = media_range.split(Merb::Const::SLASH_SPLIT)
    mime_sym = Merb.available_accepts[media_range]
    mime = Merb.available_mime_types[mime_sym]
    (quality ||= 0.0) if media_range == "*/*"          
    quality = quality ? (quality.to_f * 100).to_i : 100
    quality *= (mime && mime[:default_quality] || 1)
    [quality, mime_sym, media_range, kind, sub_type, mime]
  end

  accepts = types.sort_by {|x| x.first }.reverse!.map! {|x| x[1]}      
  
  MIME_MUTEX.synchronize do
    ACCEPT_RESULTS[accept] = accepts.freeze
  end
  
  accepts
end

- (Object) _perform_content_negotiation

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Do the content negotiation:

  1. if params[:format] is there, and provided, use it
  2. Parse the Accept header
  3. If it's /, use the first provided format
  4. Look for one that is provided, in order of request
  5. Raise 406 if none found

Raises:



286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'merb-core/lib/merb-core/controller/mixins/responder.rb', line 286

def _perform_content_negotiation
  if fmt = params[:format] and !fmt.blank?
    accepts = [fmt.to_sym]
  else
    accepts = _accept_types
  end

  provided_formats = _provided_formats
  
  specifics = accepts & provided_formats
  return specifics.first unless specifics.length == 0
  return provided_formats.first if accepts.include?(:all) && !provided_formats.empty?
  
  message  = "A format (%s) that isn't provided (%s) has been requested. "
  message += "Make sure the action provides the format, and be "
  message += "careful of before filters which won't recognize "
  message += "formats provided within actions."
  raise Merb::ControllerExceptions::NotAcceptable,
    (message % [accepts.join(', '), provided_formats.join(', ')])
end

- (Array<Symbol>) _provided_formats

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns The current list of formats provided for this instance of the controller. It starts with what has been set in the controller (or :html by default) but can be modifed on a per-action basis.

Returns:

  • (Array<Symbol>)

    The current list of formats provided for this instance of the controller. It starts with what has been set in the controller (or :html by default) but can be modifed on a per-action basis.



191
192
193
# File 'merb-core/lib/merb-core/controller/mixins/responder.rb', line 191

def _provided_formats
  @_provided_formats ||= class_provided_formats.dup
end

- (Symbol) content_type(fmt = nil)

Returns the output format for this request, based on the provided formats, params[:format] and the client's HTTP Accept header.

The first time this is called, it triggers content negotiation and caches the value. Once you call content_type you can not set or change the list of provided formats.

Called automatically by +render+, so you should only call it if you need the value, not to trigger content negotiation.

Parameters:

  • fmt (String) (defaults to: nil)

    An optional format to use instead of performing content negotiation. This can be used to pass in the values of opts[:format] from the render function to short-circuit content-negotiation when it's not necessary. This optional parameter should not be considered part of the public API.

Returns:

  • (Symbol)

    The content-type that will be used for this controller.



329
330
331
332
# File 'merb-core/lib/merb-core/controller/mixins/responder.rb', line 329

def content_type(fmt = nil)
  self.content_type = (fmt || _perform_content_negotiation) unless @_content_type
  @_content_type
end

- (Symbol) content_type=(type)

Sets the content type of the current response to a value based on a passed in key. The Content-Type header will be set to the first registered header for the mime-type.

Parameters:

  • type (Symbol)

    The content type.

Returns:

  • (Symbol)

    The content-type that was passed in.

Raises:

  • (ArgumentError)

    type is not in the list of registered mime-types.



346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# File 'merb-core/lib/merb-core/controller/mixins/responder.rb', line 346

def content_type=(type)
  unless Merb.available_mime_types.has_key?(type)
    raise Merb::ControllerExceptions::NotAcceptable.new("Unknown content_type for response: #{type}") 
  end

  @_content_type = type

  mime = Merb.available_mime_types[type]
  
  headers["Content-Type"] = mime[:content_type]
  
  # merge any format specific response headers
  mime[:response_headers].each { |k,v| headers[k] ||= v }
  
  # if given, use a block to finetune any runtime headers
  mime[:response_block].call(self) if mime[:response_block]

  @_content_type
end

- (Array<Symbol>) does_not_provide(*formats)

Removes formats from the list of provided formats for this particular request. Usually used to remove formats from a single action. See also the controller-level does_not_provide that affects all actions in a controller.

Parameters:

  • *formats (Symbol)

    Registered mime-type

Returns:

  • (Array<Symbol>)

    List of formats that remain after removing the ones not to provide.



241
242
243
# File 'merb-core/lib/merb-core/controller/mixins/responder.rb', line 241

def does_not_provide(*formats)
  @_provided_formats -= formats.flatten
end

- (Array<Symbol>) only_provides(*formats)

Sets list of provided formats for this particular request. Usually used to limit formats to a single action. See also the controller-level only_provides that affects all actions in a controller.

Parameters:

  • *formats (Symbol)

    A list of formats to use as the per-action list of provided formats.

Returns:

  • (Array<Symbol>)

    List of formats passed in.



225
226
227
228
# File 'merb-core/lib/merb-core/controller/mixins/responder.rb', line 225

def only_provides(*formats)
  @_provided_formats = []
  provides(*formats)
end

- (Array<Symbol>) provides(*formats)

Adds formats to the list of provided formats for this particular request. Usually used to add formats to a single action. See also the controller-level provides that affects all actions in a controller.

Parameters:

  • *formats (Symbol)

    A list of formats to add to the per-action list of provided formats.

Returns:

  • (Array<Symbol>)

    List of formats passed in.

Raises:



208
209
210
211
212
213
# File 'merb-core/lib/merb-core/controller/mixins/responder.rb', line 208

def provides(*formats)
  if @_content_type
    raise ContentTypeAlreadySet, "Cannot modify provided_formats because content_type has already been set"
  end
  @_provided_formats = self._provided_formats | formats # merges with class_provided_formats if not already
end