regal 0.1.0 → 0.2.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.
data/lib/regal/app.rb CHANGED
@@ -2,122 +2,182 @@ require 'rack'
2
2
 
3
3
  module Regal
4
4
  module App
5
- def self.create(*args, &block)
6
- Class.new(Route).create(nil, &block)
5
+ # Creates a new app described by the given block.
6
+ #
7
+ # @yield []
8
+ # @return [Class<Route>]
9
+ def self.create(&block)
10
+ Class.new(Route).create(&block)
7
11
  end
8
12
 
9
- def self.new(*args, &block)
10
- create(&block).new(*args)
13
+ # Creates a new app and instantiates it.
14
+ #
15
+ # This is the same as `App.create { }.new(attributes)`.
16
+ #
17
+ # @param [Hash] attributes
18
+ # @yield []
19
+ # @return [Route]
20
+ def self.new(attributes={}, &block)
21
+ create(&block).new(attributes)
11
22
  end
12
23
  end
13
24
 
14
- module RouterDsl
15
- attr_reader :name
25
+ module RoutesDsl
26
+ # @private
27
+ attr_reader :name,
28
+ :befores,
29
+ :afters,
30
+ :rescuers,
31
+ :handlers
16
32
 
33
+ # @private
17
34
  def create(name=nil, &block)
35
+ @name = name
18
36
  @mounted_apps = []
19
- @static_routes = {}
20
- @dynamic_route = nil
37
+ @routes = {}
21
38
  @handlers = {}
22
39
  @befores = []
23
40
  @afters = []
24
- @setups = []
25
- @middlewares = []
26
41
  @rescuers = []
27
- @name = name
28
42
  class_exec(&block)
43
+ @mounted_apps.freeze
44
+ @routes.freeze
45
+ @handlers.freeze
46
+ @befores.freeze
47
+ @afters.freeze
48
+ @rescuers.freeze
29
49
  self
30
50
  end
31
51
 
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)
52
+ # @private
53
+ def create_routes(attributes)
73
54
  routes = {}
74
- if @dynamic_route
75
- routes.default = @dynamic_route.new(*args)
76
- end
77
55
  @mounted_apps.each do |app|
78
- routes.merge!(app.create_routes(args))
56
+ mounted_routes = app.create_routes(attributes)
57
+ mounted_routes.merge!(mounted_routes) do |name, route, _|
58
+ MountGraft.new(app, route)
59
+ end
60
+ routes.merge!(mounted_routes)
79
61
  end
80
- @static_routes.each do |path, cls|
81
- routes[path] = cls.new(*args)
62
+ @routes.each do |path, cls|
63
+ routes[path] = cls.new(attributes)
64
+ end
65
+ if @routes.default
66
+ routes.default = @routes.default.new(attributes)
82
67
  end
83
68
  routes
84
69
  end
85
70
 
86
- def handlers
87
- @handlers.dup
88
- end
89
-
71
+ # Defines a route.
72
+ #
73
+ # A route is either static or dynamic. Static routes match request path
74
+ # components verbatim, whereas dynamic routes captures their value. A
75
+ # static route is defined by a string and a dynamic route by a symbol.
76
+ #
77
+ # When routes are matched during request handling a static route will match
78
+ # if it is exactly equal to the next path component. A dynamic route will
79
+ # always match. All static routes are tried before any dynamic route.
80
+ #
81
+ # A route can only have a single dynamic child route. If you declare multiple
82
+ # dynamic routes only the last one is kept.
83
+ #
84
+ # @param [String, Symbol] s either a string, which creates a static route
85
+ # that matches a path component exactly, or a symbol, which captures the
86
+ # value of the path component.
87
+ # @yield []
88
+ # @return [void]
90
89
  def route(s, &block)
91
90
  r = Class.new(self).create(s, &block)
92
91
  if s.is_a?(Symbol)
93
- @dynamic_route = r
92
+ @routes.default = r
94
93
  else
95
- @static_routes[s] = r
94
+ @routes[s] = r
96
95
  end
96
+ nil
97
97
  end
98
98
 
