xenon 0.0.1 → 0.0.2

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.
@@ -0,0 +1,54 @@
1
+ require 'xenon/headers/accept'
2
+
3
+ describe Xenon::Headers::Accept do
4
+
5
+ context '::parse' do
6
+ it 'can parse a basic accept range' do
7
+ header = Xenon::Headers::Accept.parse('text/plain; q=0.5')
8
+ expect(header.media_ranges.size).to eq(1)
9
+ expect(header.media_ranges[0].to_s).to eq('text/plain; q=0.5')
10
+ end
11
+
12
+ it 'can parse the first example from RFC 7231 § 5.3.2 with the right precedence' do
13
+ header = Xenon::Headers::Accept.parse('text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c')
14
+ expect(header.media_ranges.size).to eq(4)
15
+ expect(header.media_ranges[0].to_s).to eq('text/html')
16
+ expect(header.media_ranges[1].to_s).to eq('text/x-c')
17
+ expect(header.media_ranges[2].to_s).to eq('text/x-dvi; q=0.8')
18
+ expect(header.media_ranges[3].to_s).to eq('text/plain; q=0.5')
19
+ end
20
+
21
+ it 'can parse the second example from RFC 7231 § 5.3.2 with the right precedence' do
22
+ header = Xenon::Headers::Accept.parse('text/*, text/plain, text/plain;format=flowed, */*')
23
+ expect(header.media_ranges.size).to eq(4)
24
+ expect(header.media_ranges[0].to_s).to eq('text/plain; format=flowed')
25
+ expect(header.media_ranges[1].to_s).to eq('text/plain')
26
+ expect(header.media_ranges[2].to_s).to eq('text/*')
27
+ expect(header.media_ranges[3].to_s).to eq('*/*')
28
+ end
29
+
30
+ it 'can parse the third example from RFC 7231 § 5.3.2 with the right precedence' do
31
+ header = Xenon::Headers::Accept.parse('text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5')
32
+ expect(header.media_ranges.size).to eq(5)
33
+ expect(header.media_ranges[0].to_s).to eq('text/html; level=1')
34
+ expect(header.media_ranges[1].to_s).to eq('text/html; level=2; q=0.4')
35
+ expect(header.media_ranges[2].to_s).to eq('text/html; q=0.7')
36
+ expect(header.media_ranges[3].to_s).to eq('text/*; q=0.3')
37
+ expect(header.media_ranges[4].to_s).to eq('*/*; q=0.5')
38
+ end
39
+ end
40
+
41
+ context '#merge' do
42
+ it 'can merge two headers with the right precedence' do
43
+ h1 = Xenon::Headers::Accept.parse('text/plain; q=0.5, text/html')
44
+ h2 = Xenon::Headers::Accept.parse('text/x-c, text/x-dvi; q=0.8')
45
+ header = h1.merge(h2)
46
+ expect(header.media_ranges.size).to eq(4)
47
+ expect(header.media_ranges[0].to_s).to eq('text/html')
48
+ expect(header.media_ranges[1].to_s).to eq('text/x-c')
49
+ expect(header.media_ranges[2].to_s).to eq('text/x-dvi; q=0.8')
50
+ expect(header.media_ranges[3].to_s).to eq('text/plain; q=0.5')
51
+ end
52
+ end
53
+
54
+ end
@@ -0,0 +1,47 @@
1
+ require 'xenon/headers/authorization'
2
+
3
+ describe Xenon::Headers::Authorization do
4
+
5
+ context '::parse' do
6
+ it 'can parse Basic credentials' do
7
+ header = Xenon::Headers::Authorization.parse('Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==')
8
+ expect(header.credentials.class).to eq(Xenon::BasicCredentials)
9
+ expect(header.credentials.username).to eq('Aladdin')
10
+ expect(header.credentials.password).to eq('open sesame')
11
+ end
12
+
13
+ it 'can parse Digest credentials' do
14
+ header = Xenon::Headers::Authorization.parse('Digest username="Mufasa"' +
15
+ ', realm="testrealm@host.com"' +
16
+ ', nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093"' +
17
+ ', uri="/dir/index.html"' +
18
+ ', qop=auth' +
19
+ ', nc=00000001' +
20
+ ', cnonce="0a4f113b"' +
21
+ ', response="6629fae49393a05397450978507c4ef1"' +
22
+ ', opaque="5ccc069c403ebaf9f0171e9517f40e41"')
23
+ expect(header.credentials.class).to eq(Xenon::GenericCredentials)
24
+ expect(header.credentials.scheme).to eq('Digest')
25
+ expect(header.credentials.token).to eq(nil)
26
+ expect(header.credentials.params).to eq(
27
+ 'username' => 'Mufasa',
28
+ 'realm' => 'testrealm@host.com',
29
+ 'nonce' => 'dcd98b7102dd2f0e8b11d0f600bfb0c093',
30
+ 'uri' => '/dir/index.html',
31
+ 'qop' => 'auth',
32
+ 'nc' => '00000001',
33
+ 'cnonce' => '0a4f113b',
34
+ 'response' => '6629fae49393a05397450978507c4ef1',
35
+ 'opaque' => '5ccc069c403ebaf9f0171e9517f40e41')
36
+ end
37
+
38
+ it 'can parse Bearer credentials' do
39
+ header = Xenon::Headers::Authorization.parse('Bearer eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.')
40
+ expect(header.credentials.class).to eq(Xenon::GenericCredentials)
41
+ expect(header.credentials.scheme).to eq('Bearer')
42
+ expect(header.credentials.token).to eq('eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.')
43
+ expect(header.credentials.params).to eq({})
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,64 @@
1
+ require 'xenon/headers/cache_control'
2
+
3
+ describe Xenon::Headers::CacheControl do
4
+
5
+ context '::parse' do
6
+ ['max-age', 'max-stale', 'min-fresh', 's-maxage'].each do |dir|
7
+ it "can parse the #{dir} directive" do
8
+ header = Xenon::Headers::CacheControl.parse("#{dir}=5")
9
+ expect(header.directives.size).to eq(1)
10
+ expect(header.directives[0].to_s).to eq("#{dir}=5")
11
+ end
12
+
13
+ it "can parse the #{dir} directive with a quoted value" do # should not be sent by clients but is permitted
14
+ header = Xenon::Headers::CacheControl.parse("#{dir}=\"5\"")
15
+ expect(header.directives.size).to eq(1)
16
+ expect(header.directives[0].to_s).to eq("#{dir}=5")
17
+ end
18
+ end
19
+
20
+ ['no-cache', 'no-store', 'no-transform', 'only-if-cached', 'must-revalidate', 'public', 'proxy-revalidate'].each do |dir|
21
+ it "can parse the #{dir} directive" do
22
+ header = Xenon::Headers::CacheControl.parse("#{dir}")
23
+ expect(header.directives.size).to eq(1)
24
+ expect(header.directives[0].to_s).to eq("#{dir}")
25
+ end
26
+ end
27
+
28
+ it "can parse the private directive with no field names" do
29
+ header = Xenon::Headers::CacheControl.parse('private')
30
+ expect(header.directives.size).to eq(1)
31
+ expect(header.directives[0].to_s).to eq('private')
32
+ end
33
+
34
+ # TODO: private directive with field names
35
+
36
+ it 'can parse extension directives with quoted string values' do
37
+ header = Xenon::Headers::CacheControl.parse('ext="hello \"world\""')
38
+ expect(header.directives.size).to eq(1)
39
+ expect(header.directives[0].name).to eq('ext')
40
+ expect(header.directives[0].value).to eq("hello \"world\"")
41
+ end
42
+
43
+ it 'can parse more complex directives' do
44
+ header = Xenon::Headers::CacheControl.parse('public, max-age=3600, must-revalidate')
45
+ expect(header.directives.size).to eq(3)
46
+ expect(header.directives[0].to_s).to eq('public')
47
+ expect(header.directives[1].to_s).to eq('max-age=3600')
48
+ expect(header.directives[2].to_s).to eq('must-revalidate')
49
+ end
50
+ end
51
+
52
+ context '#merge' do
53
+ it 'can merge two headers and maintain directive order' do
54
+ h1 = Xenon::Headers::CacheControl.parse('public, max-age=3600')
55
+ h2 = Xenon::Headers::CacheControl.parse('must-revalidate')
56
+ header = h1.merge(h2)
57
+ expect(header.directives.size).to eq(3)
58
+ expect(header.directives[0].to_s).to eq('public')
59
+ expect(header.directives[1].to_s).to eq('max-age=3600')
60
+ expect(header.directives[2].to_s).to eq('must-revalidate')
61
+ end
62
+ end
63
+
64
+ end
@@ -0,0 +1,267 @@
1
+ require 'xenon/media_type'
2
+
3
+ describe Xenon::MediaType do
4
+
5
+ context '::parse' do
6
+ it 'can parse basic media types' do
7
+ mt = Xenon::MediaType.parse('application/json')
8
+ expect(mt.type).to eq('application')
9
+ expect(mt.subtype).to eq('json')
10
+ end
11
+ it 'can parse media types with a subtype suffix' do
12
+ mt = Xenon::MediaType.parse('application/rss+xml')
13
+ expect(mt.type).to eq('application')
14
+ expect(mt.subtype).to eq('rss+xml')
15
+ end
16
+
17
+ it 'can parse media types with parameters' do
18
+ mt = Xenon::MediaType.parse('text/plain; format=flowed; paged')
19
+ expect(mt.type).to eq('text')
20
+ expect(mt.subtype).to eq('plain')
21
+ expect(mt.params).to eq({ 'format' => 'flowed', 'paged' => nil })
22
+ end
23
+
24
+ it 'strips whitespace around separators' do
25
+ mt = Xenon::MediaType.parse('text/plain ; format = flowed ; paged')
26
+ expect(mt.type).to eq('text')
27
+ expect(mt.subtype).to eq('plain')
28
+ expect(mt.params).to eq({ 'format' => 'flowed', 'paged' => nil })
29
+ end
30
+
31
+ it 'raises an error when the media type contains wildcards' do
32
+ expect { Xenon::MediaType.parse('*/*') }.to raise_error(Xenon::ParseError)
33
+ expect { Xenon::MediaType.parse('application/*') }.to raise_error(Xenon::ParseError)
34
+ end
35
+
36
+ it 'raises an error when the media type is invalid' do
37
+ expect { Xenon::MediaType.parse('application') }.to raise_error(Xenon::ParseError)
38
+ expect { Xenon::MediaType.parse('application; foo=bar') }.to raise_error(Xenon::ParseError)
39
+ expect { Xenon::MediaType.parse('/json') }.to raise_error(Xenon::ParseError)
40
+ expect { Xenon::MediaType.parse('/json; foo=bar') }.to raise_error(Xenon::ParseError)
41
+ end
42
+ end
43
+
44
+ %w(application audio image message multipart text video).each do |type|
45
+ context "#{type}?" do
46
+ it "returns true when the root type is '#{type}'" do
47
+ mt = Xenon::MediaType.new(type, 'dummy')
48
+ expect(mt.send("#{type}?")).to eq(true)
49
+ end
50
+
51
+ it "returns false when the root type is not '#{type}'" do
52
+ mt = Xenon::MediaType.new('dummy', 'dummy')
53
+ expect(mt.send("#{type}?")).to eq(false)
54
+ end
55
+ end
56
+ end
57
+
58
+ { experimental?: 'x', personal?: 'prs', vendor?: 'vnd' }.each do |method, prefix|
59
+ context method do
60
+ it "returns true when the subtype starts with '#{prefix}.'" do
61
+ mt = Xenon::MediaType.new('application', "#{prefix}.dummy")
62
+ expect(mt.send(method)).to eq(true)
63
+ end
64
+
65
+ it "returns false when the subtype does not start with '#{prefix}.'" do
66
+ mt = Xenon::MediaType.new('application', "dummy.dummy")
67
+ expect(mt.send(method)).to eq(false)
68
+ end
69
+ end
70
+ end
71
+
72
+ %w(ber der fastinfoset json wbxml xml zip).each do |format|
73
+ context "#{format}?" do
74
+ it "returns true when the subtype is '#{format}'" do
75
+ mt = Xenon::MediaType.new('application', format)
76
+ expect(mt.send("#{format}?")).to eq(true)
77
+ end
78
+
79
+ it "returns true when the subtype ends with '+#{format}'" do
80
+ mt = Xenon::MediaType.new('application', "dummy+#{format}")
81
+ expect(mt.send("#{format}?")).to eq(true)
82
+ end
83
+
84
+ it "returns false when the subtype is not '#{format}' and does not end with '+#{format}'" do
85
+ mt = Xenon::MediaType.new('dummy', 'dummy+dummy')
86
+ expect(mt.send("#{format}?")).to eq(false)
87
+ end
88
+ end
89
+ end
90
+
91
+ context '#to_s' do
92
+ it 'returns the string representation of a media type' do
93
+ mt = Xenon::MediaType.new('text', 'plain', 'format' => 'flowed', 'paged' => nil)
94
+ expect(mt.to_s).to eq('text/plain; format=flowed; paged')
95
+ end
96
+ end
97
+
98
+ end
99
+
100
+ describe Xenon::MediaRange do
101
+
102
+ context '::parse' do
103
+ it 'can parse basic media ranges' do
104
+ mt = Xenon::MediaRange.parse('application/json')
105
+ expect(mt.type).to eq('application')
106
+ expect(mt.subtype).to eq('json')
107
+ end
108
+
109
+ it 'can parse media ranges with parameters' do
110
+ mt = Xenon::MediaRange.parse('text/plain; format=flowed; paged')
111
+ expect(mt.type).to eq('text')
112
+ expect(mt.subtype).to eq('plain')
113
+ expect(mt.params).to eq({ 'format' => 'flowed', 'paged' => nil })
114
+ end
115
+
116
+ it 'strips whitespace around separators' do
117
+ mt = Xenon::MediaRange.parse('text/plain ; format = flowed ; paged')
118
+ expect(mt.type).to eq('text')
119
+ expect(mt.subtype).to eq('plain')
120
+ expect(mt.params).to eq({ 'format' => 'flowed', 'paged' => nil })
121
+ end
122
+
123
+ it 'can parse media ranges with subtype wildcards' do
124
+ mt = Xenon::MediaRange.parse('application/*')
125
+ expect(mt.type).to eq('application')
126
+ expect(mt.subtype).to eq('*')
127
+ end
128
+
129
+ it 'can parse media ranges with type and subtype wildcards' do
130
+ mt = Xenon::MediaRange.parse('*/*')
131
+ expect(mt.type).to eq('*')
132
+ expect(mt.subtype).to eq('*')
133
+ end
134
+
135
+ it 'extracts q from the parameters' do
136
+ mt = Xenon::MediaRange.parse('text/plain; q=0.8; format=flowed; paged')
137
+ expect(mt.type).to eq('text')
138
+ expect(mt.subtype).to eq('plain')
139
+ expect(mt.q).to eq(0.8)
140
+ expect(mt.params).to eq({ 'format' => 'flowed', 'paged' => nil })
141
+ end
142
+
143
+ it 'uses the default value for q if the value is not numeric' do
144
+ mt = Xenon::MediaRange.parse('application/json; q=foo')
145
+ expect(mt.type).to eq('application')
146
+ expect(mt.subtype).to eq('json')
147
+ expect(mt.q).to eq(Xenon::MediaRange::DEFAULT_Q)
148
+ end
149
+
150
+ it 'raises an error when the media range is invalid' do
151
+ expect { Xenon::MediaRange.parse('application') }.to raise_error(Xenon::ParseError)
152
+ expect { Xenon::MediaRange.parse('application; foo=bar') }.to raise_error(Xenon::ParseError)
153
+ expect { Xenon::MediaRange.parse('*/json') }.to raise_error(Xenon::ParseError)
154
+ expect { Xenon::MediaRange.parse('/json') }.to raise_error(Xenon::ParseError)
155
+ expect { Xenon::MediaRange.parse('/json; foo=bar') }.to raise_error(Xenon::ParseError)
156
+ end
157
+ end
158
+
159
+ context '#<=>' do
160
+ it 'considers a wildcard type less than a regular type' do
161
+ mr1 = Xenon::MediaRange.new('*', '*')
162
+ mr2 = Xenon::MediaRange.new('text', '*')
163
+ expect(mr1 <=> mr2).to eq(-1)
164
+ end
165
+
166
+ it 'considers a wildcard subtype less than a regular subtype' do
167
+ mr1 = Xenon::MediaRange.new('application', '*')
168
+ mr2 = Xenon::MediaRange.new('text', 'plain')
169
+ expect(mr1 <=> mr2).to eq(-1)
170
+ end
171
+
172
+ it 'considers media ranges with type and subtype equal' do
173
+ mr1 = Xenon::MediaRange.new('application', 'json')
174
+ mr2 = Xenon::MediaRange.new('text', 'plain')
175
+ expect(mr1 <=> mr2).to eq(0)
176
+ end
177
+
178
+ it 'considers a media range with parameters greater than one without' do
179
+ mr1 = Xenon::MediaRange.new('text', 'plain', 'format' => 'flowed')
180
+ mr2 = Xenon::MediaRange.new('text', 'plain')
181
+ expect(mr1 <=> mr2).to eq(1)
182
+ end
183
+
184
+ it 'does not consider the quality when one media range is more specific' do
185
+ mr1 = Xenon::MediaRange.new('application', '*', 'q' => '0.3')
186
+ mr2 = Xenon::MediaRange.new('*', '*', 'q' => '0.5')
187
+ expect(mr1 <=> mr2).to eq(1)
188
+ end
189
+
190
+ it 'considers the quality when media ranges are equally specific' do
191
+ mr1 = Xenon::MediaRange.new('application', 'json', 'q' => '0.8')
192
+ mr2 = Xenon::MediaRange.new('application', 'xml')
193
+ expect(mr1 <=> mr2).to eq(-1)
194
+ end
195
+ end
196
+
197
+ %i(=~ ===).each do |name|
198
+ context "##{name}" do
199
+ it 'returns true when the type and subtype are wildcards' do
200
+ mr = Xenon::MediaRange.new('*', '*')
201
+ mt = Xenon::MediaType.new('application', 'json')
202
+ expect(mr.send(name, mt)).to eq(true)
203
+ end
204
+
205
+ it 'returns true when the type matches and subtype is a wildcard' do
206
+ mr = Xenon::MediaRange.new('application', '*')
207
+ mt = Xenon::MediaType.new('application', 'json')
208
+ expect(mr.send(name, mt)).to eq(true)
209
+ end
210
+
211
+ it 'returns true when the type and subtype match exactly' do
212
+ mr = Xenon::MediaRange.new('application', 'json')
213
+ mt = Xenon::MediaType.new('application', 'json')
214
+ expect(mr.send(name, mt)).to eq(true)
215
+ end
216
+
217
+ it 'returns true when the type, subtype and parameters match exactly' do
218
+ mr = Xenon::MediaRange.new('text', 'plain', 'format' => 'flowed')
219
+ mt = Xenon::MediaType.new('text', 'plain', 'format' => 'flowed', 'paged' => nil)
220
+ expect(mr.send(name, mt)).to eq(true)
221
+ end
222
+
223
+ it 'returns true when the the media type has more specific parameters' do
224
+ mr = Xenon::MediaRange.new('text', 'plain')
225
+ mt = Xenon::MediaType.new('text', 'plain', 'format' => 'flowed', 'paged' => nil)
226
+ expect(mr.send(name, mt)).to eq(true)
227
+ end
228
+
229
+ it 'returns false when the type is different' do
230
+ mr = Xenon::MediaRange.new('text', 'json')
231
+ mt = Xenon::MediaType.new('application', 'json')
232
+ expect(mr.send(name, mt)).to eq(false)
233
+ end
234
+
235
+ it 'returns false when the type matches but subtype is different' do
236
+ mr = Xenon::MediaRange.new('application', 'xml')
237
+ mt = Xenon::MediaType.new('application', 'json')
238
+ expect(mr.send(name, mt)).to eq(false)
239
+ end
240
+
241
+ it 'returns false when the media range has more specific parameters' do
242
+ mr = Xenon::MediaRange.new('text', 'plain', 'format' => 'flowed', 'paged' => nil)
243
+ mt = Xenon::MediaType.new('text', 'plain')
244
+ expect(mr.send(name, mt)).to eq(false)
245
+ end
246
+
247
+ it 'returns false when the media range has a different parameter value' do
248
+ mr = Xenon::MediaRange.new('text', 'plain', 'format' => 'flowed')
249
+ mt = Xenon::MediaType.new('text', 'plain', 'format' => 'linear')
250
+ expect(mr.send(name, mt)).to eq(false)
251
+ end
252
+ end
253
+ end
254
+
255
+ context '#to_s' do
256
+ it 'returns the string representation of a media range' do
257
+ mt = Xenon::MediaRange.new('text', 'plain', 'q' => 0.8, 'format' => 'flowed', 'paged' => nil)
258
+ expect(mt.to_s).to eq('text/plain; format=flowed; paged; q=0.8')
259
+ end
260
+
261
+ it 'omits the q parameter when it is 1.0' do
262
+ mt = Xenon::MediaRange.new('application', 'json', 'q' => 1.0)
263
+ expect(mt.to_s).to eq('application/json')
264
+ end
265
+ end
266
+
267
+ end
data/xenon.gemspec CHANGED
@@ -18,7 +18,12 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ['lib']
20
20
 
21
+ spec.required_ruby_version = '>= 2.2.0'
22
+
23
+ spec.add_runtime_dependency 'activesupport', '~> 4.0'
24
+ spec.add_runtime_dependency 'parslet', '~> 1.7'
25
+
21
26
  spec.add_development_dependency 'bundler', '~> 1.6'
22
- spec.add_development_dependency 'rake'
27
+ spec.add_development_dependency 'rake', '~> 10.0'
23
28
  spec.add_development_dependency 'rspec', '~> 3.0'
24
29
  end