rack-accept 0.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'rack-accept'
3
- s.version = '0.3'
4
- s.date = '2010-04-02'
3
+ s.version = '0.4.1'
4
+ s.date = '2010-04-05'
5
5
 
6
6
  s.summary = 'HTTP Accept* for Ruby/Rack'
7
7
  s.description = 'HTTP Accept, Accept-Charset, Accept-Encoding, and Accept-Language for Ruby/Rack'
data/CHANGES CHANGED
@@ -1,3 +1,9 @@
1
+ ## 0.4 / April 5, 2010
2
+
3
+ * Added support for media type queries with multiple range parameters
4
+ * Changed Rack::Accept::Header#sort method to return only single values
5
+ and moved previous functionality to Rack::Accept::Header#sort_with_qvalues
6
+
1
7
  ## 0.3 / April 3, 2010
2
8
 
3
9
  * Enhanced Rack middleware component to be able to automatically send a 406
data/README CHANGED
@@ -56,27 +56,25 @@ middleware pipeline and access the Rack::Accept::Request object in the
56
56
 
57
57
  Rack::Accept can also construct automatic 406 responses if you set up the types
58
58
  of media, character sets, encoding, or languages your server is able to serve
59
- ahead of time.
59
+ ahead of time. If you pass a configuration block to your `use` statement it will
60
+ yield the Rack::Accept::Context object that is used for that invocation.
60
61
 
61
62
  require 'rack/accept'
62
63
 
63
- use(Rack::Accept) do |accept|
64
+ use(Rack::Accept) do |context|
64
65
  # We only ever serve content in English or Japanese from this site, so if
65
66
  # the user doesn't accept either of these we will respond with a 406.
66
- #
67
- # Note: +accept+ is an instance of Rack::Accept::Context.
68
- accept.languages = %w< en jp >
67
+ context.languages = %w< en jp >
69
68
  end
70
69
 
71
70
  app = ...
72
71
 
73
72
  run app
74
73
 
75
- Note: You should probably think about this very carefully before you use
76
- Rack::Accept in this way. Many user agents are careless about the types of
77
- Accept headers they send, and depend on apps not being too picky. Instead of
78
- automatically sending a 406, you should probably only send one when absolutely
79
- necessary.
74
+ Note: You should think carefully before using Rack::Accept in this way.
75
+ Many user agents are careless about the types of Accept headers they send, and
76
+ depend on apps not being too picky. Instead of automatically sending a 406, you
77
+ should probably only send one when absolutely necessary.
80
78
 
81
79
  Additionally, Rack::Accept may be used outside of a Rack context to provide
82
80
  any Ruby app the ability to construct and interpret Accept headers.
@@ -3,7 +3,7 @@ Usage
3
3
 
4
4
  Rack::Accept implements the Rack middleware interface and may be used with any
5
5
  Rack-based application. Simply insert the Rack::Accept module in your Rack
6
- middleware pipeline and access the [Rack::Accept::Request][req] object in the
6
+ middleware pipeline and access the [Request][req] object in the
7
7
  "rack-accept.request" environment key, as in the following example:
8
8
 
9
9
  require 'rack/accept'
@@ -27,29 +27,28 @@ middleware pipeline and access the [Rack::Accept::Request][req] object in the
27
27
 
28
28
  run app
29
29
 
30
- Rack::Accept can also construct a [406][406] response automatically if you set
31
- up the types of media, character sets, encoding, or languages your server is
32
- able to serve ahead of time.
30
+ Rack::Accept can also construct automatic [406][406] responses if you set up
31
+ the types of media, character sets, encoding, or languages your server is able
32
+ to serve ahead of time. If you pass a configuration block to your `use`
33
+ statement it will yield the [Context][ctx] object that is used for that
34
+ invocation.
33
35
 
34
36
  require 'rack/accept'
35
37
 
36
- use(Rack::Accept) do |accept|
38
+ use(Rack::Accept) do |context|
37
39
  # We only ever serve content in English or Japanese from this site, so if
