http-accept 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 02b776cfc42af7c6a5cf36d154a76372a1190088
4
- data.tar.gz: 17e40217b15891a3ab68a9a8262687104a826ff0
3
+ metadata.gz: 409b179f21a3c570060f5d16802137a640c997e0
4
+ data.tar.gz: 93108cf6d8302906d14f1e0e979e990721e733a7
5
5
  SHA512:
6
- metadata.gz: 7abab3f7a1543dccffb606f0ca0136eeaebacb132d569abf614ba94469a9327c943972bb75860484b7a1d528476326760e5b4ea024ee18137972b99901c47bdb
7
- data.tar.gz: 46609f96228bb2b0608b086fcf6bfdcff40b3cb0b2074cbbd8383ad01e88dc229982c2a89bfd6a108e8a035ab0dbab13479c684900ecc002475ffe3f269cc55c
6
+ metadata.gz: 2c154231b09a91b992b3f6bb1fabb092782f19bcc11fcaed933adf9682632a9e28a31e438447dc5284635c346e25b0a97b8f74a7ad771ffa64addf305bf5d098
7
+ data.tar.gz: aeab4528e8ebf4ad756a95059ceaaeea3ad357d350a80b2f147b9c8f20ebb3c3500a8c6e9e94bb74e9774823ae39e36abcd3317d7c9e95c0816799d302b788f6
data/README.md CHANGED
@@ -28,6 +28,8 @@ Here are some examples of how to parse various headers.
28
28
 
29
29
  ### Parsing Accept: headers
30
30
 
31
+ You can parse the incoming `Accept:` header:
32
+
31
33
  media_types = HTTP::Accept::MediaTypes.parse("text/html;q=0.5, application/json; version=1")
32
34
 
33
35
  expect(media_types[0].mime_type).to be == "application/json"
@@ -35,14 +37,51 @@ Here are some examples of how to parse various headers.
35
37
  expect(media_types[1].mime_type).to be == "text/html"
36
38
  expect(media_types[1].parameters).to be == {'q' => '0.5'}
37
39
 
40
+ Normally, you'd want to match the media types against some set of available mime types:
41
+
42
+ module ToJSON
43
+ def content_type
44
+ "application/json"
45
+ end
46
+
47
+ def convert(object, options)
48
+ object.to_json
49
+ end
50
+ end
51
+
52
+ module ToXML
53
+ # Are you kidding?
54
+ end
55
+
56
+ map = HTTP::Accept::MediaTypes::Map.new
57
+ map << ToJSON
58
+ map << ToXML
59
+
60
+ object, media_range = map.for(media_types)
61
+ content = object.convert(model, media_range.parameters)
62
+ response = [200, {'Content-Type' => object.content_type}, [content]]
63
+
38
64
  ### Parsing Accept-Language: headers
39
65
 
66
+ You can parse the incoming `Accept-Language:` header:
67
+
40
68
  languages = HTTP::Accept::Language.parse("da, en-gb;q=0.8, en;q=0.7")
41
69
 
42
70
  expect(languages[0].locale).to be == "da"
43
71
  expect(languages[1].locale).to be == "en-gb"
