rackr 0.0.64 → 0.0.67

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.
@@ -2,41 +2,25 @@
2
2
 
3
3
  class Rackr
4
4
  class Router
5
+ # A Route for Rackr is an object that has endpoint and callbacks (before and after)
5
6
  class Route
6
7
  attr_reader :endpoint,
7
- :splitted_path,
8
- :has_params,
9
- :has_befores,
10
8
  :befores,
11
- :has_afters,
12
- :afters
9
+ :has_befores,
10
+ :afters,
11
+ :has_afters
13
12
 
14
- def initialize(path, endpoint, befores: [], afters: [], wildcard: false)
15
- @path = path
16
- @splitted_path = @path.split('/')
13
+ def initialize(endpoint, befores: [], afters: [])
17
14
  @endpoint = endpoint
18
- @params = fetch_params
19
- @has_params = @params != []
20
15
  @befores = befores
21
16
  @has_befores = befores != []
22
17
  @afters = afters
23
18
  @has_afters = afters != []
24
- @path_regex = /\A#{path.gsub(/(:\w+)/, '([^/]+)')}\z/
25
- @wildcard = wildcard
26
19
  end
27
20
 
28
- def match?(path_info)
29
- return path_info.match?(@path_regex) if @has_params
30
- return true if @wildcard
31
-
32
- path_info == @path
33
- end
34
-
35
- private
36
-
37
- def fetch_params
38
- @splitted_path.select { |value| value.start_with? ':' }
39
- end
21
+ def match? = true
22
+ def splitted_path = []
23
+ def has_params = false
40
24
  end
41
25
  end
42
26
  end
data/lib/rackr/router.rb CHANGED
@@ -1,19 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'router/errors'
4
+ require_relative 'router/endpoint'
4
5
  require_relative 'router/route'
6
+ require_relative 'router/path_route'
5
7
  require_relative 'router/build_request'
8
+ require_relative 'router/dev_html/errors'
9
+ require_relative 'router/dev_html/dump'
6
10
 
7
11
  class Rackr
12
+ # This is the core class of Rackr. This class aggregate the route instance tree, callbacks (before and after) and scopes
13
+ # then, using the building blocks, match the request and call the endpoints
8
14
  class Router
9
- attr_writer :not_found
10
- attr_reader :routes, :config
15
+ include Rackr::Utils
16
+
17
+ attr_writer :default_not_found
18
+ attr_reader :routes, :config, :not_found_tree, :error_tree, :specific_error_tree
11
19
 
12
20
  def initialize(config = {}, before: [], after: [])
13
- @instance_routes = {}
14
- %w[GET POST DELETE PUT TRACE OPTIONS PATCH].each do |method|
15
- @instance_routes[method] = { __instances: [] }
16
- end
17
21
  http_methods = HTTP_METHODS.map { |m| m.downcase.to_sym }
18
22
  @routes = Struct.new(*http_methods).new
19
23
  http_methods.each do |method|
@@ -28,55 +32,121 @@ class Rackr
28
32
  @scopes_befores = {}
29
33
  @afters = ensure_array(after)
30
34
  @scopes_afters = {}
31
- @error = proc { |_req, e| raise e }
32
- @not_found = proc { [404, {}, ['Not found']] }
33
- @splitted_request_path_info = []
34
- @current_request_path_info = nil
35
+ @path_routes_tree = {}
36
+ %w[GET POST DELETE PUT TRACE OPTIONS PATCH].each do |method|
37
+ @path_routes_tree[method] = { __instances: [] }
38
+ end
39
+ @not_found_tree = {}
40
+ @default_not_found =
41
+ Route.new(
42
+ proc { [404, {}, ['Not found']] },
43
+ befores: @befores,
44
+ afters: @afters
45
+ )
46
+ @error_tree = {}
47
+ @default_error =
48
+ Route.new(
49
+ proc { |_req, _e| [500, {}, ['Internal server error']] },
50
+ befores: @befores,
51
+ afters: @afters
52
+ )
53
+ @specific_error_tree = {}
54
+ @specific_errors = {}
35
55
  end