38
40
  # the user doesn't accept either of these we will respond with a 406.
39
- #
40
- # Note: +accept+ is an instance of Rack::Accept::Context.
41
- accept.languages = %w< en jp >
41
+ context.languages = %w< en jp >
42
42
  end
43
43
 
44
44
  app = ...
45
45
 
46
46
  run app
47
47
 
48
- __Note:__ You should probably think about this very carefully before you use
49
- Rack::Accept in this way. Many user agents are careless about the types of
50
- Accept headers they send, and depend on apps not being too picky. Instead of
51
- automatically sending a 406, you should probably only send one when absolutely
52
- necessary.
48
+ __Note:__ You should think carefully before using Rack::Accept in this way.
49
+ Many user agents are careless about the types of Accept headers they send, and
50
+ depend on apps not being too picky. Instead of automatically sending a 406, you
51
+ should probably only send one when absolutely necessary.
53
52
 
54
53
  Additionally, Rack::Accept may be used outside of a Rack context to provide
55
54
  any Ruby app the ability to construct and interpret Accept headers.
@@ -71,4 +70,5 @@ don't have to worry about these kinds of details.
71
70
 
72
71
  [req]: api/classes/Rack/Accept/Request.html
73
72
  [406]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.7
73
+ [ctx]: api/classes/Rack/Accept/Context.html
74
74
  [sec14-3]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3
@@ -3,7 +3,7 @@ require 'rack'
3
3
  module Rack::Accept
4
4
 
5
5
  # The current version of rack-accept.
6
- VERSION = [0, 3]
6
+ VERSION = [0, 4, 1]
7
7
 
8
8
  # Returns the current version of rack-accept as a string.
9
9
  def self.version
@@ -21,7 +21,7 @@ module Rack::Accept
21
21
  if m.empty?
22
22
  charset == 'iso-8859-1' ? 1 : 0
23
23
  else
24
- @qvalues[m.first]
24
+ normalize_qvalue(@qvalues[m.first])
25
25
  end
26
26
  end
27
27
 
@@ -21,7 +21,7 @@ module Rack::Accept
21
21
  if m.empty?
22
22
  encoding == 'identity' ? 1 : 0
23
23
  else
24
- @qvalues[m.first]
24
+ normalize_qvalue(@qvalues[m.first])
25
25
  end
26
26
  end
27
27
 
@@ -16,7 +16,7 @@ module Rack::Accept
16
16
  m = /^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$/.match(part) # From WEBrick
17
17
 
18
18
  if m
19
- qvalues[m[1]] = (m[2] || 1).to_f
19
+ qvalues[m[1]] = normalize_qvalue((m[2] || 1).to_f)
20
20
  else
21
21
  raise "Invalid header value: #{part.inspect}"
22
22
  end
@@ -40,11 +40,29 @@ module Rack::Accept
40
40
  # subtype, and 3) the media type parameters. An empty array is returned if
41
41
  # no match can be made.
42
42
  def parse_media_type(media_type)
43
- m = media_type.to_s.match(/^([a-z*]+)\/([a-z*-]+)(?:;([a-z0-9=]+))?$/)
43
+ m = media_type.to_s.match(/^([a-z*]+)\/([a-z*-]+)(?:;([a-z0-9=;]+))?$/)
44
44
  m ? [m[1], m[2], m[3] || ''] : []
45
45
  end
46
46
  module_function :parse_media_type
47
47
 
48
+ # Parses a string of media type range parameters into a hash of parameters
49
+ # to their respective values.
50
+ def parse_range_params(params)
51
+ params.split(';').inject({}) do |m, p|
52
+ k, v = p.split('=', 2)
53
+ m[k] = v if v
54
+ m
55
+ end
56
+ end
57
+ module_function :parse_range_params
58
+
59
+ # Converts 1.0 and 0.0 qvalues to 1 and 0 respectively. Used to maintain
60
+ # consistency across qvalue methods.
61
+ def normalize_qvalue(q)
62
+ (q == 1 || q == 0) && q.is_a?(Float) ? q.to_i : q
63
+ end
64
+ module_function :normalize_qvalue
65
+
48
66
  module PublicInstanceMethods