44
72
  expect(languages[2].locale).to be == "en"`
45
73
 
74
+ Normally, you'd want to match the languages against some set of available localizations:
75
+
76
+ available_localizations = HTTP::Accept::Languages::Locales.new(["en-nz", "en-us"])
77
+
78
+ # Given the languages that the user wants, and the localizations available, compute the set of desired localizations.
79
+ desired_localizations = available_localizations & languages
80
+
81
+ The `desired_localizations` in the example above is a subset of `available_localizations`.
82
+
83
+ `HTTP::Accept::Languages::Locales` provides an efficient data-structure for matching the Accept-Languages header to set of available localizations according to https://tools.ietf.org/html/rfc7231#section-5.3.5 and https://tools.ietf.org/html/rfc4647#section-2.3
84
+
46
85
  ## Contributing
47
86
 
48
87
  1. Fork it
@@ -25,7 +25,7 @@ require_relative 'sort'
25
25
 
26
26
  module HTTP
27
27
  module Accept
28
- class Languages
28
+ module Languages
29
29
  LOCALE = /\*|[A-Z]{1,8}(-[A-Z]{1,8})*/i
30
30
 
31
31
  # https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9
@@ -33,6 +33,48 @@ module HTTP
33
33
 
34
34
  LANGUAGE_RANGE = /(?<locale>#{LOCALE})(;q=(?<q>#{QVALUE}))?/
35
35
 
36
+ # Provides an efficient data-structure for matching the Accept-Languages header to set of available locales according to https://tools.ietf.org/html/rfc7231#section-5.3.5 and https://tools.ietf.org/html/rfc4647#section-2.3
37
+ class Locales < Array
38
+ def self.expand(locale, into)
39
+ parts = locale.split('-')
40
+
41
+ while parts.size > 0
42
+ key = parts.join('-')
43
+
44
+ into[key] ||= locale
45
+
46
+ parts.pop
47
+ end
48
+ end
49
+
50
+ def initialize(names)
51
+ super(names)
52
+
53
+ @patterns = {}
54
+
55
+ self.each{|name| self.class.expand(name, @patterns)}
56
+
57
+ self.freeze
58
+ end
59
+
60
+ def freeze
61
+ @patterns.freeze
62
+
63
+ super
64
+ end
65
+
66
+ attr :patterns
67
+
68
+ # Returns the intersection of others retaining order.
69
+ def & languages
70
+ languages.collect{|language_range| @patterns[language_range.locale]}.compact
71
+ end
72
+
73
+ def include? locale_name
74
+ @patterns.include? locale_name
75
+ end
76
+ end
77
+
36
78
  class LanguageRange < Struct.new(:locale, :q)
37
79
  def quality_factor
38
80
  (q || 1.0).to_f
@@ -50,11 +92,6 @@ module HTTP
50
92
 
51
93
  raise ParseError.new("Could not parse entire string!") unless scanner.eos?
52
94
  end
53
-
54
- # If the user ask for 'en', this satisfies any language that begins with 'en-'
55
- def prefix_of? other
56
- other.start_with(locale)
57
- end
58
95
  end
59
96
 
60
97
  def self.parse(text)
@@ -67,4 +104,3 @@ module HTTP
67
104
  end
68
105
  end
69
106
  end
70
-
@@ -26,18 +26,73 @@ require_relative 'sort'
26
26
 
27
27
  module HTTP
28
28
  module Accept
29
- class MediaTypes
29
+ module MediaTypes
30
30
  # According to https://tools.ietf.org/html/rfc7231#section-5.3.2
31
31
  MIME_TYPE = /(#{TOKEN})\/(#{TOKEN})/
32
32
  PARAMETER = /\s*;\s*(?<key>#{TOKEN})=((?<value>#{TOKEN})|(?<quoted_value>#{QUOTED_STRING}))/
33
33
 
34
+ # Map a set of mime types to objects.
35
+ class Map
36
+ WILDCARD = '*'.freeze
37
+
38
+ def initialize
39
+ @media_types = Hash.new{|h,k| h[k] = {}}
40
+
41
+ # Primarily for implementing #freeze efficiently.
42
+ @all = []
43
+ end
44
+
45
+ def freeze
46
+ @media_types.freeze
47
+ @media_types.each{|key,value| value.freeze}
48
+
49
+ @all.freeze
50
+ @all.each(&:freeze)
51
+
52
+ super
53
+ end
54
+
55
+ # Given a list of content types (e.g. from browser_preferred_content_types), return the best converter.
56
+ def for(media_types)
57
+ media_types.each do |media_range|
58
+ type, subtype = media_range.split
59
+
60
+ if object = @media_types[type][subtype]
61
+ return object, media_range
62
+ end
63
+ end
64
+
65
+ return nil
66
+ end
67
+
68
+ # Add a converter to the collection. A converter can be anything that responds to #content_type.
69
+ def << object
70
+ type, subtype = object.content_type.split('/')
71
+
72
+ if @media_types.empty?
73
+ @media_types[WILDCARD][WILDCARD] = object
74
+ end
75
+
76
+ if @media_types[type].empty?
77
+ @media_types[type][WILDCARD] = object
78
+ end
79
+
80
+ @media_types[type][subtype] = object
81
+ @all << object
82
+ end
83
+ end
84
+
34
85
  class MediaRange < Struct.new(:mime_type, :parameters)
35
86
  def quality_factor
36
87
  parameters.fetch('q', 1.0).to_f
37
88
  end
38
89
 
39
- def self.parse(scanner)
40
- return to_enum(:parse, scanner) unless block_given?
90
+ def split
91
+ @type, @subtype = mime_type.split('/')
92
+ end
93
+
94
+ def self.parse(scanner, normalize_whitespace = true)
95
+ return to_enum(:parse, scanner, normalize_whitespace) unless block_given?
41
96
 
42
97
  while mime_type = scanner.scan(MIME_TYPE)
43
98
  parameters = {}
@@ -48,7 +103,7 @@ module HTTP
48
103
  if value = scanner[:value]
49
104
  parameters[key] = value
50
105
  elsif quoted_value = scanner[:quoted_value]
51
- parameters[key] = QuotedString.new(quoted_value)
106
+ parameters[key] = QuotedString.unquote(quoted_value, normalize_whitespace)
52
107
  else
53
108
  raise ParseError.new("Could not parse parameter!")
54
109
  end
@@ -64,10 +119,10 @@ module HTTP
64
119
  end
65
120
  end
66
121
 
67
- def self.parse(text)
122
+ def self.parse(text, normalize_whitespace = true)
68
123
  scanner = StringScanner.new(text)
69
124
 
70
- media_types = MediaRange.parse(scanner)
125
+ media_types = MediaRange.parse(scanner, normalize_whitespace)
71
126
 
72
127
  return Sort.by_quality_factor(media_types)
73
128
  end
@@ -24,30 +24,21 @@ module HTTP
24
24
  TOKEN = /[!#$%&'*+\-.^_`|~0-9A-Z]+/i