36
56
 
37
57
  def call(env)
38
58
  path_info = env['PATH_INFO']
59
+ request_method = env['REQUEST_METHOD'] == 'HEAD' ? 'GET' : env['REQUEST_METHOD']
39
60
 
40
- @splitted_request_path_info = path_info.split('/')
41
- @current_request_path_info =
61
+ splitted_request_path_info = path_info.split('/')
62
+ current_request_path_info =
42
63
  path_info == '/' ? path_info : path_info.chomp('/') # remove trailing "/"
43
64
 
44
- request_builder = BuildRequest.new(env, @splitted_request_path_info)
45
- env['REQUEST_METHOD'] = 'GET' if env['REQUEST_METHOD'] == 'HEAD'
65
+ route_instance, found_scopes = match_path_route(
66
+ request_method,
67
+ splitted_request_path_info,
68
+ current_request_path_info
69
+ )
70
+ rack_request = BuildRequest.new(env, splitted_request_path_info).call(route_instance)
71
+ befores = route_instance.befores
72
+ before_result = nil
46
73
 
47
- route_instance = match_route(env['REQUEST_METHOD'])
48
- return call_endpoint(@not_found, request_builder.call) if route_instance.nil?
74
+ begin
75
+ i = 0
76
+ while i < befores.size
77
+ before_result = Endpoint.call(befores[i], rack_request, @routes, @config)
78
+ unless before_result.is_a?(Rack::Request)
79
+ Errors.check_rack_response(before_result, 'before callback')
49
80
 
50
- rack_request = request_builder.call(route_instance)
81
+ return before_result
82
+ end
51
83
 
52
- befores = route_instance.befores
53
- before_result = nil
54
- i = 0
55
- while i < befores.size
56
- before_result = call_endpoint(befores[i], rack_request)
57
- return before_result unless before_result.is_a?(Rack::Request)
84
+ i += 1
85
+ end
58
86
 
59
- rack_request = before_result
87
+ endpoint_result = Endpoint.call(route_instance.endpoint, before_result || rack_request, @routes, @config)
60
88
 
61
- i += 1
89
+ call_afters(route_instance, endpoint_result)
90
+ rescue Rackr::NotFound
91
+ return not_found_fallback(found_scopes, route_instance, before_result || rack_request)
92
+ rescue Rackr::Dump => e
93
+ return Endpoint.call(DevHtml::Dump, env.merge({ 'dump' => e }))
94
+ # rubocop:disable Lint/RescueException
95
+ rescue Exception => e
96
+ return error_fallback(found_scopes, route_instance, before_result || rack_request, e, env)
62
97
  end
98
+ # rubocop:enable Lint/RescueException
99
+
100
+ Errors.check_rack_response(endpoint_result, 'action')
101
+ endpoint_result
102
+ end
103
+
104
+ def not_found_fallback(found_scopes, route_instance, request)
105
+ endpoint_result = Endpoint.call(
106
+ match_route(
107
+ found_scopes,
108
+ not_found_tree,
109
+ @default_not_found
110
+ ).endpoint,
111
+ request,
112
+ @routes,
113
+ @config
114
+ )
63
115
 
64
- endpoint_result = call_endpoint(route_instance.endpoint, before_result || rack_request)
116
+ call_afters(route_instance, endpoint_result)
65
117
 
