From ea41a42f605002394b14f91881b28b216314850f Mon Sep 17 00:00:00 2001 From: Jack Chu Date: Fri, 22 Jun 2012 20:09:41 -0400 Subject: [PATCH 1/9] extract accept-extensions from the header. #12 --- lib/rack/accept/media_type.rb | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/rack/accept/media_type.rb b/lib/rack/accept/media_type.rb index 328578c..2222dc0 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' @@ -40,13 +42,24 @@ def matches(media_type) 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 = {} + header.to_s.split(',').each do |raw_media_type| + params = { 'q' => 1 } + parts = raw_media_type.split(';') + media_type = parts.shift.strip.downcase + parts.each do |part| + pair = part.split('=', 2) + pair[0].strip.downcase + pair[1].strip + params[pair[0]] = pair[0] == 'q' ? normalize_qvalue(pair[1]).to_f : pair[1] + end + @extensions[media_type] = params + end - super(header) + @qvalues = {} + @extensions.each do |k, v| + @qvalues[k] = v['q'] + end end # Returns true if all parameters and values in +match+ are also present in From c6b2633de3766835830a3bc343bee1dfbc5d894e Mon Sep 17 00:00:00 2001 From: Jack Chu Date: Mon, 25 Jun 2012 16:52:46 -0400 Subject: [PATCH 2/9] populate @qvalues and @extensions in the same loop --- lib/rack/accept/media_type.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/rack/accept/media_type.rb b/lib/rack/accept/media_type.rb index 2222dc0..78d5904 100644 --- a/lib/rack/accept/media_type.rb +++ b/lib/rack/accept/media_type.rb @@ -43,6 +43,8 @@ def matches(media_type) def initialize(header) @extensions = {} + @qvalues = {} + header.to_s.split(',').each do |raw_media_type| params = { 'q' => 1 } parts = raw_media_type.split(';') @@ -54,11 +56,7 @@ def initialize(header) params[pair[0]] = pair[0] == 'q' ? normalize_qvalue(pair[1]).to_f : pair[1] end @extensions[media_type] = params - end - - @qvalues = {} - @extensions.each do |k, v| - @qvalues[k] = v['q'] + @qvalues[media_type] = params['q'] end end From ef9b11e51758055ef8ee25313116221a3eaa6f29 Mon Sep 17 00:00:00 2001 From: Jack Chu Date: Tue, 26 Jun 2012 10:36:00 -0400 Subject: [PATCH 3/9] split first on comma, then ignore any leading white space --- lib/rack/accept/header.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rack/accept/header.rb b/lib/rack/accept/header.rb index 98c24d3..58b049e 100644 --- a/lib/rack/accept/header.rb +++ b/lib/rack/accept/header.rb @@ -10,8 +10,8 @@ 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) From 06e1f1a82eb6ad81491701ea8f9ee6cbcdea530b Mon Sep 17 00:00:00 2001 From: Jack Chu Date: Tue, 26 Jun 2012 10:37:10 -0400 Subject: [PATCH 4/9] downcast returned type and subtype, strip whitespace on params --- lib/rack/accept/header.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/rack/accept/header.rb b/lib/rack/accept/header.rb index 58b049e..0c297af 100644 --- a/lib/rack/accept/header.rb +++ b/lib/rack/accept/header.rb @@ -38,8 +38,8 @@ 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 @@ -48,7 +48,7 @@ def parse_media_type(media_type) def parse_range_params(params) params.split(';').inject({}) do |m, p| k, v = p.split('=', 2) - m[k] = v if v + m[k.strip] = v.strip if v m end end From 53e8d6fe534ad7fbcaefdf65e3fa1245d6b2d18f Mon Sep 17 00:00:00 2001 From: Jack Chu Date: Tue, 26 Jun 2012 10:37:46 -0400 Subject: [PATCH 5/9] reuse #parse_media_type and #parse_range_params for parsing out the header in a media type --- lib/rack/accept/media_type.rb | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/lib/rack/accept/media_type.rb b/lib/rack/accept/media_type.rb index 78d5904..785435f 100644 --- a/lib/rack/accept/media_type.rb +++ b/lib/rack/accept/media_type.rb @@ -46,17 +46,11 @@ def initialize(header) @qvalues = {} header.to_s.split(',').each do |raw_media_type| - params = { 'q' => 1 } - parts = raw_media_type.split(';') - media_type = parts.shift.strip.downcase - parts.each do |part| - pair = part.split('=', 2) - pair[0].strip.downcase - pair[1].strip - params[pair[0]] = pair[0] == 'q' ? normalize_qvalue(pair[1]).to_f : pair[1] - end + type, subtype, raw_params = parse_media_type(raw_media_type) + media_type = "#{type}/#{subtype}" + params = ({'q' => '1'}).merge(parse_range_params(raw_params)) @extensions[media_type] = params - @qvalues[media_type] = params['q'] + @qvalues[media_type] = normalize_qvalue(params['q']).to_f end end From aec93ad1a204a3f338ff082bc16be870baea923d Mon Sep 17 00:00:00 2001 From: Jack Chu Date: Tue, 26 Jun 2012 11:20:08 -0400 Subject: [PATCH 6/9] raise InvalidHeader exception when a part of the header can't be parsed --- lib/rack/accept/header.rb | 4 +++- test/header_test.rb | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/rack/accept/header.rb b/lib/rack/accept/header.rb index 0c297af..39e4b5d 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 @@ -16,7 +18,7 @@ def parse(header) 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 diff --git a/test/header_test.rb b/test/header_test.rb index 2ff4d4d..e9b3002 100644 --- a/test/header_test.rb +++ b/test/header_test.rb @@ -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 From fea1ca2230349567d37dee175c7f363225df28e3 Mon Sep 17 00:00:00 2001 From: Jack Chu Date: Tue, 26 Jun 2012 11:21:55 -0400 Subject: [PATCH 7/9] when parsing range params, set the default q value --- lib/rack/accept/header.rb | 2 +- lib/rack/accept/media_type.rb | 7 ++++--- test/header_test.rb | 8 ++++---- test/media_type_test.rb | 10 +++++++++- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/lib/rack/accept/header.rb b/lib/rack/accept/header.rb index 39e4b5d..a746057 100644 --- a/lib/rack/accept/header.rb +++ b/lib/rack/accept/header.rb @@ -48,7 +48,7 @@ def parse_media_type(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.strip] = v.strip if v m diff --git a/lib/rack/accept/media_type.rb b/lib/rack/accept/media_type.rb index 785435f..deb8d30 100644 --- a/lib/rack/accept/media_type.rb +++ b/lib/rack/accept/media_type.rb @@ -45,10 +45,11 @@ def initialize(header) @extensions = {} @qvalues = {} - header.to_s.split(',').each do |raw_media_type| - type, subtype, raw_params = parse_media_type(raw_media_type) + 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 = ({'q' => '1'}).merge(parse_range_params(raw_params)) + params = parse_range_params(raw_params) @extensions[media_type] = params @qvalues[media_type] = normalize_qvalue(params['q']).to_f end diff --git a/test/header_test.rb b/test/header_test.rb index e9b3002..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 diff --git a/test/media_type_test.rb b/test/media_type_test.rb index cfae292..1ae9459 100644 --- a/test/media_type_test.rb +++ b/test/media_type_test.rb @@ -16,6 +16,14 @@ 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('')) @@ -46,7 +54,7 @@ def test_extension m = M.new('text/*;q=0.5;a=42') assert_equal(0.5, m.qvalue('text/plain')) end - + def test_vendored_types m = M.new("application/vnd.ms-excel") assert_equal(nil, m.best_of(%w< application/vnd.ms-powerpoint >)) From 63a9a4bd465f7dd9c574e416b51f0a03a571460c Mon Sep 17 00:00:00 2001 From: Jack Chu Date: Tue, 26 Jun 2012 11:22:46 -0400 Subject: [PATCH 8/9] media type matches ignore params --- test/media_type_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/media_type_test.rb b/test/media_type_test.rb index 1ae9459..f21d458 100644 --- a/test/media_type_test.rb +++ b/test/media_type_test.rb @@ -30,8 +30,8 @@ def test_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 From 7497c8435fdd9cb68a4b980a5f90cde6cd3183d8 Mon Sep 17 00:00:00 2001 From: Jack Chu Date: Tue, 26 Jun 2012 12:34:48 -0400 Subject: [PATCH 9/9] add a #params method to MediaType, that accepts a media_type and will return a params hash based on the best matched media type --- lib/rack/accept/media_type.rb | 7 +++++++ test/media_type_test.rb | 14 ++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/rack/accept/media_type.rb b/lib/rack/accept/media_type.rb index deb8d30..c76b7a5 100644 --- a/lib/rack/accept/media_type.rb +++ b/lib/rack/accept/media_type.rb @@ -39,6 +39,13 @@ 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) diff --git a/test/media_type_test.rb b/test/media_type_test.rb index f21d458..b1fa6d9 100644 --- a/test/media_type_test.rb +++ b/test/media_type_test.rb @@ -50,9 +50,19 @@ 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