99
+ # Mount a child app.
100
+ #
101
+ # Mounting a child app makes that app's routes available as if they were
102
+ # defined in this route.
103
+ #
104
+ # @param [Class<Route>] app
105
+ # @return [void]
99
106
  def mount(app)
100
107
  @mounted_apps << app
108
+ nil
101
109
  end
102
110
 
103
- def use(middleware, *args, &block)
104
- @middlewares << [middleware, args, block]
105
- end
106
-
107
- def setup(&block)
108
- @setups << block
111
+ # Wrap the child routes and handlers in a scope.
112
+ #
113
+ # Scopes can have before, after and rescue blocks that are not shared with
114
+ # sibling scopes. They work more or less like mounting apps, but inline.
115
+ #
116
+ # @yield []
117
+ # @return [void]
118
+ def scope(&block)
119
+ mount(Class.new(self).create(&block))
109
120
  end
110
121
 
122
+ # Register a before block for this route.
123
+ #
124
+ # Before blocks run before the request handler and have access to the
125
+ # request and response.
126
+ #
127
+ # A route can have any number of before blocks and they will be called
128
+ # in the order that they are declared with the outermost route's before
129
+ # blocks being called first, followed by child routes.
130
+ #
131
+ # @yield [request, response]
132
+ # @yieldparam request [Request]
133
+ # @yieldparam response [Response]
134
+ # @return [void]
111
135
  def before(&block)
112
136
  @befores << block
137
+ nil
113
138
  end
114
139
 
140
+ # Register an after block for this route.
141
+ #
142
+ # After blocks run after the request handler and have access to the
143
+ # request and response.
144
+ #
145
+ # A route can have any number of after blocks and they will be called
146
+ # in the order that they are declared with the innermost route's after
147
+ # blocks being called first, followed by the parent route.
148
+ #
149
+ # @yield [request, response]
150
+ # @yieldparam request [Request]
151
+ # @yieldparam response [Response]
152
+ # @return [void]
115
153
  def after(&block)
116
154
  @afters << block
155
+ nil
117
156
  end
118
157
 
158
+ # Register a rescue block for this route.
159
+ #
160
+ # Rescue blocks run when a before or after block, or a handler raises
161
+ # an error that matches the block's error type (compared using `#===`).
162
+ #
163
+ # A route can have any number of rescue blocks, but the order they are
164
+ # declared in is important. When an error is raised the blocks are searched
165
+ # in reverse order for a match, so the last declared rescue block with a
166
+ # matching type will be the one to handle the error.
167
+ #
168
+ # Once an error handler has been called the error is assumed to have been
169
+ # handled. If the error handler raises an error, the next matching handler
170
+ # will be found, or the error will bubble up outside the app.
171
+ #
172
+ # @param [Class<Error>] type
173
+ # @yield [error, request, response]
174
+ # @yieldparam error [Error]
175
+ # @yieldparam request [Request]
176
+ # @yieldparam response [Response]
177
+ # @return [void]
119
178
  def rescue_from(type, &block)
120
179
  @rescuers << [type, block]
180
+ nil
121
181
  end
122
182
 
123
183
  [:get, :head, :options, :delete, :post, :put, :patch].each do |name|
@@ -127,110 +187,308 @@ module Regal
127
187
  end
128
188
  end
129
189
 
