regal 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ddd9f8426b202a6cd710732b5d90121c1be6de3d
4
+ data.tar.gz: 923b6298aeaa7086e43e3856e4a3a1cfc840037b
5
+ SHA512:
6
+ metadata.gz: 875dc13b486d7d0629ecce89564c3f26b2084a0c72fb6a98cca6ef3316527be8482d9dbfbe7b3216326afdf85e930bc7ab33bf5a672ddeb24a4077ce20efda01
7
+ data.tar.gz: 47ef101e32275bacc1019a74d7a54ab040b020fc31931b92c5fd8ddee4dd9108524efa74003a167655b04a39d6e12f9ad5e06458c92ff9066da33c8cfa297f99
data/.yardopts ADDED
@@ -0,0 +1,4 @@
1
+ --no-private
2
+ --markup markdown
3
+ lib/**/*.rb
4
+ -- README
data/lib/regal.rb ADDED
@@ -0,0 +1,6 @@
1
+ module Regal
2
+ end
3
+
4
+ require 'regal/app'
5
+ require 'regal/request'
6
+ require 'regal/response'
data/lib/regal/app.rb ADDED
@@ -0,0 +1,236 @@
1
+ require 'rack'
2
+
3
+ module Regal
4
+ module App
5
+ def self.create(*args, &block)
6
+ Class.new(Route).create(nil, &block)
7
+ end
8
+
9
+ def self.new(*args, &block)
10
+ create(&block).new(*args)
11
+ end
12
+ end
13
+
14
+ module RouterDsl
15
+ attr_reader :name
16
+
17
+ def create(name=nil, &block)
18
+ @mounted_apps = []
19
+ @static_routes = {}
20
+ @dynamic_route = nil
21
+ @handlers = {}
22
+ @befores = []
23
+ @afters = []
24
+ @setups = []
25
+ @middlewares = []
26
+ @rescuers = []
27
+ @name = name
28
+ class_exec(&block)
29
+ self
30
+ end
31
+
32
+ def setups
33
+ if superclass.respond_to?(:setups) && (setups = superclass.setups)
34
+ setups + @setups
35
+ else
36
+ @setups && @setups.dup
37
+ end
38
+ end
39
+
40
+ def befores
41
+ if superclass.respond_to?(:befores) && (befores = superclass.befores)
42
+ befores + @befores
43
+ else
44
+ @befores && @befores.dup
45
+ end
46
+ end
47
+
48
+ def afters
49
+ if superclass.respond_to?(:afters) && (afters = superclass.afters)
50
+ afters + @afters
51
+ else
52
+ @afters && @afters.dup
53
+ end
54
+ end
55
+
56
+ def middlewares
57
+ if superclass.respond_to?(:middlewares) && (middlewares = superclass.middlewares)
58
+ middlewares + @middlewares
59
+ else
60
+ @middlewares && @middlewares.dup
61
+ end
62
+ end
63
+
64
+ def rescuers
65
+ if superclass.respond_to?(:rescuers) && (rescuers = superclass.rescuers)
66
+ rescuers + @rescuers
67
+ else
68
+ @rescuers && @rescuers.dup
69
+ end
70
+ end
71
+
72
+ def create_routes(args)
73
+ routes = {}
74
+ if @dynamic_route
75
+ routes.default = @dynamic_route.new(*args)
76
+ end
77
+ @mounted_apps.each do |app|
78
+ routes.merge!(app.create_routes(args))
79
+ end
80
+ @static_routes.each do |path, cls|
81
+ routes[path] = cls.new(*args)
82
+ end
83
+ routes
84
+ end
85
+
86
+ def handlers
87
+ @handlers.dup
88
+ end
89
+
90
+ def route(s, &block)
91
+ r = Class.new(self).create(s, &block)
92
+ if s.is_a?(Symbol)
93
+ @dynamic_route = r
94
+ else
95
+ @static_routes[s] = r
96
+ end
97
+ end
98
+
99
+ def mount(app)
100
+ @mounted_apps << app
101
+ end
102
+
103
+ def use(middleware, *args, &block)
104
+ @middlewares << [middleware, args, block]
105
+ end
106
+
107
+ def setup(&block)
108
+ @setups << block
109
+ end
110
+
111
+ def before(&block)
112
+ @befores << block
113
+ end
114
+
115
+ def after(&block)
116
+ @afters << block
117
+ end
118
+
119
+ def rescue_from(type, &block)
120
+ @rescuers << [type, block]
121
+ end
122
+
123
+ [:get, :head, :options, :delete, :post, :put, :patch].each do |name|
124
+ upcased_name = name.to_s.upcase
125
+ define_method(name) do |&block|
126
+ @handlers[upcased_name] = block
127
+ end
128
+ end
129
+
130
+ def any(&block)
131
+ @handlers.default = block
132
+ end
133
+ end
134
+
135
+ class Route
136
+ extend RouterDsl
137
+
138
+ SLASH = '/'.freeze
139
+ PATH_CAPTURES_KEY = 'regal.path_captures'.freeze
140
+ PATH_COMPONENTS_KEY = 'regal.path_components'.freeze
141
+ PATH_INFO_KEY = 'PATH_INFO'.freeze
142
+ REQUEST_METHOD_KEY = 'REQUEST_METHOD'.freeze
143
+ METHOD_NOT_ALLOWED_RESPONSE = [405, {}.freeze, [].freeze].freeze
144
+ NOT_FOUND_RESPONSE = [404, {}.freeze, [].freeze].freeze
145
+ EMPTY_BODY = ''.freeze
146
+
147
+ attr_reader :name
148
+
149
+ def initialize(*args)
150
+ @actual = self.dup
151
+ self.class.setups.each do |setup|
152
+ @actual.instance_exec(*args, &setup)
153
+ end
154
+ @befores = self.class.befores
155
+ @afters = self.class.afters.reverse
156
+ @rescuers = self.class.rescuers
157
+ @routes = self.class.create_routes(args)
158
+ @handlers = self.class.handlers
159
+ @name = self.class.name
160
+ if !self.class.middlewares.empty?
161
+ @app = self.class.middlewares.reduce(method(:handle)) do |app, (middleware, args, block)|
162
+ middleware.new(app, *args, &block)
163
+ end
164
+ end
165
+ freeze
166
+ end
167
+
168
+ def call(env)
169
+ path_components = env[PATH_COMPONENTS_KEY] ||= env[PATH_INFO_KEY].split(SLASH).drop(1)
170
+ path_component = path_components.shift
171
+ if path_component && (app = @routes[path_component])
172
+ dynamic_route = !@routes.key?(path_component)
173
+ if dynamic_route
174
+ env[PATH_CAPTURES_KEY] ||= {}
175
+ env[PATH_CAPTURES_KEY][app.name] = path_component
176
+ end
177
+ app.call(env)
178
+ elsif path_component.nil?
179
+ if @app
180
+ @app.call(env)
181
+ else
182
+ handle(env)
183
+ end
184
+ else
185
+ NOT_FOUND_RESPONSE
186
+ end
187
+ end
188
+
189
+ private
190
+
191
+ def handle(env)
192
+ if (handler = @handlers[env[REQUEST_METHOD_KEY]])
193
+ request = Request.new(env)
194
+ response = Response.new
195
+ begin
196
+ @befores.each do |before|
197
+ break if response.finished?
198
+ @actual.instance_exec(request, response, &before)
199
+ end
200
+ unless response.finished?
201
+ result = @actual.instance_exec(request, response, &handler)
202
+ if request.head? || response.status < 200 || response.status == 204 || response.status == 205 || response.status == 304
203
+ response.no_body
204
+ elsif !response.finished?
205
+ response.body = result
206
+ end
207
+ end
208
+ rescue => e
209
+ handle_error(e, request, response)
210
+ end
211
+ @afters.each do |after|
212
+ begin
213
+ @actual.instance_exec(request, response, &after)
214
+ rescue => e
215
+ handle_error(e, request, response)
216
+ end
217
+ end
218
+ response
219
+ else
220
+ METHOD_NOT_ALLOWED_RESPONSE
221
+ end
222
+ end
223
+
224
+ def handle_error(e, request, response)
225
+ handled = false
226
+ @rescuers.each do |type, handler|
227
+ if type === e
228
+ handler.call(e, request, response)
229
+ handled = true
230
+ break
231
+ end
232
+ end
233
+ raise unless handled
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,61 @@
1
+ module Regal
2
+ class Request
3
+ attr_reader :env, :attributes
4
+
5
+ def initialize(env)
6
+ @env = env
7
+ @attributes = {}
8
+ end
9
+
10
+ def request_method
11
+ @env[REQUEST_METHOD_KEY]
12
+ end
13
+
14
+ def head?
15
+ request_method == HEAD_METHOD
16
+ end
17
+
18
+ def parameters
19
+ @parameters ||= begin
20
+ path_captures = @env[Route::PATH_CAPTURES_KEY]
21
+ query = Rack::Utils.parse_query(@env[QUERY_STRING_KEY])
22
+ query.merge!(path_captures) if path_captures
23
+ query.freeze
24
+ end
25
+ end
26
+
27
+ def headers
28
+ @headers ||= begin
29
+ headers = @env.each_with_object({}) do |(key, value), headers|
30
+ if key.start_with?(HEADER_PREFIX)
31
+ normalized_key = key[HEADER_PREFIX.length, key.length - HEADER_PREFIX.length]
32
+ normalized_key.gsub!(/(?<=^.|_.)[^_]+/) { |str| str.downcase }
33
+ normalized_key.gsub!('_', '-')
34
+ elsif key == CONTENT_LENGTH_KEY
35
+ normalized_key = CONTENT_LENGTH_HEADER
36
+ elsif key == CONTENT_TYPE_KEY
37
+ normalized_key = CONTENT_TYPE_HEADER
38
+ end
39
+ if normalized_key
40
+ headers[normalized_key] = value
41
+ end
42
+ end
43
+ headers.freeze
44
+ end
45
+ end
46
+
47
+ def body
48
+ @env[RACK_INPUT_KEY]
49
+ end
50
+
51
+ HEADER_PREFIX = 'HTTP_'.freeze
52
+ QUERY_STRING_KEY = 'QUERY_STRING'.freeze
53
+ CONTENT_LENGTH_KEY = 'CONTENT_LENGTH'.freeze
54
+ CONTENT_LENGTH_HEADER = 'Content-Length'.freeze
55
+ CONTENT_TYPE_KEY = 'CONTENT_TYPE'.freeze
56
+ CONTENT_TYPE_HEADER = 'Content-Type'.freeze
57
+ RACK_INPUT_KEY = 'rack.input'.freeze
58
+ REQUEST_METHOD_KEY = 'REQUEST_METHOD'.freeze
59
+ HEAD_METHOD = 'HEAD'.freeze
60
+ end
61
+ end
@@ -0,0 +1,61 @@
1
+ module Regal
2
+ class Response
3
+ attr_accessor :status, :body, :raw_body
4
+ attr_reader :headers
5
+
6
+ EMPTY_BODY = [].freeze
7
+
8
+ def initialize
9
+ @status = 200
10
+ @headers = {}
11
+ @body = nil
12
+ @raw_body = nil
13
+ @finished = false
14
+ end
15
+
16
+ def finish
17
+ @finished = true
18
+ end
19
+
20
+ def finished?
21
+ @finished
22
+ end
23
+
24
+ def no_body
25
+ @raw_body = EMPTY_BODY
26
+ end
27
+
28
+ def [](n)
29
+ case n
30
+ when 0 then @status
31
+ when 1 then @headers
32
+ when 2 then rack_body
33
+ end
34
+ end
35
+
36
+ def []=(n, v)
37
+ case n
38
+ when 0 then @status = v
39
+ when 1 then @headers = v
40
+ when 2 then @raw_body = v
41
+ end
42
+ end
43
+
44
+ def to_ary
45
+ [@status, @headers, rack_body]
46
+ end
47
+ alias_method :to_a, :to_ary
48
+
49
+ private
50
+
51
+ def rack_body
52
+ if @raw_body
53
+ @raw_body
54
+ elsif @body.is_a?(String)
55
+ [@body]
56
+ else
57
+ @body
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,3 @@
1
+ module Regal
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,924 @@
1
+ require 'spec_helper'
2
+ require 'json'
3
+
4
+ module Regal
5
+ describe App do
6
+ include Rack::Test::Methods
7
+
8
+ context 'a basic app' do
9
+ let :app do
10
+ App.new do
11
+ get do
12
+ 'root'
13
+ end
14
+
15
+ route 'hello' do
16
+ get do
17
+ 'hello'
18
+ end
19
+
20
+ route 'world' do
21
+ get do
22
+ 'hello world'
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ it 'routes a request' do
30
+ get '/hello'
31
+ expect(last_response.status).to eq(200)
32
+ expect(last_response.body).to eq('hello')
33
+ end
34
+
35
+ it 'routes a request to the root' do
36
+ get '/'
37
+ expect(last_response.status).to eq(200)
38
+ expect(last_response.body).to eq('root')
39
+ end
40
+
41
+ it 'routes a request with more than one path component' do
42
+ get '/hello/world'
43
+ expect(last_response.status).to eq(200)
44
+ expect(last_response.body).to eq('hello world')
45
+ end
46
+
47
+ it 'responds with 404 when the path does not match any route' do
48
+ get '/hello/fnord'
49
+ expect(last_response.status).to eq(404)
50
+ end
51
+
52
+ it 'responds with 405 when the path matches a route but there is no handler for the HTTP method' do
53
+ delete '/hello/world'
54
+ expect(last_response.status).to eq(405)
55
+ end
56
+ end
57
+
58
+ context 'a simple interactive app' do
59
+ let :app do
60
+ App.new do
61
+ route 'echo' do
62
+ get do |request|
63
+ request.parameters['s']
64
+ end
65
+
66
+ post do |request|
67
+ request.body.read
68
+ end
69
+ end
70
+
71
+ route 'international-hello' do
72
+ get do |request|
73
+ case request.headers['Accept-Language']
74
+ when 'sv_SE'
75
+ 'hej'
76
+ when 'fr_FR'
77
+ 'bonjour'
78
+ else
79
+ '?'
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ it 'can access the query parameters' do
87
+ get '/echo?s=hallo'
88
+ expect(last_response.status).to eq(200)
89
+ expect(last_response.body).to eq('hallo')
90
+ end
91
+
92
+ it 'can access the request headers' do
93
+ get '/international-hello', nil, {'HTTP_ACCEPT_LANGUAGE' => 'sv_SE'}
94
+ expect(last_response.status).to eq(200)
95
+ expect(last_response.body).to eq('hej')
96
+ get '/international-hello', nil, {'HTTP_ACCEPT_LANGUAGE' => 'fr_FR'}
97
+ expect(last_response.status).to eq(200)
98
+ expect(last_response.body).to eq('bonjour')
99
+ end
100
+
101
+ it 'can access the request body' do
102
+ post '/echo', 'blobblobblob'
103
+ expect(last_response.status).to eq(200)
104
+ expect(last_response.body).to eq('blobblobblob')
105
+ end
106
+ end
107
+
108
+ context 'an app that does more than just respond with a body' do
109
+ let :app do
110
+ App.new do
111
+ route 'redirect' do
112
+ get do |_, response|
113
+ response.status = 307
114
+ response.headers['Location'] = 'somewhere/else'
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ it 'can change the response code' do
121
+ get '/redirect'
122
+ expect(last_response.status).to eq(307)
123
+ end
124
+
125
+ it 'can set response headers' do
126
+ get '/redirect'
127
+ expect(last_response.headers).to include('Location' => 'somewhere/else')
128
+ end
129
+ end
130
+
131
+ context 'an app doing work before route handlers' do
132
+ let :app do
133
+ App.new do
134
+ before do |request|
135
+ request.attributes[:some_key] = [1]
136
+ end
137
+
138
+ get do |request|
139
+ request.attributes[:some_key].join(',')
140
+ end
141
+
142
+ route 'one-before' do
143
+ before do |request|
144
+ request.attributes[:some_key] << 2
145
+ end
146
+
147
+ get do |request|
148
+ request.attributes[:some_key].join(',')
149
+ end
150
+ end
151
+
152
+ route 'two-before' do
153
+ before do |request|
154
+ request.attributes[:some_key] << 2
155
+ end
156
+
157
+ before do |request|
158
+ request.attributes[:some_key] << 3
159
+ end
160
+
161
+ get do |request|
162
+ request.attributes[:some_key].join(',')
163
+ end
164
+
165
+ route 'another-before' do
166
+ before do |request|
167
+ request.attributes[:some_key] << 4
168
+ end
169
+
170
+ get do |request|
171
+ request.attributes[:some_key].join(',')
172
+ end
173
+ end
174
+ end
175
+
176
+ route 'redirect-before' do
177
+ before do |_, response|
178
+ response.headers['Location'] = 'somewhere/else'
179
+ response.status = 307
180
+ response.body = 'Go somewhere else'
181
+ response.finish
182
+ end
183
+
184
+ before do |_, response|
185
+ response.body = 'whoopiedoo'
186
+ end
187
+
188
+ get do
189
+ "I'm not called!"
190
+ end
191
+
192
+ after do |_, response|
193
+ response.headers['WasAfterCalled'] = 'yes'
194
+ end
195
+ end
196
+ end
197
+ end
198
+
199
+ it 'calls the before block before the request handler' do
200
+ get '/'
201
+ expect(last_response.body).to eq('1')
202
+ end
203
+
204
+ it 'calls the before blocks of all routes before the request handler' do
205
+ get '/one-before'
206
+ expect(last_response.body).to eq('1,2')
207
+ end
208
+
209
+ it 'calls the before blocks of a route in order' do
210
+ get '/two-before'
211
+ expect(last_response.body).to eq('1,2,3')
212
+ end
213
+
214
+ it 'calls all before blocks of a route before the request handler' do
215
+ get '/two-before/another-before'
216
+ expect(last_response.body).to eq('1,2,3,4')
217
+ end
218
+
219
+ it 'gives the before blocks access to the response' do
220
+ get '/redirect-before'
221
+ expect(last_response.status).to eq(307)
222
+ end
223
+
224
+ context 'when the response is marked as finished' do
225
+ before do
226
+ get '/redirect-before'
227
+ end
228
+
229
+ it 'does not call further handlers or before blocks when the response is marked as finished' do
230
+ expect(last_response.body).to eq('Go somewhere else')
231
+ end
232
+
233
+ it 'calls after blocks' do
234
+ expect(last_response.headers).to include('WasAfterCalled' => 'yes')
235
+ end
236
+ end
237
+ end
238
+
239
+ context 'an app doing work after route handlers' do
240
+ let :app do
241
+ App.new do
242
+ after do |_, response|
243
+ response.headers['Content-Type'] = 'application/json'
244
+ response.body = JSON.dump(response.body)
245
+ end
246
+
247
+ get do |request|
248
+ {'root' => true}
249
+ end
250
+
251
+ route 'one-after' do
252
+ after do |_, response|
253
+ response.body['list'] << 1
254
+ end
255
+
256
+ get do |request|
257
+ {'list' => []}
258
+ end
259
+ end
260
+
261
+ route 'two-after' do
262
+ after do |request, response|
263
+ response.body['list'] << 1
264
+ end
265
+
266
+ after do |request, response|
267
+ response.body['list'] << 2
268
+ end
269
+
270
+ get do |request|
271
+ {'list' => []}
272
+ end
273
+
274
+ route 'another-after' do
275
+ after do |request, response|
276
+ response.body['list'] << 3
277
+ end
278
+
279
+ get do |request|
280
+ {'list' => []}
281
+ end
282
+ end
283
+ end
284
+ end
285
+ end
286
+
287
+ it 'calls the after block after the request handler' do
288
+ get '/'
289
+ expect(last_response.body).to eq('{"root":true}')
290
+ end
291
+
292
+ it 'calls the after blocks of all routes after the request handler' do
293
+ get '/one-after'
294
+ expect(last_response.body).to eq('{"list":[1]}')
295
+ end
296
+
297
+ it 'calls all after blocks of a route in order' do
298
+ get '/two-after'
299
+ expect(last_response.body).to eq('{"list":[2,1]}')
300
+ end
301
+
302
+ it 'calls all after blocks of a route after the request handler' do
303
+ get '/two-after/another-after'
304
+ expect(last_response.body).to eq('{"list":[3,2,1]}')
305
+ end
306
+ end
307
+
308
+ context 'an app that has capturing routes' do
309
+ let :app do
310
+ App.new do
311
+ route 'foo' do
312
+ route :bar do
313
+ get do
314
+ 'whatever'
315
+ end
316
+
317
+ route 'echo' do
318
+ get do |request|
319
+ request.parameters[:bar]
320
+ end
321
+ end
322
+ end
323
+
324
+ route 'bar' do
325
+ get do
326
+ 'bar'
327
+ end
328
+ end
329
+ end
330
+ end
331
+ end
332
+
333
+ it 'matches anything for the capture route' do
334
+ get '/foo/something'
335
+ expect(last_response.status).to eq(200)
336
+ expect(last_response.body).to eq('whatever')
337
+ get '/foo/something-else'
338
+ expect(last_response.status).to eq(200)
339
+ expect(last_response.body).to eq('whatever')
340
+ end
341
+
342
+ it 'picks static routes first' do
343
+ get '/foo/bar'
344
+ expect(last_response.status).to eq(200)
345
+ expect(last_response.body).to eq('bar')
346
+ end
347
+
348
+ it 'captures the path component as a parameter using a symbol as key' do
349
+ get '/foo/zzz/echo'
350
+ expect(last_response.status).to eq(200)
351
+ expect(last_response.body).to eq('zzz')
352
+ get '/foo/q/echo'
353
+ expect(last_response.status).to eq(200)
354
+ expect(last_response.body).to eq('q')
355
+ end
356
+ end
357
+
358
+ context 'an app that mounts another app' do
359
+ GoodbyeApp = App.create do
360
+ route 'goodbye' do
361
+ get do
362
+ 'goodbye'
363
+ end
364
+ end
365
+ end
366
+
367
+ HelloApp = App.create do
368
+ route 'hello' do
369
+ get do
370
+ 'hello'
371
+ end
372
+
373
+ route 'you' do
374
+ route 'say' do
375
+ mount GoodbyeApp
376
+ end
377
+ end
378
+ end
379
+ end
380
+
381
+ let :app do
382
+ App.new do
383
+ route 'i' do
384
+ route 'say' do
385
+ mount HelloApp
386
+ mount GoodbyeApp
387
+ end
388
+ end
389
+
390
+ route 'oh' do
391
+ mount HelloApp
392
+ end
393
+ end
394
+ end
395
+
396
+ it 'routes a request into the other app' do
397
+ get '/i/say/hello'
398
+ expect(last_response.status).to eq(200)
399
+ expect(last_response.body).to eq('hello')
400
+ end
401
+
402
+ it 'can mount multiple apps' do
403
+ get '/i/say/goodbye'
404
+ expect(last_response.status).to eq(200)
405
+ expect(last_response.body).to eq('goodbye')
406
+ end
407
+
408
+ it 'routes a request into apps that mount yet more apps' do
409
+ get '/i/say/hello/you/say/goodbye'
410
+ expect(last_response.status).to eq(200)
411
+ expect(last_response.body).to eq('goodbye')
412
+ end
413
+
414
+ it 'can mount the same app multiple times' do
415
+ get '/oh/hello'
416
+ expect(last_response.status).to eq(200)
417
+ expect(last_response.body).to eq('hello')
418
+ end
419
+ end
420
+
421
+ context 'an app that supports all HTTP methods' do
422
+ let :app do
423
+ App.new do
424
+ get do |request|
425
+ request.request_method
426
+ end
427
+
428
+ head do |request|
429
+ request.request_method
430
+ end
431
+
432
+ options do |request|
433
+ request.request_method
434
+ end
435
+
436
+ delete do |request|
437
+ request.request_method
438
+ end
439
+
440
+ post do |request|
441
+ request.request_method
442
+ end
443
+
444
+ put do |request|
445
+ request.request_method
446
+ end
447
+
448
+ patch do |request|
449
+ request.request_method
450
+ end
451
+
452
+ route 'anything' do
453
+ any do |request|
454
+ request.request_method
455
+ end
456
+ end
457
+ end
458
+ end
459
+
460
+ it 'routes GET requests' do
461
+ get '/'
462
+ expect(last_response.status).to eq(200)
463
+ expect(last_response.body).to eq('GET')
464
+ end
465
+
466
+ it 'routes HEAD requests, but does not respond with any body' do
467
+ head '/'
468
+ expect(last_response.status).to eq(200)
469
+ expect(last_response.body).to be_empty
470
+ end
471
+
472
+ it 'routes OPTIONS requests' do
473
+ options '/'
474
+ expect(last_response.status).to eq(200)
475
+ expect(last_response.body).to eq('OPTIONS')
476
+ end
477
+
478
+ it 'routes DELETE requests' do
479
+ delete '/'
480
+ expect(last_response.status).to eq(200)
481
+ expect(last_response.body).to eq('DELETE')
482
+ end
483
+
484
+ it 'routes POST requests' do
485
+ post '/'
486
+ expect(last_response.status).to eq(200)
487
+ expect(last_response.body).to eq('POST')
488
+ end
489
+
490
+ it 'routes PUT requests' do
491
+ put '/'
492
+ expect(last_response.status).to eq(200)
493
+ expect(last_response.body).to eq('PUT')
494
+ end
495
+
496
+ it 'routes PATCH requests' do
497
+ patch '/'
498
+ expect(last_response.status).to eq(200)
499
+ expect(last_response.body).to eq('PATCH')
500
+ end
501
+
502
+ it 'routes all HTTP requests when there is an any handler' do
503
+ get '/anything'
504
+ expect(last_response.status).to eq(200)
505
+ expect(last_response.body).to eq('GET')
506
+ delete '/anything'
507
+ expect(last_response.status).to eq(200)
508
+ expect(last_response.body).to eq('DELETE')
509
+ head '/anything'
510
+ expect(last_response.status).to eq(200)
511
+ expect(last_response.body).to be_empty
512
+ put '/anything'
513
+ expect(last_response.status).to eq(200)
514
+ expect(last_response.body).to eq('PUT')
515
+ end
516
+ end
517
+
518
+ context 'an app with helper methods' do
519
+ let :app do
520
+ App.new do
521
+ def top_level_helper
522
+ 'top_level_helper'
523
+ end
524
+
525
+ route 'one' do
526
+ def first_level_helper
527
+ 'first_level_helper'
528
+ end
529
+
530
+ get do
531
+ first_level_helper
532
+ end
533
+
534
+ route 'two' do
535
+ def second_level_helper
536
+ 'second_level_helper'
537
+ end
538
+
539
+ before do |_, response|
540
+ response.body = 'before:' << [top_level_helper, first_level_helper, second_level_helper].join(',')
541
+ end
542
+
543
+ after do |_, response|
544
+ response.body += '|after:' << [top_level_helper, first_level_helper, second_level_helper].join(',')
545
+ end
546
+
547
+ get do |_, response|
548
+ response.body + '|handler:' << [top_level_helper, first_level_helper, second_level_helper].join(',')
549
+ end
550
+ end
551
+ end
552
+ end
553
+ end
554
+
555
+ it 'can use the helper methods defined on the same route as the handler' do
556
+ get '/one'
557
+ expect(last_response.status).to eq(200)
558
+ expect(last_response.body).to eq('first_level_helper')
559
+ end
560
+
561
+ it 'can use the helper methods defined on all routes above a handler' do
562
+ get '/one/two'
563
+ expect(last_response.status).to eq(200)
564
+ expect(last_response.body).to include('handler:top_level_helper,first_level_helper,second_level_helper')
565
+ end
566
+
567
+ it 'can use the helper methods in before blocks' do
568
+ get '/one/two'
569
+ expect(last_response.status).to eq(200)
570
+ expect(last_response.body).to include('before:top_level_helper,first_level_helper,second_level_helper')
571
+ end
572
+
573
+ it 'can use the helper methods in after blocks' do
574
+ get '/one/two'
575
+ expect(last_response.status).to eq(200)
576
+ expect(last_response.body).to include('after:top_level_helper,first_level_helper,second_level_helper')
577
+ end
578
+ end
579
+
580
+ context 'an app that receives configuration when created' do
581
+ let :app do
582
+ App.new(this_thing, that_other_thing) do
583
+ setup do |*args|
584
+ @args = args
585
+ end
586
+
587
+ get do
588
+ @args.join(',')
589
+ end
590
+
591
+ route 'one' do
592
+ setup do |thing1, thing2|
593
+ @thing1 = thing1
594
+ @thing2 = thing2
595
+ end
596
+
597
+ get do
598
+ [*@args, @thing1, @thing2].join(',')
599
+ end
600
+ end
601
+
602
+ route 'two' do
603
+ setup do |thing1, thing2|
604
+ @thing1 = thing1
605
+ @thing2 = thing2
606
+ end
607
+
608
+ setup do |_, thing_two|
609
+ @thing_two = thing_two
610
+ end
611
+
612
+ get do
613
+ [*@args, @thing1, @thing2, @thing_two].join(',')
614
+ end
615
+ end
616
+ end
617
+ end
618
+
619
+ let :this_thing do
620
+ double(:this_thing, to_s: 'this_thing')
621
+ end
622
+
623
+ let :that_other_thing do
624
+ double(:that_other_thing, to_s: 'that_other_thing')
625
+ end
626
+
627
+ it 'calls its setup methods with the configuration' do
628
+ get '/'
629
+ expect(last_response.status).to eq(200)
630
+ expect(last_response.body).to eq('this_thing,that_other_thing')
631
+ end
632
+
633
+ it 'calls the setup methods of all routes' do
634
+ get '/one'
635
+ expect(last_response.status).to eq(200)
636
+ expect(last_response.body).to eq('this_thing,that_other_thing,this_thing,that_other_thing')
637
+ end
638
+
639
+ it 'calls all setup methods' do
640
+ get '/two'
641
+ expect(last_response.status).to eq(200)
642
+ expect(last_response.body).to eq('this_thing,that_other_thing,this_thing,that_other_thing,that_other_thing')
643
+ end
644
+ end
645
+
646
+ context 'an app that uses Rack middleware' do
647
+ class Reverser
648
+ def initialize(app)
649
+ @app = app
650
+ end
651
+
652
+ def call(env)
653
+ response = @app.call(env)
654
+ body = response[2][0]
655
+ body && body.reverse!
656
+ response
657
+ end
658
+ end
659
+
660
+ class Uppercaser
661
+ def initialize(app)
662
+ @app = app
663
+ end
664
+
665
+ def call(env)
666
+ response = @app.call(env)
667
+ body = response[2][0]
668
+ body && body.upcase!
669
+ response
670
+ end
671
+ end
672
+
673
+ class Mutator
674
+ def initialize(app, &block)
675
+ @app = app
676
+ @block = block
677
+ end
678
+
679
+ def call(env)
680
+ @app.call(@block.call(env))
681
+ end
682
+ end
683
+
684
+ let :app do
685
+ App.new do
686
+ use Reverser
687
+
688
+ get do
689
+ 'lorem ipsum'
690
+ end
691
+
692
+ route 'more' do
693
+ use Uppercaser
694
+
695
+ get do
696
+ 'dolor sit'
697
+ end
698
+ end
699
+
700
+ route 'hello' do
701
+ use Rack::Runtime, 'Regal'
702
+ use Mutator do |env|
703
+ env['app.greeting'] = 'Bonjour'
704
+ env
705
+ end
706
+
707
+ get do |request|
708
+ request.env['app.greeting'] + ', ' + request.parameters['name']
709
+ end
710
+ end
711
+ end
712
+ end
713
+
714
+ it 'calls the middleware when processing the request' do
715
+ get '/'
716
+ expect(last_response.status).to eq(200)
717
+ expect(last_response.body).to eq('muspi merol')
718
+ end
719
+
720
+ it 'calls the middleware of all routes' do
721
+ get '/more'
722
+ expect(last_response.status).to eq(200)
723
+ expect(last_response.body).to eq('TIS ROLOD')
724
+ end
725
+
726
+ it 'passes arguments when instantiating the middleware' do
727
+ get '/hello?name=Eve'
728
+ expect(last_response.status).to eq(200)
729
+ expect(last_response.headers).to have_key('X-Runtime-Regal')
730
+ end
731
+
732
+ it 'passes blocks when instantiating the middleware' do
733
+ get '/hello?name=Eve'
734
+ expect(last_response.status).to eq(200)
735
+ expect(last_response.body).to eq('Bonjour, Eve'.reverse)
736
+ end
737
+ end
738
+
739
+ context 'an app which needs more control over the response body' do
740
+ let :app do
741
+ App.new do
742
+ route 'no-overwrite' do
743
+ get do |_, response|
744
+ response.body = 'foobar'
745
+ response.finish
746
+ end
747
+ end
748
+
749
+ route 'raw-body' do
750
+ get do |_, response|
751
+ response.raw_body = 'a'..'z'
752
+ end
753
+ end
754
+
755
+ route 'no-body' do
756
+ before do |_, response|
757
+ response.no_body
758
+ end
759
+
760
+ get do
761
+ 'I will not be used'
762
+ end
763
+ end
764
+ end
765
+ end
766
+
767
+ it 'can finish the response so that the result of the handler will not be used as body' do
768
+ get '/no-overwrite'
769
+ expect(last_response.body).to eq('foobar')
770
+ end
771
+
772
+ it 'can set the raw body of the response' do
773
+ get '/raw-body'
774
+ expect(last_response.body).to eq('abcdefghijklmnopqrstuvwxyz')
775
+ end
776
+
777
+ it 'can disable the response body completely' do
778
+ get '/no-body'
779
+ expect(last_response.body).to be_empty
780
+ end
781
+ end
782
+
783
+ context 'an app that responds with no-body response codes' do
784
+ let :app do
785
+ App.new do
786
+ [111, 204, 205, 304].each do |status|
787
+ route status.to_s do
788
+ get do |_, response|
789
+ response.status = status
790
+ 'this will not be returned'
791
+ end
792
+ end
793
+ end
794
+ end
795
+ end
796
+
797
+ it 'ignore the response body' do
798
+ [111, 204, 205, 304].each do |status|
799
+ get "/#{status}"
800
+ expect(last_response.status).to eq(status)
801
+ expect(last_response.body).to be_empty
802
+ end
803
+ end
804
+ end
805
+
806
+ context 'an app that raises exceptions' do
807
+ class SomeNastyError < StandardError; end
808
+ class AppError < StandardError; end
809
+ class SpecificError < AppError; end
810
+
811
+ let :app do
812
+ App.new do
813
+ route 'unhandled' do
814
+ get do
815
+ raise 'Bork!'
816
+ end
817
+ end
818
+
819
+ route 'handled' do
820
+ rescue_from AppError do |error, request, response|
821
+ response.body = error.message
822
+ end
823
+
824
+ after do |_, response|
825
+ response.headers['WasAfterCalled'] = 'yes'
826
+ end
827
+
828
+ get do
829
+ raise SpecificError, 'Boom!'
830
+ end
831
+
832
+ route 'handled' do
833
+ get do
834
+ raise AppError, 'Crash!'
835
+ end
836
+ end
837
+
838
+ route 'unhandled' do
839
+ get do
840
+ raise SomeNastyError
841
+ end
842
+ end
843
+
844
+ route 'from-before' do
845
+ before do
846
+ raise SpecificError, 'Bang!'
847
+ end
848
+
849
+ get do
850
+ end
851
+ end
852
+
853
+ route 'from-after' do
854
+ after do
855
+ raise SpecificError, 'Kazam!'
856
+ end
857
+
858
+ after do |_, response|
859
+ response.headers['NextAfterWasCalled'] = 'yes'
860
+ end
861
+
862
+ get do
863
+ end
864
+ end
865
+
866
+ route 'handled-locally' do
867
+ rescue_from SpecificError do |error, request, response|
868
+ end
869
+
870
+ get do
871
+ raise SpecificError, 'Bam!'
872
+ end
873
+ end
874
+ end
875
+ end
876
+ end
877
+
878
+ context 'from handlers' do
879
+ it 'does not catch them' do
880
+ expect { get '/unhandled' }.to raise_error('Bork!')
881
+ end
882
+
883
+ it 'delegates them to matching error handlers' do
884
+ get '/handled'
885
+ expect(last_response.body).to eq('Boom!')
886
+ end
887
+
888
+ it 'calls after blocks when errors are handled' do
889
+ get '/handled'
890
+ expect(last_response.headers['WasAfterCalled']).to eq('yes')
891
+ end
892
+
893
+ it 'lets them bubble all the way up when there are no matching error handlers' do
894
+ expect { get '/handled/unhandled' }.to raise_error(SomeNastyError)
895
+ end
896
+ end
897
+
898
+ context 'from before blocks' do
899
+ it 'delegates them to matching error handlers' do
900
+ get '/handled/from-before'
901
+ expect(last_response.body).to eq('Bang!')
902
+ end
903
+
904
+ it 'calls after blocks when errors are handled' do
905
+ get '/handled/from-before'
906
+ expect(last_response.headers['WasAfterCalled']).to eq('yes')
907
+ end
908
+ end
909
+
910
+ context 'from after blocks' do
911
+ it 'delegates them to matching error handlers' do
912
+ get '/handled/from-after'
913
+ expect(last_response.body).to eq('Kazam!')
914
+ end
915
+
916
+ it 'calls the rest of the after blocks when errors are handled' do
917
+ get '/handled/from-after'
918
+ expect(last_response.headers['NextAfterWasCalled']).to eq('yes')
919
+ expect(last_response.headers['WasAfterCalled']).to eq('yes')
920
+ end
921
+ end
922
+ end
923
+ end
924
+ end