module Merb # Thanks to Chris Wanstrath # use this in your controllers to switch output based on # the HTTP_ACCEPT header. like so: # respond_to do |type| # type.js { render_js } # type.html { render } # type.xml { @foo.to_xml } # type.yaml { @foo.to_yaml } # end # TODO : revisit this whole patern. Can we improve on this? module ResponderMixin def respond_to(&block) responder = Rest::Responder.new(@env['HTTP_ACCEPT'], params) block.call(responder) responder.respond(headers) @status = responder.status responder.body end module Rest TYPES = { :all => %w[*/*], :yaml => %w[application/x-yaml text/yaml], :text => %w[text/plain], :html => %w[text/html application/xhtml+xml application/html], :xml => %w[application/xml text/xml application/x-xml], :js => %w[application/json text/x-json text/javascript application/javascript application/x-javascript] } class Responder attr_reader :body, :type, :status def initialize(accept_header, params={}) MERB_LOGGER.info accept_header @accepts = Responder.parse(accept_header) @params = params @stack = {} end def method_missing(symbol, &block) raise "respond_to expects a block" unless block_given? # the first method we encounter here will be used for the catch all mime-type */* @stack[:all] = block unless @stack[:all] @stack[symbol] = block end def respond(headers) unless @stack.keys.all?{|k| TYPES.has_key?(k) } raise "unrecognized mime type in respond_to block" end mime_type = negotiate_content if mime_type headers['Content-Type'] = mime_type.super_range @status = 200 @body = @stack[mime_type.to_sym].call else headers['Content-Type'] = nil @status = 406 @body = nil end end protected def self.parse(accept_header) index = 0 list = accept_header.split(/,/).map! do |entry| AcceptType.new(entry,index += 1) end.sort!.uniq end private def negotiate_content if @params['format'] negotiate_by_format elsif (@stack.keys & @accepts.map(&:to_sym)).size > 0 negotiate_by_accept_header end end def negotiate_by_format format = @params['format'].to_sym if @stack[format] if @accepts.map(&:to_sym).include?(format) @accepts.select{|a| a.to_sym == format }.first else AcceptType.new(TYPES[format].first,0) end end end def negotiate_by_accept_header @accepts.each do |accept| return accept if @stack[accept.to_sym] || accept.to_sym == :all end end end class AcceptType attr_reader :media_range, :quality, :index, :type, :sub_type def initialize(entry,index) @index = index @media_range, quality = entry.split(/;\s*q=/).map(&:strip) @type, @sub_type = @media_range.split(/\//) quality ||= 0.0 if @media_range == '*/*' @quality = ((quality || 1.0).to_f * 100).to_i end def <=>(entry) returning (entry.quality <=> quality).to_s do |c| c.replace((index <=> entry.index).to_s) if c == '0' end.to_i end def eql?(entry) synonyms.include?(entry.media_range) end def ==(entry); eql?(entry); end def hash; super_range.hash; end def synonyms TYPES.values.select{|e| e.include?(@media_range)}.flatten end def super_range synonyms.first || @media_range end def to_sym TYPES.select{|k,v| v == synonyms }.flatten.first end def to_s @media_range end end end end end