190
+ # @!group Handlers
191
+
192
+ # Register a handler for `GET` requests to this route
193
+ #
194
+ # @!method get
195
+ # @yield [request, response]
196
+ # @yieldparam request [Request]
197
+ # @yieldparam response [Response]
198
+ # @yieldreturn [Object] the response body
199
+ # @return [void]
200
+
201
+ # Register a handler for `HEAD` requests to this route
202
+ #
203
+ # @!method head
204
+ # @yield [request, response]
205
+ # @yieldparam request [Request]
206
+ # @yieldparam response [Response]
207
+ # @yieldreturn [Object] the response body
208
+ # @return [void]
209
+
210
+ # Register a handler for `OPTIONS` requests to this route
211
+ #
212
+ # @!method options
213
+ # @yield [request, response]
214
+ # @yieldparam request [Request]
215
+ # @yieldparam response [Response]
216
+ # @yieldreturn [Object] the response body
217
+ # @return [void]
218
+
219
+ # Register a handler for `DELETE` requests to this route
220
+ #
221
+ # @!method delete
222
+ # @yield [request, response]
223
+ # @yieldparam request [Request]
224
+ # @yieldparam response [Response]
225
+ # @yieldreturn [Object] the response body
226
+ # @return [void]
227
+
228
+ # Register a handler for `POST` requests to this route
229
+ #
230
+ # @!method post
231
+ # @yield [request, response]
232
+ # @yieldparam request [Request]
233
+ # @yieldparam response [Response]
234
+ # @yieldreturn [Object] the response body
235
+ # @return [void]
236
+
237
+ # Register a handler for `PUT` requests to this route
238
+ #
239
+ # @!method put
240
+ # @yield [request, response]
241
+ # @yieldparam request [Request]
242
+ # @yieldparam response [Response]
243
+ # @yieldreturn [Object] the response body
244
+ # @return [void]
245
+
246
+ # Register a handler for `PATCH` requests to this route
247
+ #
248
+ # @!method patch
249
+ # @yield [request, response]
250
+ # @yieldparam request [Request]
251
+ # @yieldparam response [Response]
252
+ # @yieldreturn [Object] the response body
253
+ # @return [void]
254
+
255
+ # Register a handler for any request method
256
+ #
257
+ # `any` handlers are called when there is no specific handler in this route
258
+ # for the request method.
259
+ #
260
+ # @yield [request, response]
261
+ # @yieldparam request [Request]
262
+ # @yieldparam response [Response]
263
+ # @yieldreturn [Object] the response body
264
+ # @return [void]
130
265
  def any(&block)
131
266
  @handlers.default = block
132
267
  end
133
- end
134
268
 
135
- class Route
136
- extend RouterDsl
269
+ # @!endgroup
270
+ end
137
271
 
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
272
+ # @private
273
+ module Arounds
274
+ def before(request, response)
275
+ @befores.each do |before|
276
+ unless response.finished?
277
+ @route.instance_exec(request, response, &before)
278
+ end
279
+ end
280
+ end
146
281
 
147
- attr_reader :name
282
+ def after(request, response)
283
+ @afters.each do |after|
284
+ begin
285
+ @route.instance_exec(request, response, &after)
286
+ rescue => e
287
+ raise unless rescue_error(e, request, response)
288
+ end
289
+ end
290
+ end
148
291
 
149
- def initialize(*args)
150
- @actual = self.dup
151
- self.class.setups.each do |setup|
152
- @actual.instance_exec(*args, &setup)
292
+ def rescue_error(e, request, response)
293
+ @rescuers.reverse_each do |type, handler|
294
+ if type === e
295
+ @route.instance_exec(e, request, response, &handler)
296
+ return true
297
+ end
153
298
  end
299
+ false
300
+ end
301
+ end
302
+
303
+ # A route is an application, or a part of an application
304
+ class Route
305
+ extend RoutesDsl
306
+ include Arounds
307
+
308
+ # @private
309
+ attr_reader :name,
310
+ :routes
311
+
312
+ # Create a new application with this route as its root
313
+ #
314
+ # @param [Hash] attributes a copy of this hash will be available
315
+ # from {Request#attributes} during request processing
316
+ def initialize(attributes)
317
+ @attributes = attributes.dup.freeze
318
+ @name = self.class.name
154
319
  @befores = self.class.befores
155
- @afters = self.class.afters.reverse
320
+ @afters = self.class.afters
156
321
  @rescuers = self.class.rescuers
157
- @routes = self.class.create_routes(args)
158
322
  @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
323
+ @routes = self.class.create_routes(attributes)
324
+ @route = self
165
325
  freeze
166
326
  end
167
327
 
328
+ # Route and handle a request
329
+ #
330
+ # @param [Hash] env
331
+ # @return [Array<(Integer, Hash, Enumerable)>]
168
332
  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
