http-accept 1.0.1 → 1.1.0

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.
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