118
+ endpoint_result
119
+ end
120
+
121
+ def error_fallback(found_scopes, route_instance, request, error, env)
122
+ error_route = match_route(
123
+ found_scopes,
124
+ specific_error_tree[error.class] || error_tree,
125
+ @specific_errors[error.class] || @default_error
126
+ )
127
+
128
+ return Endpoint.call(DevHtml::Errors, env.merge({ 'error' => error })) if @dev_mode && error_route == @default_error
129
+
130
+ endpoint_result = Endpoint.call(error_route.endpoint, request, @routes, @config, error)
131
+
132
+ if endpoint_result.nil?
133
+ return Endpoint.call(DevHtml::Errors, env.merge({ 'error' => error })) if @dev_mode
134
+
135
+ endpoint_result = Endpoint.call(@default_error.endpoint, request, @routes, @config, error)
136
+ end
137
+
138
+ call_afters(route_instance, endpoint_result)
139
+
140
+ endpoint_result
141
+ end
142
+
143
+ def call_afters(route_instance, endpoint_result)
66
144
  afters = route_instance.afters
67
145
  i = 0
68
146
  while i < afters.size
69
- call_endpoint(afters[i], endpoint_result)
147
+ Endpoint.call(afters[i], endpoint_result, @routes, @config)
70
148
  i += 1
71
149
  end
72
-
73
- endpoint_result
74
- rescue Rackr::NotFound
75
- call_endpoint(@not_found, request_builder.call)
76
- rescue Exception => e
77
- return @error.call(request_builder.call, e) if !@dev_mode || ENV['RACKR_ERROR_DEV']
78
-
79
- call_endpoint(Errors::DevHtml, env.merge({ 'error' => e }))
80
150
  end
81
151
 
82
152
  def add(method, path, endpoint, as: nil, route_befores: [], route_afters: [])
@@ -89,34 +159,65 @@ class Rackr
89
159
  method = :get if method == :head
90
160
 
91
161
  wildcard = path == '*'
92
- path = path.is_a?(Symbol) ? path.inspect : path.sub(%r{\A/}, '')
162
+ path = path.is_a?(Symbol) ? path.inspect : path.delete_prefix('/')
93
163
  path_with_scopes = "/#{not_empty_scopes.join('/')}#{put_path_slash(path)}"
94
164
  add_named_route(method, path_with_scopes, as)
165
+ action_befores, action_afters = fetch_endpoint_callbacks(endpoint)
95
166
 
96
167
  route_instance =