25
25
  QUOTED_STRING = /"(?:.(?!(?<!\\)"))*.?"/
26
26
 
27
- class QuotedString
28
- def initialize(value)
29
- @value = value
30
- end
31
-
32
- def unquote(normalize_whitespace = false)
33
- value = @value[1...-1]
27
+ module QuotedString
28
+ # Unquote a "quoted-string" value according to https://tools.ietf.org/html/rfc7230#section-3.2.6
29
+ # It should already match the QUOTED_STRING pattern above by the parser.
30
+ def self.unquote(value, normalize_whitespace = true)
31
+ value = value[1...-1]
34
32
 
35
33
  value.gsub!(/\\(.)/, '\1')
36
34
 
37
35
  if normalize_whitespace
38
- value.gsub!(/[\r\n]\s+/, ' ')
36
+ # LWS = [CRLF] 1*( SP | HT )
37
+ value.gsub!(/[\r\n]+\s+/, ' ')
39
38
  end
40
39
 
41
40
  return value
42
41
  end
43
-
44
- def to_str
45
- unquote(true)
46
- end
47
-
48
- def to_s
49
- to_str
50
- end
51
42
  end
52
43
  end
53
44
  end
@@ -20,6 +20,6 @@
20
20
 
21
21
  module HTTP
22
22
  module Accept
23
- VERSION = "1.0.1"
23
+ VERSION = "1.1.0"
24
24
  end
25
25
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: http-accept
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-02-03 00:00:00.000000000 Z
11
+ date: 2016-02-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler