Skip to content
16 changes: 9 additions & 7 deletions lib/rack/accept/header.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@ module Rack::Accept
# headers. The MediaType, Charset, Encoding, and Language classes all mixin
# this module.
module Header
class InvalidHeader < StandardError; end

# Parses the value of an Accept-style request header into a hash of
# acceptable values and their respective quality factors (qvalues). The
# +join+ method may be used on the resulting hash to obtain a header
# string that is the semantic equivalent of the one provided.
def parse(header)
qvalues = {}

header.to_s.split(/,\s*/).each do |part|
m = /^([^\s,]+?)(?:\s*;\s*q\s*=\s*(\d+(?:\.\d+)?))?$/.match(part)
header.to_s.split(',').each do |part|
m = /^\s*([^\s,]+?)(?:\s*;\s*q\s*=\s*(\d+(?:\.\d+)?))?$/.match(part)

if m
qvalues[m[1].downcase] = normalize_qvalue((m[2] || 1).to_f)
else
raise "Invalid header value: #{part.inspect}"
raise InvalidHeader, "Invalid header value: #{part.inspect}"
end
end

Expand All @@ -38,17 +40,17 @@ def join(qvalues)
# subtype, and 3) the media type parameters. An empty array is returned if
# no match can be made.
def parse_media_type(media_type)
m = media_type.to_s.match(/^([a-z*]+)\/([a-z0-9*\-.+]+)(?:;([a-z0-9=;]+))?$/)
m ? [m[1], m[2], m[3] || ''] : []
m = media_type.to_s.match(/^\s*([a-zA-Z*]+)\s*\/\s*([a-zA-Z0-9*\-.+]+)\s*(?:;(.+))?$/)
m ? [m[1].downcase, m[2].downcase, m[3] || ''] : []
end
module_function :parse_media_type

# Parses a string of media type range parameters into a hash of parameters
# to their respective values.
def parse_range_params(params)
params.split(';').inject({}) do |m, p|
params.split(';').inject({'q' => '1'}) do |m, p|
k, v = p.split('=', 2)
m[k] = v if v
m[k.strip] = v.strip if v
m
end
end
Expand Down
25 changes: 19 additions & 6 deletions lib/rack/accept/media_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ module Rack::Accept
class MediaType
include Header

attr_accessor :extensions

# The name of this header.
def name
'Accept'
Expand Down Expand Up @@ -37,16 +39,27 @@ def matches(media_type)
}.reverse
end

# Returns a params hash for the media type that matches
def params(media_type)
return {} unless media_type
key = matches(media_type).first
@extensions[key] || {}
end

private

def initialize(header)
# Strip accept-extension for now. We may want to do something with this
# later if people actually start to use it.
header = header.to_s.split(/,\s*/).map {|part|
part.sub(/(;\s*q\s*=\s*[\d.]+).*$/, '\1')
}.join(', ')
@extensions = {}
@qvalues = {}

super(header)
header.to_s.split(',').each do |part|
type, subtype, raw_params = parse_media_type(part)
raise InvalidHeader, "Invalid header value: #{part.inspect}" if !type || !subtype
media_type = "#{type}/#{subtype}"
params = parse_range_params(raw_params)
@extensions[media_type] = params
@qvalues[media_type] = normalize_qvalue(params['q']).to_f
end
end

# Returns true if all parameters and values in +match+ are also present in
Expand Down
14 changes: 10 additions & 4 deletions test/header_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ def test_parse_media_type
end

def test_parse_range_params
assert_equal({}, H.parse_range_params(''))
assert_equal({}, H.parse_range_params('a'))
assert_equal({'a' => 'a'}, H.parse_range_params('a=a'))
assert_equal({'a' => 'a', 'b' => 'b'}, H.parse_range_params('a=a;b=b'))
assert_equal({'q' => '1'}, H.parse_range_params(''))
assert_equal({'q' => '1'}, H.parse_range_params('a'))
assert_equal({'q' => '1', 'a' => 'a'}, H.parse_range_params('a=a'))
assert_equal({'q' => '1', 'a' => 'a', 'b' => 'b'}, H.parse_range_params('a=a;b=b'))
end

def test_normalize_qvalue
Expand All @@ -60,4 +60,10 @@ def test_normalize_qvalue
assert_equal(0, H.normalize_qvalue(0))
assert_equal(0.5, H.normalize_qvalue(0.5))
end

def test_invalid_header
assert_raise Rack::Accept::Header::InvalidHeader do
h = H.parse(' / ')
end
end
end
28 changes: 23 additions & 5 deletions test/media_type_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,22 @@ def test_qvalue
assert_equal(1, m.qvalue('text/html'))
end

def test_invalid_media_type
assert_raise Rack::Accept::Header::InvalidHeader do
m = M.new('')
m = M.new('text')
m = M.new('text;q=1')
end
end

def test_matches
m = M.new('text/*, text/html, text/html;level=1, */*')
assert_equal(%w{*/*}, m.matches(''))
assert_equal(%w{*/*}, m.matches('image/jpeg'))
assert_equal(%w{text/* */*}, m.matches('text/plain'))
assert_equal(%w{text/html text/* */*}, m.matches('text/html'))
assert_equal(%w{text/html;level=1 text/html text/* */*}, m.matches('text/html;level=1'))
assert_equal(%w{text/html;level=1 text/html text/* */*}, m.matches('text/html;level=1;answer=42'))
assert_equal(%w{text/html text/* */*}, m.matches('text/html;level=1'))
assert_equal(%w{text/html text/* */*}, m.matches('text/html;level=1;answer=42'))
end

def test_best_of
Expand All @@ -42,11 +50,21 @@ def test_best_of
assert_equal('text/xml', m.best_of(%w< text/xml text/html >))
end

def test_extension
def test_extensions
m = M.new('text/plain')
assert_equal({'text/plain' => {'q' => '1'}}, m.extensions)
m = M.new('text/*;q=0.5;a=42')
assert_equal(0.5, m.qvalue('text/plain'))
assert_equal({'text/*' => {'q' => '0.5', 'a' => '42'}}, m.extensions)
end


def test_params
m = M.new('text/plain;q=0.7;version=1.0')
assert_equal({'q' => '0.7', 'version' => '1.0'}, m.params('text/plain'))
m = M.new('text/*;q=0.5;a=42, application/json;b=12')
assert_equal({'q' => '0.5', 'a' => '42'}, m.params('text/plain'))
assert_equal({'q' => '1', 'b' => '12'}, m.params('application/json'))
end

def test_vendored_types
m = M.new("application/vnd.ms-excel")
assert_equal(nil, m.best_of(%w< application/vnd.ms-powerpoint >))
Expand Down