333
+ path_components = path_components = env[PATH_INFO_KEY].split(SLASH).drop(1)
334
+ parent_routes, path_captures = match_route(path_components)
335
+ matching_route = parent_routes.last
336
+ request_method = env[REQUEST_METHOD_KEY]
337
+ if matching_route && matching_route.can_handle?(request_method)
338
+ request = Request.new(env, path_captures, @attributes)
339
+ response = Response.new
340
+ finishing_route = run_befores(parent_routes, request, response)
341
+ if finishing_route.nil? && !response.finished?
342
+ begin
343
+ result = matching_route.handle(request_method, request, response)
344
+ unless response.finished?
345
+ response.body = result
346
+ end
347
+ rescue => e
348
+ finishing_route = handle_error(parent_routes, finishing_route, e, request, response)
349
+ end
176
350
  end
177
- app.call(env)
178
- elsif path_component.nil?
179
- if @app
180
- @app.call(env)
181
- else
182
- handle(env)
351
+ run_afters(parent_routes, finishing_route, request, response)
352
+ if no_body_response?(request_method, response)
353
+ response.no_body
183
354
  end
355
+ response
356
+ elsif matching_route
357
+ METHOD_NOT_ALLOWED_RESPONSE
184
358
  else
185
359
  NOT_FOUND_RESPONSE
186
360
  end
187
361
  end
188
362
 
363
+ # @private
364
+ def can_handle?(request_method)
365
+ !!@handlers[request_method]
366
+ end
367
+
368
+ # @private
369
+ def handle(request_method, request, response)
370
+ handler = @handlers[request_method]
371
+ instance_exec(request, response, &handler)
372
+ end
373
+
189
374
  private
190
375
 
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)
376
+ METHOD_NOT_ALLOWED_RESPONSE = [405, {}.freeze, [].freeze].freeze
377
+ NOT_FOUND_RESPONSE = [404, {}.freeze, [].freeze].freeze
378
+ SLASH = '/'.freeze
379
+ PATH_INFO_KEY = 'PATH_INFO'.freeze
380
+ REQUEST_METHOD_KEY = 'REQUEST_METHOD'.freeze
381
+ HEAD_METHOD = 'HEAD'.freeze
382
+
383
+ def no_body_response?(request_method, response)
384
+ request_method == HEAD_METHOD || response.status < 200 || response.status == 204 || response.status == 205 || response.status == 304
385
+ end
386
+
387
+ def match_route(path_components)
388
+ path_captures = {}
389
+ matching_route = self
390
+ parent_routes = [self]
391
+ path_components.each do |path_component|
392
+ if matching_route
393
+ wildcard_route = !matching_route.routes.include?(path_component)
394
+ matching_route = matching_route.routes[path_component]
395
+ if matching_route && wildcard_route
396
+ path_captures[matching_route.name] = path_component
199
397
  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
398
+ end
399
+ parent_routes << matching_route
400
+ end
401
+ [parent_routes, path_captures]
402
+ end
403
+
404
+ def run_befores(parent_routes, request, response)
405
+ parent_routes.each do |parent_route|
406
+ begin
407
+ parent_route.before(request, response)
408
+ if response.finished?
409
+ return parent_route
207
410
  end
208
411
  rescue => e
209
- handle_error(e, request, response)
412
+ return handle_error(parent_routes, parent_route, e, request, response)
210
413
  end
211
- @afters.each do |after|
414
+ end
415
+ nil
416
+ end
417
+
418
+ def run_afters(parent_routes, finishing_route, request, response)
419
+ skip_routes = !finishing_route.nil?
420
+ parent_routes.reverse_each do |parent_route|
421
+ if !skip_routes || finishing_route == parent_route
422
+ skip_routes = false
212
423
  begin
213
- @actual.instance_exec(request, response, &after)
424
+ parent_route.after(request, response)
214
425
  rescue => e
215
- handle_error(e, request, response)
426
+ skip_routes = true
427
+ finishing_route = handle_error(parent_routes, parent_route, e, request, response)
216
428
  end
217
429
  end
218
- response
219
- else
220
- METHOD_NOT_ALLOWED_RESPONSE
221
430
  end
222
431
  end
