jellyfish 0.6.0 → 0.8.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 +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
|