xenon-routing 0.0.4
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 +7 -0
- data/.codeclimate.yml +18 -0
- data/.gitignore +25 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/Gemfile +20 -0
- data/Guardfile +16 -0
- data/LICENSE +22 -0
- data/README.md +116 -0
- data/Rakefile +40 -0
- data/VERSION +1 -0
- data/examples/hello_world/config.ru +3 -0
- data/examples/hello_world/hello_world.rb +27 -0
- data/xenon-http/lib/xenon/auth.rb +63 -0
- data/xenon-http/lib/xenon/errors.rb +5 -0
- data/xenon-http/lib/xenon/etag.rb +48 -0
- data/xenon-http/lib/xenon/headers.rb +112 -0
- data/xenon-http/lib/xenon/headers/accept.rb +34 -0
- data/xenon-http/lib/xenon/headers/accept_charset.rb +59 -0
- data/xenon-http/lib/xenon/headers/accept_encoding.rb +63 -0
- data/xenon-http/lib/xenon/headers/accept_language.rb +59 -0
- data/xenon-http/lib/xenon/headers/authorization.rb +50 -0
- data/xenon-http/lib/xenon/headers/cache_control.rb +56 -0
- data/xenon-http/lib/xenon/headers/content_type.rb +23 -0
- data/xenon-http/lib/xenon/headers/if_match.rb +53 -0
- data/xenon-http/lib/xenon/headers/if_modified_since.rb +22 -0
- data/xenon-http/lib/xenon/headers/if_none_match.rb +53 -0
- data/xenon-http/lib/xenon/headers/if_range.rb +45 -0
- data/xenon-http/lib/xenon/headers/if_unmodified_since.rb +22 -0
- data/xenon-http/lib/xenon/headers/user_agent.rb +65 -0
- data/xenon-http/lib/xenon/headers/www_authenticate.rb +71 -0
- data/xenon-http/lib/xenon/http.rb +7 -0
- data/xenon-http/lib/xenon/http_version.rb +3 -0
- data/xenon-http/lib/xenon/media_type.rb +162 -0
- data/xenon-http/lib/xenon/parsers/basic_rules.rb +86 -0
- data/xenon-http/lib/xenon/parsers/header_rules.rb +60 -0
- data/xenon-http/lib/xenon/parsers/media_type.rb +53 -0
- data/xenon-http/lib/xenon/quoted_string.rb +20 -0
- data/xenon-http/spec/spec_helper.rb +94 -0
- data/xenon-http/spec/xenon/etag_spec.rb +19 -0
- data/xenon-http/spec/xenon/headers/accept_charset_spec.rb +31 -0
- data/xenon-http/spec/xenon/headers/accept_encoding_spec.rb +40 -0
- data/xenon-http/spec/xenon/headers/accept_language_spec.rb +33 -0
- data/xenon-http/spec/xenon/headers/accept_spec.rb +54 -0
- data/xenon-http/spec/xenon/headers/authorization_spec.rb +47 -0
- data/xenon-http/spec/xenon/headers/cache_control_spec.rb +64 -0
- data/xenon-http/spec/xenon/headers/if_match_spec.rb +73 -0
- data/xenon-http/spec/xenon/headers/if_modified_since_spec.rb +19 -0
- data/xenon-http/spec/xenon/headers/if_none_match_spec.rb +79 -0
- data/xenon-http/spec/xenon/headers/if_range_spec.rb +45 -0
- data/xenon-http/spec/xenon/headers/if_unmodified_since_spec.rb +19 -0
- data/xenon-http/spec/xenon/headers/user_agent_spec.rb +67 -0
- data/xenon-http/spec/xenon/headers/www_authenticate_spec.rb +43 -0
- data/xenon-http/spec/xenon/media_type_spec.rb +267 -0
- data/xenon-http/xenon-http.gemspec +25 -0
- data/xenon-routing/lib/xenon/api.rb +118 -0
- data/xenon-routing/lib/xenon/marshallers.rb +48 -0
- data/xenon-routing/lib/xenon/request.rb +40 -0
- data/xenon-routing/lib/xenon/response.rb +29 -0
- data/xenon-routing/lib/xenon/routing.rb +6 -0
- data/xenon-routing/lib/xenon/routing/context.rb +35 -0
- data/xenon-routing/lib/xenon/routing/directives.rb +14 -0
- data/xenon-routing/lib/xenon/routing/header_directives.rb +32 -0
- data/xenon-routing/lib/xenon/routing/method_directives.rb +26 -0
- data/xenon-routing/lib/xenon/routing/param_directives.rb +22 -0
- data/xenon-routing/lib/xenon/routing/path_directives.rb +37 -0
- data/xenon-routing/lib/xenon/routing/route_directives.rb +51 -0
- data/xenon-routing/lib/xenon/routing/security_directives.rb +34 -0
- data/xenon-routing/lib/xenon/routing_version.rb +3 -0
- data/xenon-routing/spec/spec_helper.rb +94 -0
- data/xenon-routing/xenon-routing.gemspec +25 -0
- data/xenon.gemspec +26 -0
- metadata +145 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'xenon/headers/www_authenticate'
|
2
|
+
|
3
|
+
describe Xenon::Headers::WWWAuthenticate do
|
4
|
+
|
5
|
+
context '::parse' do
|
6
|
+
|
7
|
+
it 'can parse a Basic challenge with a realm' do
|
8
|
+
header = Xenon::Headers::WWWAuthenticate.parse('Basic realm="simple"')
|
9
|
+
expect(header.challenges.size).to eq(1)
|
10
|
+
expect(header.challenges[0].auth_scheme).to eq('Basic')
|
11
|
+
expect(header.challenges[0].realm).to eq('simple')
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'can parse a Digest challenge with a realm' do
|
15
|
+
header = Xenon::Headers::WWWAuthenticate.parse('Digest realm="testrealm@host.com", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"')
|
16
|
+
expect(header.challenges.size).to eq(1)
|
17
|
+
expect(header.challenges[0].auth_scheme).to eq('Digest')
|
18
|
+
expect(header.challenges[0].realm).to eq('testrealm@host.com')
|
19
|
+
expect(header.challenges[0].qop).to eq('auth,auth-int')
|
20
|
+
expect(header.challenges[0].nonce).to eq('dcd98b7102dd2f0e8b11d0f600bfb0c093')
|
21
|
+
expect(header.challenges[0].opaque).to eq('5ccc069c403ebaf9f0171e9517f40e41')
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'can parse a custom challenge' do
|
25
|
+
header = Xenon::Headers::WWWAuthenticate.parse('Newauth realm="apps", type=1, title="Login to \"apps\""')
|
26
|
+
expect(header.challenges.size).to eq(1)
|
27
|
+
expect(header.challenges[0].auth_scheme).to eq('Newauth')
|
28
|
+
expect(header.challenges[0].realm).to eq('apps')
|
29
|
+
expect(header.challenges[0].type).to eq('1')
|
30
|
+
expect(header.challenges[0].title).to eq('Login to "apps"')
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'can parse multiple challenges' do
|
34
|
+
header = Xenon::Headers::WWWAuthenticate.parse('Digest realm="testrealm@host.com", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41", Basic realm="simple", Newauth realm="apps", type=1, title="Login to \"apps\""')
|
35
|
+
expect(header.challenges.size).to eq(3)
|
36
|
+
expect(header.challenges[0].auth_scheme).to eq('Digest')
|
37
|
+
expect(header.challenges[1].auth_scheme).to eq('Basic')
|
38
|
+
expect(header.challenges[2].auth_scheme).to eq('Newauth')
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
end
|
43
|
+
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
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'xenon/http_version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'xenon-http'
|
8
|
+
spec.version = Xenon::HTTP_VERSION
|
9
|
+
spec.authors = ['Greg Beech']
|
10
|
+
spec.email = ['greg@gregbeech.com']
|
11
|
+
spec.summary = %q{An HTTP framework for building RESTful APIs.}
|
12
|
+
spec.description = %q{Provides a model for the HTTP protocol and a tree-based routing syntax.}
|
13
|
+
spec.homepage = 'https://github.com/gregbeech/xenon'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^xenon-http/bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^xenon-http/(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
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
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'rack'
|
3
|
+
require 'xenon/routing'
|
4
|
+
|
5
|
+
module Xenon
|
6
|
+
class API
|
7
|
+
include Xenon::Routing::Directives
|
8
|
+
|
9
|
+
DEFAULT_MARSHALLERS = [JsonMarshaller.new]
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def marshallers(*marshallers)
|
13
|
+
@marshallers = marshallers unless marshallers.nil? || marshallers.empty?
|
14
|
+
(@marshallers.nil? || @marshallers.empty?) ? DEFAULT_MARSHALLERS : @marshallers
|
15
|
+
end
|
16
|
+
|
17
|
+
def select_marshaller(media_ranges)
|
18
|
+
weighted = marshallers.map do |marshaller|
|
19
|
+
media_range = media_ranges.find { |media_range| marshaller.marshal_to?(media_range) }
|
20
|
+
[marshaller, media_range ? media_range.q : 0]
|
21
|
+
end
|
22
|
+
weighted.select { |_, q| q > 0 }.sort_by { |_, q| q }.map { |m, _| m }.last
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :context
|
27
|
+
|
28
|
+
class << self
|
29
|
+
def routes
|
30
|
+
@routes ||= []
|
31
|
+
end
|
32
|
+
|
33
|
+
def method_missing(name, *args, &block)
|
34
|
+
if instance_methods.include?(name)
|
35
|
+
routes << [name, args, block]
|
36
|
+
else
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def call(env)
|
43
|
+
dup.call!(env)
|
44
|
+
end
|
45
|
+
|
46
|
+
def call!(env)
|
47
|
+
@context = Routing::Context.new(Request.new(Rack::Request.new(env)), Response.new)
|
48
|
+
|
49
|
+
accept = @context.request.header('Accept')
|
50
|
+
marshaller = accept ? self.class.select_marshaller(accept.media_ranges) : self.class.marshallers.first
|
51
|
+
|
52
|
+
catch (:complete) do
|
53
|
+
begin
|
54
|
+
if marshaller
|
55
|
+
self.class.routes.each do |route|
|
56
|
+
name, args, block = route
|
57
|
+
route_block = proc { instance_eval(&block) }
|
58
|
+
send(name, *args, &route_block)
|
59
|
+
end
|
60
|
+
else
|
61
|
+
reject :accept, supported: self.class.marshallers.map(&:media_type)
|
62
|
+
end
|
63
|
+
handle_rejections(@context.rejections)
|
64
|
+
rescue => e
|
65
|
+
handle_error(e)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
marshaller ||= self.class.marshallers.first
|
70
|
+
resp = @context.response.copy(
|
71
|
+
headers: @context.response.headers.set(Headers::ContentType.new(marshaller.content_type)),
|
72
|
+
body: marshaller.marshal(@context.response.body))
|
73
|
+
[resp.status, resp.headers.map { |h| [h.name, h.to_s] }.to_h, resp.body]
|
74
|
+
end
|
75
|
+
|
76
|
+
def handle_error(e)
|
77
|
+
puts "handle_error: #{e.class}: #{e}\n #{e.backtrace.join("\n ")}"
|
78
|
+
case e
|
79
|
+
when ParseError
|
80
|
+
fail 400, e.message
|
81
|
+
else
|
82
|
+
fail 500, e.message # TODO: Only if verbose errors configured
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def handle_rejections(rejections)
|
87
|
+
puts "handle_rejections: #{rejections}"
|
88
|
+
if rejections.empty?
|
89
|
+
fail 404
|
90
|
+
else
|
91
|
+
rejection = rejections.first
|
92
|
+
case rejection.reason
|
93
|
+
when :accept
|
94
|
+
fail 406, "Supported media types: #{rejection[:supported].join(", ")}"
|
95
|
+
when :forbidden
|
96
|
+
fail 403
|
97
|
+
when :header
|
98
|
+
fail 400, "Missing required header: #{rejection[:required]}"
|
99
|
+
when :method
|
100
|
+
supported = rejections.take_while { |r| r.reason == :method }.map { |r| r[:supported].upcase }
|
101
|
+
fail 405, "Supported methods: #{supported.join(", ")}"
|
102
|
+
when :unauthorized
|
103
|
+
if rejection[:scheme]
|
104
|
+
challenge = Headers::Challenge.new(rejection[:scheme], rejection.info.except(:scheme))
|
105
|
+
respond_with_header Headers::WWWAuthenticate.new(challenge) do
|
106
|
+
fail 401
|
107
|
+
end
|
108
|
+
else
|
109
|
+
fail 401
|
110
|
+
end
|
111
|
+
else
|
112
|
+
fail 500
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|