jellyfish 0.6.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +0 -1
- data/.travis.yml +1 -0
- data/CHANGES.md +71 -0
- data/README.md +488 -20
- data/jellyfish.gemspec +25 -10
- data/lib/jellyfish.rb +84 -40
- data/lib/jellyfish/chunked_body.rb +20 -0
- data/lib/jellyfish/multi_actions.rb +35 -0
- data/lib/jellyfish/newrelic.rb +0 -1
- data/lib/jellyfish/normalized_params.rb +55 -0
- data/lib/jellyfish/normalized_path.rb +13 -0
- data/lib/jellyfish/sinatra.rb +6 -47
- data/lib/jellyfish/test.rb +7 -2
- data/lib/jellyfish/version.rb +1 -1
- data/task/gemgem.rb +7 -6
- data/test/sinatra/test_base.rb +110 -0
- data/test/sinatra/test_chunked_body.rb +43 -0
- data/test/sinatra/test_error.rb +145 -0
- data/test/sinatra/test_multi_actions.rb +217 -0
- data/test/sinatra/test_routing.rb +425 -0
- data/test/test_from_readme.rb +39 -0
- data/test/test_inheritance.rb +88 -0
- metadata +32 -25
- data/example/config.ru +0 -118
- data/example/rainbows.rb +0 -4
- data/example/server.sh +0 -3
@@ -0,0 +1,425 @@
|
|
1
|
+
|
2
|
+
require 'jellyfish/test'
|
3
|
+
|
4
|
+
class RegexpLookAlike
|
5
|
+
class MatchData
|
6
|
+
def captures
|
7
|
+
["this", "is", "a", "test"]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def match(string)
|
12
|
+
::RegexpLookAlike::MatchData.new if string == "/this/is/a/test/"
|
13
|
+
end
|
14
|
+
|
15
|
+
def keys
|
16
|
+
["one", "two", "three", "four"]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# stolen from sinatra
|
21
|
+
describe 'Sinatra routing_test.rb' do
|
22
|
+
behaves_like :jellyfish
|
23
|
+
|
24
|
+
%w[get put post delete options patch head].each do |verb|
|
25
|
+
should "define #{verb.upcase} request handlers with #{verb}" do
|
26
|
+
app = Class.new{
|
27
|
+
include Jellyfish
|
28
|
+
send verb, '/hello' do
|
29
|
+
'Hello World'
|
30
|
+
end
|
31
|
+
}.new
|
32
|
+
|
33
|
+
status, _, body = send(verb, '/hello', app)
|
34
|
+
status.should.eq 200
|
35
|
+
body .should.eq ['Hello World']
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
should '404s when no route satisfies the request' do
|
40
|
+
app = Class.new{
|
41
|
+
include Jellyfish
|
42
|
+
get('/foo'){}
|
43
|
+
}.new
|
44
|
+
status, _, _ = get('/bar', app)
|
45
|
+
status.should.eq 404
|
46
|
+
end
|
47
|
+
|
48
|
+
should 'allows using unicode' do
|
49
|
+
app = Class.new{
|
50
|
+
include Jellyfish
|
51
|
+
controller_include Jellyfish::NormalizedPath
|
52
|
+
get("/f\u{f6}\u{f6}"){}
|
53
|
+
}.new
|
54
|
+
status, _, _ = get('/f%C3%B6%C3%B6', app)
|
55
|
+
status.should.eq 200
|
56
|
+
end
|
57
|
+
|
58
|
+
should 'handle encoded slashes correctly' do
|
59
|
+
app = Class.new{
|
60
|
+
include Jellyfish
|
61
|
+
controller_include Jellyfish::NormalizedPath
|
62
|
+
get(%r{^/(.+)}){ |m| m[1] }
|
63
|
+
}.new
|
64
|
+
status, _, body = get('/foo%2Fbar', app)
|
65
|
+
status.should.eq 200
|
66
|
+
body .should.eq ['foo/bar']
|
67
|
+
end
|
68
|
+
|
69
|
+
should 'override the content-type in error handlers' do
|
70
|
+
app = Class.new{
|
71
|
+
include Jellyfish
|
72
|
+
get{
|
73
|
+
self.headers 'Content-Type' => 'text/plain'
|
74
|
+
status, headers, body = jellyfish.app.call(env)
|
75
|
+
self.status status
|
76
|
+
self.body body
|
77
|
+
headers_merge(headers)
|
78
|
+
}
|
79
|
+
}.new(Class.new{
|
80
|
+
include Jellyfish
|
81
|
+
handle Jellyfish::NotFound do
|
82
|
+
headers_merge 'Content-Type' => 'text/html'
|
83
|
+
status 404
|
84
|
+
'<h1>Not Found</h1>'
|
85
|
+
end
|
86
|
+
}.new)
|
87
|
+
|
88
|
+
status, headers, body = get('/foo', app)
|
89
|
+
status .should.eq 404
|
90
|
+
headers['Content-Type'].should.eq 'text/html'
|
91
|
+
body .should.eq ['<h1>Not Found</h1>']
|
92
|
+
end
|
93
|
+
|
94
|
+
should 'match empty PATH_INFO to "/" if no route is defined for ""' do
|
95
|
+
app = Class.new{
|
96
|
+
include Jellyfish
|
97
|
+
controller_include Jellyfish::NormalizedPath
|
98
|
+
get('/'){ 'worked' }
|
99
|
+
}.new
|
100
|
+
|
101
|
+
status, headers, body = get('', app)
|
102
|
+
status.should.eq 200
|
103
|
+
body .should.eq ['worked']
|
104
|
+
end
|
105
|
+
|
106
|
+
should 'exposes params with indifferent hash' do
|
107
|
+
app = Class.new{
|
108
|
+
include Jellyfish
|
109
|
+
controller_include Jellyfish::NormalizedParams
|
110
|
+
|
111
|
+
get %r{^/(?<foo>\w+)} do
|
112
|
+
params['foo'].should.eq 'bar'
|
113
|
+
params[:foo ].should.eq 'bar'
|
114
|
+
'well, alright'
|
115
|
+
end
|
116
|
+
}.new
|
117
|
+
|
118
|
+
_, _, body = get('/bar', app)
|
119
|
+
body.should.eq ['well, alright']
|
120
|
+
end
|
121
|
+
|
122
|
+
should 'merges named params and query string params in params' do
|
123
|
+
app = Class.new{
|
124
|
+
include Jellyfish
|
125
|
+
controller_include Jellyfish::NormalizedParams
|
126
|
+
|
127
|
+
get %r{^/(?<foo>\w+)} do
|
128
|
+
params['foo'].should.eq 'bar'
|
129
|
+
params['baz'].should.eq 'biz'
|
130
|
+
end
|
131
|
+
}.new
|
132
|
+
|
133
|
+
status, _, _ = get('/bar', app, 'QUERY_STRING' => 'baz=biz')
|
134
|
+
status.should.eq 200
|
135
|
+
end
|
136
|
+
|
137
|
+
should 'support named captures like %r{/hello/(?<person>[^/?#]+)}' do
|
138
|
+
app = Class.new{
|
139
|
+
include Jellyfish
|
140
|
+
get Regexp.new('/hello/(?<person>[^/?#]+)') do |m|
|
141
|
+
"Hello #{m['person']}"
|
142
|
+
end
|
143
|
+
}.new
|
144
|
+
|
145
|
+
_, _, body = get('/hello/Frank', app)
|
146
|
+
body.should.eq ['Hello Frank']
|
147
|
+
end
|
148
|
+
|
149
|
+
should 'support optional named captures' do
|
150
|
+
app = Class.new{
|
151
|
+
include Jellyfish
|
152
|
+
get Regexp.new('/page(?<format>.[^/?#]+)?') do |m|
|
153
|
+
"format=#{m[:format]}"
|
154
|
+
end
|
155
|
+
}.new
|
156
|
+
|
157
|
+
status, _, body = get('/page.html', app)
|
158
|
+
status.should.eq 200
|
159
|
+
body .should.eq ['format=.html']
|
160
|
+
|
161
|
+
status, _, body = get('/page.xml', app)
|
162
|
+
status.should.eq 200
|
163
|
+
body .should.eq ['format=.xml']
|
164
|
+
|
165
|
+
status, _, body = get('/page', app)
|
166
|
+
status.should.eq 200
|
167
|
+
body .should.eq ['format=']
|
168
|
+
end
|
169
|
+
|
170
|
+
should 'not concatinate params with the same name' do
|
171
|
+
app = Class.new{
|
172
|
+
include Jellyfish
|
173
|
+
controller_include Jellyfish::NormalizedParams
|
174
|
+
|
175
|
+
get(%r{^/(?<foo>\w+)}){ |m| params[:foo] }
|
176
|
+
}.new
|
177
|
+
|
178
|
+
_, _, body = get('/a', app, 'QUERY_STRING' => 'foo=b')
|
179
|
+
body.should.eq ['a']
|
180
|
+
end
|
181
|
+
|
182
|
+
should 'support basic nested params' do
|
183
|
+
app = Class.new{
|
184
|
+
include Jellyfish
|
185
|
+
get('/hi'){ request.params['person']['name'] }
|
186
|
+
}.new
|
187
|
+
|
188
|
+
status, _, body = get('/hi', app,
|
189
|
+
'QUERY_STRING' => 'person[name]=John+Doe')
|
190
|
+
status.should.eq 200
|
191
|
+
body.should.eq ['John Doe']
|
192
|
+
end
|
193
|
+
|
194
|
+
should "expose nested params with indifferent hash" do
|
195
|
+
app = Class.new{
|
196
|
+
include Jellyfish
|
197
|
+
controller_include Jellyfish::NormalizedParams
|
198
|
+
|
199
|
+
get '/testme' do
|
200
|
+
params['bar']['foo'].should.eq 'baz'
|
201
|
+
params['bar'][:foo ].should.eq 'baz'
|
202
|
+
'well, alright'
|
203
|
+
end
|
204
|
+
}.new
|
205
|
+
|
206
|
+
_, _, body = get('/testme', app, 'QUERY_STRING' => 'bar[foo]=baz')
|
207
|
+
body.should.eq ['well, alright']
|
208
|
+
end
|
209
|
+
|
210
|
+
should 'preserve non-nested params' do
|
211
|
+
app = Class.new{
|
212
|
+
include Jellyfish
|
213
|
+
get '/foo' do
|
214
|
+
request.params['article_id'] .should.eq '2'
|
215
|
+
request.params['comment']['body'].should.eq 'awesome'
|
216
|
+
request.params['comment[body]'] .should.eq nil
|
217
|
+
'looks good'
|
218
|
+
end
|
219
|
+
}.new
|
220
|
+
|
221
|
+
status, _, body = get('/foo', app,
|
222
|
+
'QUERY_STRING' => 'article_id=2&comment[body]=awesome')
|
223
|
+
status.should.eq 200
|
224
|
+
body .should.eq ['looks good']
|
225
|
+
end
|
226
|
+
|
227
|
+
should 'match paths that include spaces encoded with %20' do
|
228
|
+
app = Class.new{
|
229
|
+
include Jellyfish
|
230
|
+
controller_include Jellyfish::NormalizedPath
|
231
|
+
get('/path with spaces'){ 'looks good' }
|
232
|
+
}.new
|
233
|
+
|
234
|
+
status, _, body = get('/path%20with%20spaces', app)
|
235
|
+
status.should.eq 200
|
236
|
+
body .should.eq ['looks good']
|
237
|
+
end
|
238
|
+
|
239
|
+
should 'match paths that include spaces encoded with +' do
|
240
|
+
app = Class.new{
|
241
|
+
include Jellyfish
|
242
|
+
controller_include Jellyfish::NormalizedPath
|
243
|
+
get('/path with spaces'){ 'looks good' }
|
244
|
+
}.new
|
245
|
+
|
246
|
+
status, _, body = get('/path+with+spaces', app)
|
247
|
+
status.should.eq 200
|
248
|
+
body .should.eq ['looks good']
|
249
|
+
end
|
250
|
+
|
251
|
+
should 'make regular expression captures available' do
|
252
|
+
app = Class.new{
|
253
|
+
include Jellyfish
|
254
|
+
get(/^\/fo(.*)\/ba(.*)/) do |m|
|
255
|
+
m[1..-1].should.eq ['orooomma', 'f']
|
256
|
+
'right on'
|
257
|
+
end
|
258
|
+
}.new
|
259
|
+
|
260
|
+
status, _, body = get('/foorooomma/baf', app)
|
261
|
+
status.should.eq 200
|
262
|
+
body .should.eq ['right on']
|
263
|
+
end
|
264
|
+
|
265
|
+
it 'supports regular expression look-alike routes' do
|
266
|
+
app = Class.new{
|
267
|
+
include Jellyfish
|
268
|
+
controller_include Jellyfish::NormalizedParams
|
269
|
+
matcher = Object.new
|
270
|
+
def matcher.match path
|
271
|
+
%r{/(?<one>\w+)/(?<two>\w+)/(?<three>\w+)/(?<four>\w+)}.match(path)
|
272
|
+
end
|
273
|
+
|
274
|
+
get(matcher) do |m|
|
275
|
+
[m, params].each do |q|
|
276
|
+
q[:one] .should.eq 'this'
|
277
|
+
q[:two] .should.eq 'is'
|
278
|
+
q[:three].should.eq 'a'
|
279
|
+
q[:four] .should.eq 'test'
|
280
|
+
end
|
281
|
+
'right on'
|
282
|
+
end
|
283
|
+
}.new
|
284
|
+
|
285
|
+
status, _, body = get('/this/is/a/test/', app)
|
286
|
+
status.should.eq 200
|
287
|
+
body .should.eq ['right on']
|
288
|
+
end
|
289
|
+
|
290
|
+
should 'raise a TypeError when pattern is not a String or Regexp' do
|
291
|
+
lambda{ Class.new{ include Jellyfish; get(42){} } }.
|
292
|
+
should.raise(TypeError)
|
293
|
+
end
|
294
|
+
|
295
|
+
should 'return response immediately on next or halt' do
|
296
|
+
app = Class.new{
|
297
|
+
include Jellyfish
|
298
|
+
controller_include Jellyfish::MultiActions
|
299
|
+
|
300
|
+
get '/next' do
|
301
|
+
body 'Hello World'
|
302
|
+
next
|
303
|
+
'Boo-hoo World'
|
304
|
+
end
|
305
|
+
|
306
|
+
get '/halt' do
|
307
|
+
body 'Hello World'
|
308
|
+
halt
|
309
|
+
'Boo-hoo World'
|
310
|
+
end
|
311
|
+
}.new
|
312
|
+
|
313
|
+
%w[/next /halt].each do |path|
|
314
|
+
status, _, body = get(path, app)
|
315
|
+
status.should.eq 200
|
316
|
+
body .should.eq ['Hello World']
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
should 'halt with a response tuple' do
|
321
|
+
app = Class.new{
|
322
|
+
include Jellyfish
|
323
|
+
controller_include Jellyfish::MultiActions
|
324
|
+
|
325
|
+
get '/' do
|
326
|
+
halt [295, {'Content-Type' => 'text/plain'}, ['Hello World']]
|
327
|
+
end
|
328
|
+
}.new
|
329
|
+
|
330
|
+
status, headers, body = get('/', app)
|
331
|
+
status .should.eq 295
|
332
|
+
headers['Content-Type'].should.eq 'text/plain'
|
333
|
+
body .should.eq ['Hello World']
|
334
|
+
end
|
335
|
+
|
336
|
+
should 'transition to the next matching route on next' do
|
337
|
+
app = Class.new{
|
338
|
+
include Jellyfish
|
339
|
+
controller_include Jellyfish::MultiActions, Jellyfish::NormalizedParams
|
340
|
+
get %r{^/(?<foo>\w+)} do
|
341
|
+
params['foo'].should.eq 'bar'
|
342
|
+
next
|
343
|
+
'Hello Foo'
|
344
|
+
end
|
345
|
+
|
346
|
+
get do
|
347
|
+
params.should.not.include?('foo')
|
348
|
+
'Hello World'
|
349
|
+
end
|
350
|
+
}.new
|
351
|
+
|
352
|
+
status, _, body = get('/bar', app)
|
353
|
+
status.should.eq 200
|
354
|
+
body .should.eq ['Hello World']
|
355
|
+
end
|
356
|
+
|
357
|
+
should 'match routes defined in superclasses' do
|
358
|
+
sup = Class.new{
|
359
|
+
include Jellyfish
|
360
|
+
get('/foo'){ 'foo' }
|
361
|
+
}
|
362
|
+
app = Class.new(sup){
|
363
|
+
get('/bar'){ 'bar' }
|
364
|
+
}.new
|
365
|
+
|
366
|
+
%w[foo bar].each do |path|
|
367
|
+
status, _, body = get("/#{path}", app)
|
368
|
+
status.should.eq 200
|
369
|
+
body .should.eq [path]
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
should 'match routes itself first then downward app' do
|
374
|
+
sup = Class.new{
|
375
|
+
include Jellyfish
|
376
|
+
get('/foo'){ 'foo sup' }
|
377
|
+
get('/bar'){ 'bar sup' }
|
378
|
+
}
|
379
|
+
app = Class.new{
|
380
|
+
include Jellyfish
|
381
|
+
get('/foo'){ 'foo sub' }
|
382
|
+
}.new(sup.new)
|
383
|
+
|
384
|
+
status, _, body = get('/foo', app)
|
385
|
+
status.should.eq 200
|
386
|
+
body .should.eq ['foo sub']
|
387
|
+
|
388
|
+
status, _, body = get('/bar', app)
|
389
|
+
status.should.eq 200
|
390
|
+
body .should.eq ['bar sup']
|
391
|
+
end
|
392
|
+
|
393
|
+
should 'allow using call to fire another request internally' do
|
394
|
+
app = Class.new{
|
395
|
+
include Jellyfish
|
396
|
+
get '/foo' do
|
397
|
+
status, headers, body = call(env.merge('PATH_INFO' => '/bar'))
|
398
|
+
self.status status
|
399
|
+
self.headers headers
|
400
|
+
self.body body.map(&:upcase)
|
401
|
+
end
|
402
|
+
|
403
|
+
get '/bar' do
|
404
|
+
'bar'
|
405
|
+
end
|
406
|
+
}.new
|
407
|
+
|
408
|
+
status, _, body = get('/foo', app)
|
409
|
+
status.should.eq 200
|
410
|
+
body .should.eq ['BAR']
|
411
|
+
end
|
412
|
+
|
413
|
+
should 'play well with other routing middleware' do
|
414
|
+
middleware = Class.new{include Jellyfish}
|
415
|
+
inner_app = Class.new{include Jellyfish; get('/foo'){ 'hello' } }
|
416
|
+
builder = Rack::Builder.new do
|
417
|
+
use middleware
|
418
|
+
map('/test'){ run inner_app.new }
|
419
|
+
end
|
420
|
+
|
421
|
+
status, _, body = get('/test/foo', builder.to_app)
|
422
|
+
status.should.eq 200
|
423
|
+
body .should.eq ['hello']
|
424
|
+
end
|
425
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
require 'jellyfish/test'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
describe 'from README.md' do
|
6
|
+
after do
|
7
|
+
[:Tank, :Heater, :Protector].each do |const|
|
8
|
+
Object.send(:remove_const, const) if Object.const_defined?(const)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
readme = File.read(
|
13
|
+
"#{File.dirname(File.expand_path(__FILE__))}/../README.md")
|
14
|
+
codes = readme.scan(
|
15
|
+
/### ([^\n]+).+?``` ruby\n(.+?)\n```\n\n<!---(.+?)-->/m)
|
16
|
+
|
17
|
+
codes.each.with_index do |(title, code, test), index|
|
18
|
+
if title =~ /NewRelic/i
|
19
|
+
warn "Skip NewRelic Test" unless Bacon.kind_of?(Bacon::TestUnitOutput)
|
20
|
+
next
|
21
|
+
end
|
22
|
+
|
23
|
+
should "pass from README.md #%02d #{title}" % index do
|
24
|
+
method_path, expect = test.strip.split("\n", 2)
|
25
|
+
method, path = method_path.split(' ')
|
26
|
+
uri = URI.parse(path)
|
27
|
+
pinfo, query = uri.path, uri.query
|
28
|
+
|
29
|
+
status, headers, body = File.open(File::NULL) do |input|
|
30
|
+
Rack::Builder.new{ eval(code) }.call(
|
31
|
+
'REQUEST_METHOD' => method, 'PATH_INFO' => pinfo,
|
32
|
+
'QUERY_STRING' => query , 'rack.input' => input)
|
33
|
+
end
|
34
|
+
|
35
|
+
body.extend(Enumerable)
|
36
|
+
[status, headers, body.to_a].should.eq eval(expect, binding, __FILE__)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|