49
67
  # A table of all values of this header to their respective quality
50
68
  # factors (qvalues).
@@ -87,29 +105,37 @@ module Rack::Accept
87
105
  # containing two objects: 1) the value's qvalue and 2) the original
88
106
  # value.
89
107
  #
90
- # It is important to note that when performing this sort the order of
91
- # the original values is preserved so long as the qvalue for each input
92
- # value is the same. This expectation can be useful for example when
108
+ # It is important to note that this sort is a "stable sort". In other
109
+ # words, the order of the original values is preserved so long as the
110
+ # qvalue for each is the same. This expectation can be useful when
93
111
  # trying to determine which of a variety of options has the highest
94
112
  # qvalue. If the user prefers using one option over another (for any
95
113
  # number of reasons), he should put it first in +values+. He may then
96
114
  # use the first result with confidence that it is both most acceptable
97
- # to the user and most convenient for him as well.
98
- def sort(values)
99
- values.map {|v| [ qvalue(v), v ] }.sort.reverse
115
+ # to the client and most convenient for him as well.
116
+ def sort_with_qvalues(values, keep_unacceptables=true)
117
+ qvalues = {}
118
+ values.each do |v|
119
+ q = qvalue(v)
120
+ if q != 0 || keep_unacceptables
121
+ qvalues[q] ||= []
122
+ qvalues[q] << v
123
+ end
124
+ end
125
+ order = qvalues.keys.sort.reverse
126
+ order.inject([]) {|m, q| m.concat(qvalues[q].map {|v| [q, v] }) }
100
127
  end
101
128
 
102
- # Determines the most preferred value to use of those provided in
103
- # +values+. See the documentation for #sort for more information on
104
- # exactly how the sorting is done.
105
- #
106
- # If +keep_unacceptables+ is false (the default) and no values are
107
- # acceptable the return value will be +nil+. Otherwise, the most
108
- # acceptable value will be returned.
129
+ # Sorts the given +values+ according to the qvalue of each while
130
+ # preserving the original order. See #sort_with_qvalues for more
131
+ # information on exactly how the sort is performed.
132
+ def sort(values, keep_unacceptables=false)
133
+ sort_with_qvalues(values, keep_unacceptables).map {|q, v| v }
134
+ end
135
+
136
+ # A shortcut for retrieving the first result of #sort.
109
137
  def best_of(values, keep_unacceptables=false)
110
- s = sort(values)
111
- s.reject! {|q, v| q == 0 } unless keep_unacceptables
112
- s.first && s.first[1]
138
+ sort(values, keep_unacceptables).first
113
139
  end
114
140
 
115
141
  # Returns a string representation of this header.
@@ -20,7 +20,7 @@ module Rack::Accept
20
20
  return 1 if @qvalues.empty?
21
21
  m = matches(language)
22
22
  return 0 if m.empty?
23
- @qvalues[m.first]
23
+ normalize_qvalue(@qvalues[m.first])
24
24
  end
25
25
 
26
26
  # Returns an array of languages from this header that match the given
@@ -20,7 +20,7 @@ module Rack::Accept
20
20
  return 1 if @qvalues.empty?
21
21
  m = matches(media_type)
22
22
  return 0 if m.empty?
23
- @qvalues[m.first]
23
+ normalize_qvalue(@qvalues[m.first])
24
24
  end
25
25
 
26
26
  # Returns an array of media types from this header that match the given
@@ -32,7 +32,7 @@ module Rack::Accept
32
32
  true
33
33
  else
34
34
  t, s, p = parse_media_type(v)
35
- t == type && (s == subtype || s == '*') && (p == params || p == '')
35
+ t == type && (s == '*' || s == subtype) && (p == '' || params_match?(params, p))
36
36
  end
