rack-accept 0.3 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
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