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 +4 -4
- data/README.md +39 -0
- data/lib/http/accept/languages.rb +43 -7
- data/lib/http/accept/media_types.rb +61 -6
- data/lib/http/accept/quoted_string.rb +7 -16
- data/lib/http/accept/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 409b179f21a3c570060f5d16802137a640c997e0
|
4
|
+
data.tar.gz: 93108cf6d8302906d14f1e0e979e990721e733a7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
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
|
40
|
-
|
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.
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
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
|
data/lib/http/accept/version.rb
CHANGED
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
|
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-
|
11
|
+
date: 2016-02-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|