37
37
  }.sort_by {|v|
38
38
  # Most specific gets precedence.
@@ -40,5 +40,14 @@ module Rack::Accept
40
40
  }.reverse
41
41
  end
42
42
 
43
+ private
44
+
45
+ # Returns true if all parameters and values in +match+ are also present in
46
+ # +params+.
47
+ def params_match?(params, match)
48
+ return true if params == match
49
+ parsed = parse_range_params(params)
50
+ parsed == parsed.merge(parse_range_params(match))
51
+ end
43
52
  end
44
53
  end
@@ -34,4 +34,12 @@ class CharsetTest < Test::Unit::TestCase
34
34
  assert_equal(%w{*}, c.matches('unicode-1-1'))
35
35
  end
36
36
 
37
+ def test_best_of
38
+ c = C.new('iso-8859-5, unicode-1-1;q=0.8')
39
+ assert_equal('iso-8859-5', c.best_of(%w< iso-8859-5 unicode-1-1 >))
40
+ assert_equal('iso-8859-5', c.best_of(%w< iso-8859-5 utf-8 >))
41
+ assert_equal('iso-8859-1', c.best_of(%w< iso-8859-1 utf-8 >))
42
+ assert_equal(nil, c.best_of(%w< utf-8 >))
43
+ end
44
+
37
45
  end
@@ -21,4 +21,11 @@ class EncodingTest < Test::Unit::TestCase
21
21
  assert_equal(%w{*}, e.matches('compress'))
22
22
  end
23
23
 
24
+ def test_best_of
25
+ e = E.new('gzip, compress')
26
+ assert_equal('gzip', e.best_of(%w< gzip compress >))
27
+ assert_equal('identity', e.best_of(%w< identity compress >))
28
+ assert_equal(nil, e.best_of(%w< zip >))
29
+ end
30
+
24
31
  end
@@ -43,7 +43,23 @@ class HeaderTest < Test::Unit::TestCase
43
43
  assert_equal(['text', '*', ''], H.parse_media_type('text/*'))
44
44
  assert_equal(['text', 'html', ''], H.parse_media_type('text/html'))
45
45
  assert_equal(['text', 'html', 'level=1'], H.parse_media_type('text/html;level=1'))
46
+ assert_equal(['text', 'html', 'level=1;answer=42'], H.parse_media_type('text/html;level=1;answer=42'))
46
47
  assert_equal(['text', 'x-dvi', ''], H.parse_media_type('text/x-dvi'))
47
48
  end
48
49
 
50
+ def test_parse_range_params
51
+ assert_equal({}, H.parse_range_params(''))
52
+ assert_equal({}, H.parse_range_params('a'))
53
+ assert_equal({'a' => 'a'}, H.parse_range_params('a=a'))
54
+ assert_equal({'a' => 'a', 'b' => 'b'}, H.parse_range_params('a=a;b=b'))
55
+ end
56
+
57
+ def test_normalize_qvalue
58
+ assert_equal(1, H.normalize_qvalue(1.0))
59
+ assert_equal(0, H.normalize_qvalue(0.0))
60
+ assert_equal(1, H.normalize_qvalue(1))
61
+ assert_equal(0, H.normalize_qvalue(0))
62
+ assert_equal(0.5, H.normalize_qvalue(0.5))
63
+ end
64
+
49
65
  end
@@ -26,4 +26,12 @@ class LanguageTest < Test::Unit::TestCase
26
26
  assert_equal(%w{en-gb en}, l.matches('en-gb'))
27
27
  end
28
28
 
29
+ def test_best_of
30
+ l = L.new('en;q=0.5, en-gb')
31
+ assert_equal('en-gb', l.best_of(%w< en en-gb >))
32
+ assert_equal('en', l.best_of(%w< en da >))
33
+ assert_equal('en-us', l.best_of(%w< en-us en-au >))
34
+ assert_equal(nil, l.best_of(%w< da >))
35
+ end
36
+
29
37
  end
@@ -24,6 +24,19 @@ class MediaTypeTest < Test::Unit::TestCase
24
24
  assert_equal(%w{text/* */*}, m.matches('text/plain'))
