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