diff --git a/lib/rack/accept/header.rb b/lib/rack/accept/header.rb index 98c24d3..a746057 100644 --- a/lib/rack/accept/header.rb +++ b/lib/rack/accept/header.rb @@ -3,6 +3,8 @@ 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 @@ -10,13 +12,13 @@ module Header 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 @@ -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 diff --git a/lib/rack/accept/media_type.rb b/lib/rack/accept/media_type.rb index 328578c..c76b7a5 100644 --- a/lib/rack/accept/media_type.rb +++ b/lib/rack/accept/media_type.rb @@ -7,6 +7,8 @@ module Rack::Accept class MediaType include Header + attr_accessor :extensions + # The name of this header. def name 'Accept' @@ -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 diff --git a/test/header_test.rb b/test/header_test.rb index 2ff4d4d..8fb4ef8 100644 --- a/test/header_test.rb +++ b/test/header_test.rb @@ -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 @@ -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 diff --git a/test/media_type_test.rb b/test/media_type_test.rb index cfae292..b1fa6d9 100644 --- a/test/media_type_test.rb +++ b/test/media_type_test.rb @@ -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 @@ -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 >))