25
25
  assert_equal(%w{text/html text/* */*}, m.matches('text/html'))
26
26
  assert_equal(%w{text/html;level=1 text/html text/* */*}, m.matches('text/html;level=1'))
27
+ assert_equal(%w{text/html;level=1 text/html text/* */*}, m.matches('text/html;level=1;answer=42'))
28
+ end
29
+
30
+ def test_best_of
31
+ m = M.new('text/*;q=0.5, text/html')
32
+ assert_equal('text/html', m.best_of(%w< text/plain text/html >))
33
+ assert_equal('text/plain', m.best_of(%w< text/plain image/png >))
34
+ assert_equal('text/plain', m.best_of(%w< text/plain text/javascript >))
35
+ assert_equal(nil, m.best_of(%w< image/png >))
36
+
37
+ m = M.new('text/*')
38
+ assert_equal('text/html', m.best_of(%w< text/html text/xml >))
39
+ assert_equal('text/xml', m.best_of(%w< text/xml text/html >))
27
40
  end
28
41
 
29
42
  end
@@ -5,70 +5,47 @@ class RequestTest < Test::Unit::TestCase
5
5
  R = Rack::Accept::Request
6
6
 
7
7
  def test_media_type
8
- request = R.new('HTTP_ACCEPT' => 'text/*;q=0, text/html')
9
- assert(request.media_type?('text/html'))
10
- assert(request.media_type?('text/html;level=1'))
11
- assert(!request.media_type?('text/plain'))
12
- assert(!request.media_type?('image/png'))
8
+ r = R.new('HTTP_ACCEPT' => 'text/*;q=0, text/html')
9
+ assert(r.media_type?('text/html'))
10
+ assert(r.media_type?('text/html;level=1'))
11
+ assert(!r.media_type?('text/plain'))
12
+ assert(!r.media_type?('image/png'))
13
13
 
14
14
  request = R.new('HTTP_ACCEPT' => '*/*')
15
15
  assert(request.media_type?('image/png'))
16
16
  end
17
17
 
18
- def test_best_media_type
19
- request = R.new('HTTP_ACCEPT' => 'text/*;q=0.5, text/html')
20
- assert_equal('text/html', request.best_media_type(%w< text/plain text/html >))
21
- assert_equal('text/plain', request.best_media_type(%w< text/plain image/png >))
22
- assert_equal('text/plain', request.best_media_type(%w< text/plain text/javascript >))
23
- assert_equal(nil, request.best_media_type(%w< image/png >))
24
- end
25
-
26
18
  def test_charset
27
- request = R.new('HTTP_ACCEPT_CHARSET' => 'iso-8859-5, unicode-1-1;q=0.8')
28
- assert(request.charset?('iso-8859-5'))
29
- assert(request.charset?('unicode-1-1'))
30
- assert(request.charset?('iso-8859-1'))
31
- assert(!request.charset?('utf-8'))
19
+ r = R.new('HTTP_ACCEPT_CHARSET' => 'iso-8859-5, unicode-1-1;q=0.8')
20
+ assert(r.charset?('iso-8859-5'))
21
+ assert(r.charset?('unicode-1-1'))
22
+ assert(r.charset?('iso-8859-1'))
23
+ assert(!r.charset?('utf-8'))
32
24
 
33
- request = R.new('HTTP_ACCEPT_CHARSET' => 'iso-8859-1;q=0')
34
- assert(!request.charset?('iso-8859-1'))
35
- end
36
-
37
- def test_best_charset
38
- request = R.new('HTTP_ACCEPT_CHARSET' => 'iso-8859-5, unicode-1-1;q=0.8')
39
- assert_equal('iso-8859-5', request.best_charset(%w< iso-8859-5 unicode-1-1 >))
40
- assert_equal('iso-8859-5', request.best_charset(%w< iso-8859-5 utf-8 >))
41
- assert_equal('iso-8859-1', request.best_charset(%w< iso-8859-1 utf-8 >))
42
- assert_equal(nil, request.best_charset(%w< utf-8 >))
25
+ r = R.new('HTTP_ACCEPT_CHARSET' => 'iso-8859-1;q=0')
26
+ assert(!r.charset?('iso-8859-1'))
43
27
  end