97
- Route.new(
168
+ PathRoute.new(
98
169
  path_with_scopes,
99
170
  endpoint,
100
- befores: @befores + ensure_array(route_befores),
101
- afters: @afters + ensure_array(route_afters),
171
+ befores: @befores + ensure_array(route_befores) + action_befores,
172
+ afters: @afters + ensure_array(route_afters) + action_afters,
102
173
  wildcard: wildcard
103
174
  )
104
175
 
105
- return push_to_scope(method.to_s.upcase, route_instance) if @scopes.size >= 1
176
+ path_segments = path_with_scopes.split('/').reject(&:empty?)
106
177
 
107
- @instance_routes[method.to_s.upcase][:__instances].push(route_instance)
178
+ if path_segments.empty?
179
+ @path_routes_tree[method.to_s.upcase][:__instances].push(route_instance)
180
+ else
181
+ deep_hash_push(@path_routes_tree[method.to_s.upcase], *(path_segments + [:__instances]), route_instance)
182
+ end
108
183
  end
109
184
 
110
185
  def add_not_found(endpoint)
111
- Errors.check_endpoint(endpoint, 'not_found')
186
+ Errors.check_endpoint(endpoint, 'not found')
112
187
 
113
- @not_found = endpoint
188
+ action_befores, action_afters = fetch_endpoint_callbacks(endpoint)
189
+ route_instance =
190
+ Route.new(
191
+ endpoint,
192
+ befores: @befores + action_befores,
193
+ afters: @afters + action_afters
194
+ )
195
+
196
+ return set_to_scope(not_found_tree, route_instance) if @scopes.size >= 1
197
+
198
+ @default_not_found = route_instance
114
199
  end
115
200
 
116
- def add_error(endpoint)
201
+ def add_error(endpoint, error_class = nil)
117
202
  Errors.check_endpoint(endpoint, 'error')
118
203
 
119
- @error = endpoint
204
+ action_befores, action_afters = fetch_endpoint_callbacks(endpoint)
205
+ route_instance =
206
+ Route.new(
207
+ endpoint,
208
+ befores: @befores + action_befores,
209
+ afters: @afters + action_afters
210
+ )
211
+
212
+ if error_class
213
+ return set_to_scope(specific_error_tree[error_class] ||= {}, route_instance) if @scopes.size >= 1
214
+
215
+ @specific_errors[error_class] = route_instance
216
+ else
217
+ return set_to_scope(error_tree, route_instance) if @scopes.size >= 1
218
+
219
+ @default_error = route_instance
220
+ end
120
221
  end
121
222
 
122
223
  def append_scope(name, scope_befores: [], scope_afters: [])
@@ -144,45 +245,34 @@ class Rackr
144
245
  @scopes = @scopes.first(@scopes.size - 1)
145
246
  end
146
247
 
147
- private
248
+ def not_empty_scopes
249
+ @scopes.reject { |v| (v == '') }
250
+ end
148
251
 
149
- def call_endpoint(endpoint, content)
150
- return endpoint.call(content) if endpoint.respond_to?(:call)
252
+ private
151
253
 
152
- if endpoint < Rackr::Action || endpoint < Rackr::Callback
153
- return endpoint.new(routes: @routes, config: @config).call(content)
254
+ def fetch_endpoint_callbacks(endpoint)
255
+ action_befores = []
256
+ action_afters = []
257
+ if endpoint.is_a?(Class) && endpoint.ancestors.include?(Rackr::Action)
258
+ action_instance = endpoint.new
259
+ action_befores = action_instance.befores
260
+ action_afters = action_instance.afters
154
261
  end
155
262
 
156
- endpoint.new.call(content)
157
- end
158
-
159
- def ensure_array(list)
160
- return [] if list.nil?
161
- return list if list.is_a?(Array)
162
-
163
- [list]
263
+ [action_befores, action_afters]
164
264
  end
165
265
 
166
266
  def add_named_route(method, path_with_scopes, as)
167
267
  return @routes.send(method.downcase)[:root] = path_with_scopes if path_with_scopes == '/'
168
268
  return @routes.send(method.downcase)[as] = path_with_scopes unless as.nil?
169
269
 
170
- key = path_with_scopes.sub('/', '').gsub(':', '').gsub('/', '_')
270
+ key = path_with_scopes.sub('/', '').delete(':').tr('/', '_')
171
271
  @routes.send(method.downcase)[key.to_s.to_sym] = path_with_scopes
172
272
  end
173
273
 
174
- def push_to_scope(method, route_instance)
175
- scopes_with_slash = not_empty_scopes + %i[__instances]
176
- push_it(@instance_routes[method], *scopes_with_slash, route_instance)
177
- end
178
-
179
- def push_it(hash, first_key, *rest_keys, val)
180
- if rest_keys.empty?
181
- (hash[first_key] ||= []) << val
182
- else
183
- hash[first_key] = push_it(hash[first_key] ||= {}, *rest_keys, val)
184
- end
185
- hash
274
+ def set_to_scope(instances, route_instance)
275
+ deep_hash_set(instances, not_empty_scopes + %i[__instance], route_instance)
186
276
  end
187
277
 
188
278
  def put_path_slash(path)
@@ -195,52 +285,62 @@ class Rackr
195
285
  path
196
286
  end
197
287
 
198
- def not_empty_scopes
199
- @scopes.reject { |v| (v == '') }
200
- end
288
+ def match_path_route(request_method, splitted_request_path_info, current_request_path_info)
289
+ path_routes_tree = @path_routes_tree[request_method]
290
+ found_scopes = []
201
291
 
202
- def match_route(request_method)
203
- find_instance_in_scope = proc do |request_method, found_scopes|
204
- @instance_routes[request_method].dig(
205
- *(found_scopes + [:__instances])
206
- )&.detect { |route_instance| route_instance.match?(@current_request_path_info) }
292
+ i = 1
293
+ while i < splitted_request_path_info.size
294
+ segment = splitted_request_path_info[i]
295
+
296
+ if path_routes_tree.key?(segment)
297
+ found_scopes << segment
298
+ path_routes_tree = path_routes_tree[segment]
299
+ elsif (param_key = path_routes_tree.keys.find { |k| k.start_with?(':') })
300
+ found_scopes << param_key
301
+ path_routes_tree = path_routes_tree[param_key]
302
+ elsif path_routes_tree.key?('*')
303
+ path_routes_tree = path_routes_tree['*']
304
+ break
305
+ else
306
+ break
307
+ end
308
+ i += 1
207
309
  end
208
310
 
209
- last_tail = @splitted_request_path_info.drop(1)
210
- found_scopes = []
311
+ route_instance = path_routes_tree[:__instances]&.detect do |route|
312
+ route.match?(current_request_path_info)
313
+ end
211
314
 
212
- instance_routes = @instance_routes[request_method]
315
+ route_instance = find_not_found_route(found_scopes) if route_instance.nil?
213
316
 
214
- while last_tail && !last_tail.empty?
215
- segment = last_tail.shift
216
- found_route = nil
317
+ [route_instance, found_scopes]
318
+ end
217
319
 
218
- instance_routes.each_key do |scope|
219
- next if scope == :__instances
320
+ def find_not_found_route(found_scopes)
321
+ not_found_route = nil
220
322
 
221
- if segment == scope
222
- found_scopes << scope
223
- instance_routes = @instance_routes[request_method].dig(*found_scopes)
224
- break
225
- elsif scope.start_with?(':')
226
- found_route = find_instance_in_scope.(request_method, found_scopes)
227
- return found_route if found_route
323
+ while not_found_route.nil? && !found_scopes.empty?
324
+ not_found_route = @not_found_tree.dig(*found_scopes, :__instance)
325
+ break if not_found_route
228
326
 
229
- found_scopes << scope
230
- instance_routes = @instance_routes[request_method].dig(*found_scopes)
231
- break
232
- end
233
- end
327
+ found_scopes.pop
234
328
  end
235
329
 
236
- result_route = find_instance_in_scope.(request_method, found_scopes)
330
+ not_found_route || @default_not_found
331
+ end
237
332
 
238
- if result_route == nil && !found_scopes.empty?
239
- found_scopes.shift
240
- result_route = find_instance_in_scope.(request_method, found_scopes)
333
+ def match_route(found_scopes, instances, default_instance)
334
+ route_instance = nil
335
+
336
+ while route_instance.nil? && found_scopes != []
337
+ route_instance = instances&.dig(*(found_scopes + [:__instance]))
338
+ found_scopes.pop
241
339
  end
242
340
 
243
- result_route
341
+ return default_instance if route_instance.nil?
342
+
343
+ route_instance
244
344
  end
245
345
  end
246
346
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rackr
4
+ # Utils methods for Rackr
5
+ module Utils
6
+ def deep_hash_push(hash, first_key, *rest_keys, val)
7
+ if rest_keys.empty?
8
+ (hash[first_key] ||= []) << val
9
+ else
10
+ hash[first_key] = deep_hash_push(hash[first_key] ||= {}, *rest_keys, val)
11
+ end
12
+ hash
13
+ end
14
+
15
+ def ensure_array(list)
16
+ return [] if list.nil?
17
+ return list if list.is_a?(Array)
18
+
19
+ [list]
20
+ end
21
+
22
+ def deep_hash_set(hash, keys, value)
23
+ *path, last = keys
24
+ node = path.inject(hash) { |h, k| h[k] ||= {} }
25
+ node[last] = value
26
+ end
27
+ end
28
+ end