223
432
 
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
433
+ def handle_error(parent_routes, finishing_route, e, request, response)
434
+ skip_routes = !finishing_route.nil?
435
+ parent_routes.reverse_each do |parent_route|
436
+ if !skip_routes || finishing_route == parent_route
437
+ skip_routes = false
438
+ begin
439
+ if parent_route.rescue_error(e, request, response)
440
+ return parent_route
441
+ end
442
+ rescue => e
443
+ if parent_routes.first == parent_route
444
+ raise e
445
+ else
446
+ next_level = parent_routes[parent_routes.index(parent_route) - 1]
447
+ return handle_error(parent_routes, next_level, e, request, response)
448
+ end
449
+ end
231
450
  end
232
451
  end
233
- raise unless handled
452
+ raise e
453
+ end
454
+ end
455
+
456
+ # @private
457
+ class MountGraft
458
+ include Arounds
459
+
460
+ attr_reader :name,
461
+ :routes
462
+
463
+ def initialize(mounted_app, route)
464
+ @route = route
465
+ @name = route.name
466
+ @routes = route.routes
467
+ @befores = mounted_app.befores
468
+ @afters = mounted_app.afters
469
+ @rescuers = mounted_app.rescuers
470
+ end
471
+
472
+ def can_handle?(request_method)
473
+ @route.can_handle?(request_method)
474
+ end
475
+
476
+ def handle(*args)
477
+ @route.handle(*args)
478
+ end
479
+
480
+ def before(*args)
481
+ super
482
+ @route.before(*args)
483
+ end
484
+
485
+ def after(*args)
486
+ @route.after(*args)
487
+ super
488
+ end
489
+
490
+ def rescue_error(e, *args)
491
+ @route.rescue_error(e, *args) or super
234
492
  end
235
493
  end
236
494
  end
data/lib/regal/request.rb CHANGED
@@ -1,29 +1,31 @@
1
1
  module Regal
2
2
  class Request
3
- attr_reader :env, :attributes
3
+ # @!attribute [r] env
4
+ # @return [Hash]
4
5
 
5
- def initialize(env)
6
- @env = env
7
- @attributes = {}
8
- end
6
+ # @!attribute [r] attributes
7
+ # @return [Hash]
9
8
 
10
- def request_method
11
- @env[REQUEST_METHOD_KEY]
12
- end
9
+ attr_reader :env,
10
+ :attributes
13
11
 
14
- def head?
15
- request_method == HEAD_METHOD
12
+ # @private
13
+ def initialize(env, path_captures, attributes_prototype={})
14
+ @env = env
15
+ @path_captures = path_captures
16
+ @attributes = attributes_prototype.dup
16
17
  end
17
18
 
19
+ # @return [Hash]
18
20
  def parameters
19
21
  @parameters ||= begin
20
- path_captures = @env[Route::PATH_CAPTURES_KEY]
21
22
  query = Rack::Utils.parse_query(@env[QUERY_STRING_KEY])
22
- query.merge!(path_captures) if path_captures
23
+ query.merge!(@path_captures)
23
24
  query.freeze
24
25
  end
25
26
  end
26
27
 
28
+ # @return [Hash]
27
29
  def headers
28
30
  @headers ||= begin
29
31
  headers = @env.each_with_object({}) do |(key, value), headers|
@@ -44,10 +46,13 @@ module Regal
44
46
  end
45
47
  end
46
48
 
49
+ # @return [IO]
47
50
  def body
48
51
  @env[RACK_INPUT_KEY]
49
52
  end
50
53
 
54
+ private
55
+
51
56
  HEADER_PREFIX = 'HTTP_'.freeze
52
57
  QUERY_STRING_KEY = 'QUERY_STRING'.freeze
53
58
  CONTENT_LENGTH_KEY = 'CONTENT_LENGTH'.freeze
@@ -55,7 +60,5 @@ module Regal
55
60
  CONTENT_TYPE_KEY = 'CONTENT_TYPE'.freeze
56
61
  CONTENT_TYPE_HEADER = 'Content-Type'.freeze
57
62
  RACK_INPUT_KEY = 'rack.input'.freeze
58
- REQUEST_METHOD_KEY = 'REQUEST_METHOD'.freeze
59
- HEAD_METHOD = 'HEAD'.freeze
60
63
  end
61
64
  end