44
28
 
45
29
  def test_encoding
46
- request = R.new('HTTP_ACCEPT_ENCODING' => '')
47
- assert(request.encoding?('identity'))
48
- assert(!request.encoding?('gzip'))
30
+ r = R.new('HTTP_ACCEPT_ENCODING' => '')
31
+ assert(r.encoding?('identity'))
32
+ assert(!r.encoding?('gzip'))
49
33
 
50
- request = R.new('HTTP_ACCEPT_ENCODING' => 'gzip')
51
- assert(request.encoding?('identity'))
52
- assert(request.encoding?('gzip'))
53
- assert(!request.encoding?('compress'))
34
+ r = R.new('HTTP_ACCEPT_ENCODING' => 'gzip')
35
+ assert(r.encoding?('identity'))
36
+ assert(r.encoding?('gzip'))
37
+ assert(!r.encoding?('compress'))
54
38
 
55
- request = R.new('HTTP_ACCEPT_ENCODING' => 'gzip;q=0, *')
56
- assert(request.encoding?('compress'))
57
- assert(request.encoding?('identity'))
58
- assert(!request.encoding?('gzip'))
39
+ r = R.new('HTTP_ACCEPT_ENCODING' => 'gzip;q=0, *')
40
+ assert(r.encoding?('compress'))
41
+ assert(r.encoding?('identity'))
42
+ assert(!r.encoding?('gzip'))
59
43
 
60
- request = R.new('HTTP_ACCEPT_ENCODING' => 'identity;q=0')
61
- assert(!request.encoding?('identity'))
44
+ r = R.new('HTTP_ACCEPT_ENCODING' => 'identity;q=0')
45
+ assert(!r.encoding?('identity'))
62
46
 
63
- request = R.new('HTTP_ACCEPT_ENCODING' => '*;q=0')
64
- assert(!request.encoding?('identity'))
65
- end
66
-
67
- def test_best_encoding
68
- request = R.new('HTTP_ACCEPT_ENCODING' => 'gzip, compress')
69
- assert_equal('gzip', request.best_encoding(%w< gzip compress >))
70
- assert_equal('identity', request.best_encoding(%w< identity compress >))
71
- assert_equal(nil, request.best_encoding(%w< zip >))
47
+ r = R.new('HTTP_ACCEPT_ENCODING' => '*;q=0')
48
+ assert(!r.encoding?('identity'))
72
49
  end
73
50
 
74
51
  def test_language
@@ -82,12 +59,4 @@ class RequestTest < Test::Unit::TestCase
82
59
  assert(!request.language?('da'))
83
60
  end
84
61
 
85
- def test_best_language
86
- request = R.new('HTTP_ACCEPT_LANGUAGE' => 'en;q=0.5, en-gb')
87
- assert_equal('en-gb', request.best_language(%w< en en-gb >))
88
- assert_equal('en', request.best_language(%w< en da >))
89
- assert_equal('en-us', request.best_language(%w< en-us en-au >))
90
- assert_equal(nil, request.best_language(%w< da >))
91
- end
92
-
93
62
  end
metadata CHANGED
@@ -4,8 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 3
8
- version: "0.3"
7
+ - 4
8
+ - 1
9
+ version: 0.4.1
9
10
  platform: ruby
10
11
  authors:
11
12
  - Michael J. I. Jackson
@@ -13,7 +14,7 @@ autorequire:
13
14
  bindir: bin
14
15
  cert_chain: []
15
16
 
16
- date: 2010-04-02 00:00:00 -06:00
17
+ date: 2010-04-05 00:00:00 -06:00
17
18
  default_executable:
18
19
  dependencies:
19
20
  - !ruby/object:Gem::Dependency