regal 0.1.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 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