rack-jet_router 1.2.0 → 1.3.1
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 +5 -5
- data/CHANGES.md +58 -0
- data/MIT-LICENSE +1 -1
- data/README.md +187 -77
- data/bench/Gemfile +13 -0
- data/bench/Rakefile.rb +11 -0
- data/bench/bench.rb +367 -0
- data/lib/rack/jet_router.rb +410 -210
- data/rack-jet_router.gemspec +21 -18
- data/test/builder_test.rb +638 -0
- data/test/misc_test.rb +23 -0
- data/test/router_test.rb +597 -0
- data/test/run_all.rb +6 -0
- data/test/{test_helper.rb → shared.rb} +8 -3
- metadata +36 -42
- data/Rakefile +0 -58
- data/test/rack/jet_router_test.rb +0 -726
data/lib/rack/jet_router.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
4
|
###
|
4
|
-
### $Release: 1.
|
5
|
-
### $Copyright: copyright(c) 2015
|
5
|
+
### $Release: 1.3.1 $
|
6
|
+
### $Copyright: copyright(c) 2015 kwatch@gmail.com $
|
6
7
|
### $License: MIT License $
|
7
8
|
###
|
8
9
|
|
@@ -15,60 +16,139 @@ module Rack
|
|
15
16
|
##
|
16
17
|
## Jet-speed router class, derived from Keight.rb.
|
17
18
|
##
|
18
|
-
##
|
19
|
-
##
|
20
|
-
##
|
21
|
-
##
|
22
|
-
##
|
23
|
-
##
|
24
|
-
##
|
25
|
-
##
|
26
|
-
##
|
27
|
-
##
|
28
|
-
##
|
29
|
-
##
|
30
|
-
##
|
31
|
-
##
|
32
|
-
##
|
33
|
-
##
|
34
|
-
##
|
35
|
-
##
|
19
|
+
## Example #1:
|
20
|
+
## ### (assume that 'xxxx_app' are certain Rack applications.)
|
21
|
+
## mapping = {
|
22
|
+
## "/" => home_app,
|
23
|
+
## "/api" => {
|
24
|
+
## "/books" => {
|
25
|
+
## "" => books_app,
|
26
|
+
## "/:id(.:format)" => book_app,
|
27
|
+
## "/:book_id/comments/:comment_id" => comment_app,
|
28
|
+
## },
|
29
|
+
## },
|
30
|
+
## "/admin" => {
|
31
|
+
## "/books" => admin_books_app,
|
32
|
+
## },
|
33
|
+
## }
|
34
|
+
## router = Rack::JetRouter.new(mapping)
|
35
|
+
## router.lookup("/api/books/123.html")
|
36
|
+
## #=> [book_app, {"id"=>"123", "format"=>"html"}]
|
37
|
+
## status, headers, body = router.call(env)
|
36
38
|
##
|
37
|
-
##
|
38
|
-
##
|
39
|
-
##
|
40
|
-
##
|
41
|
-
##
|
42
|
-
##
|
43
|
-
##
|
44
|
-
##
|
45
|
-
##
|
46
|
-
##
|
47
|
-
##
|
48
|
-
##
|
49
|
-
##
|
50
|
-
##
|
51
|
-
##
|
52
|
-
##
|
53
|
-
##
|
54
|
-
##
|
39
|
+
## Example #2:
|
40
|
+
## mapping = [
|
41
|
+
## ["/" , {GET: home_app}],
|
42
|
+
## ["/api", [
|
43
|
+
## ["/books", [
|
44
|
+
## ["" , {GET: book_list_app, POST: book_create_app}],
|
45
|
+
## ["/:id(.:format)" , {GET: book_show_app, PUT: book_update_app}],
|
46
|
+
## ["/:book_id/comments/:comment_id", {POST: comment_create_app}],
|
47
|
+
## ]],
|
48
|
+
## ]],
|
49
|
+
## ["/admin", [
|
50
|
+
## ["/books" , {ANY: admin_books_app}],
|
51
|
+
## ]],
|
52
|
+
## ]
|
53
|
+
## router = Rack::JetRouter.new(mapping)
|
54
|
+
## router.lookup("/api/books/123")
|
55
|
+
## #=> [{"GET"=>book_show_app, "PUT"=>book_update_app}, {"id"=>"123", "format"=>nil}]
|
56
|
+
## status, headers, body = router.call(env)
|
57
|
+
##
|
58
|
+
## Example #3:
|
59
|
+
## class Map < Hash # define subclass of Hash
|
60
|
+
## end
|
61
|
+
## def Map(**kwargs) # define helper method to create Map object easily
|
62
|
+
## return Map.new.update(kwargs)
|
63
|
+
## end
|
64
|
+
## mapping = {
|
65
|
+
## "/" => Map(GET: home_app),
|
66
|
+
## "/api" => {
|
67
|
+
## "/books" => {
|
68
|
+
## "" => Map(GET: book_list_app, POST: book_create_app),
|
69
|
+
## "/:id(.:format)" => Map(GET: book_show_app, PUT: book_update_app),
|
70
|
+
## "/:book_id/comments/:comment_id" => Map(POST: comment_create_app),
|
71
|
+
## },
|
72
|
+
## },
|
73
|
+
## "/admin" => {
|
74
|
+
## "/books" => Map(ANY: admin_books_app),
|
75
|
+
## },
|
76
|
+
## }
|
77
|
+
## router = Rack::JetRouter.new(mapping)
|
78
|
+
## router.lookup("/api/books/123")
|
79
|
+
## #=> [{"GET"=>book_show_app, "PUT"=>book_update_app}, {"id"=>"123", "format"=>nil}]
|
80
|
+
## status, headers, body = router.call(env)
|
55
81
|
##
|
56
82
|
class JetRouter
|
57
83
|
|
58
|
-
RELEASE = '$Release: 1.
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
@
|
84
|
+
RELEASE = '$Release: 1.3.1 $'.split()[1]
|
85
|
+
|
86
|
+
#; [!haggu] contains available request methods.
|
87
|
+
REQUEST_METHODS = %w[GET POST PUT DELETE PATCH HEAD OPTIONS TRACE LINK UNLINK] \
|
88
|
+
.each_with_object({}) {|s, d| d[s] = s.intern }
|
89
|
+
|
90
|
+
def initialize(mapping, cache_size: 0, env_key: 'rack.urlpath_params',
|
91
|
+
int_param: nil, # ex: /(?:\A|_)id\z/
|
92
|
+
urlpath_cache_size: 0, # for backward compatibility
|
93
|
+
_enable_range: true) # undocumentend keyword arg
|
94
|
+
@env_key = env_key
|
95
|
+
@int_param = int_param
|
96
|
+
#; [!21mf9] 'urlpath_cache_size:' kwarg is available for backward compatibility.
|
97
|
+
@cache_size = [cache_size, urlpath_cache_size].max()
|
98
|
+
#; [!5tw57] cache is disabled when 'cache_size:' is zero.
|
99
|
+
@cache_dict = @cache_size > 0 ? {} : nil
|
100
|
+
##
|
101
|
+
## Pair list of endpoint and Rack app.
|
102
|
+
## ex:
|
103
|
+
## [
|
104
|
+
## ["/api/books" , books_app ],
|
105
|
+
## ["/api/books/:id" , book_app ],
|
106
|
+
## ["/api/orders" , orders_app],
|
107
|
+
## ["/api/orders/:id" , order_app ],
|
108
|
+
## ]
|
109
|
+
##
|
110
|
+
@all_endpoints = []
|
111
|
+
##
|
112
|
+
## Endpoints without any path parameters.
|
113
|
+
## ex:
|
114
|
+
## {
|
115
|
+
## "/" => home_app,
|
116
|
+
## "/api/books" => books_app,
|
117
|
+
## "/api/orders" => orders_app,
|
118
|
+
## }
|
119
|
+
##
|
120
|
+
@fixed_endpoints = {}
|
121
|
+
##
|
122
|
+
## Endpoints with one or more path parameters.
|
123
|
+
## ex:
|
124
|
+
## [
|
125
|
+
## [%r!\A/api/books/([^./?]+)\z! , ["id"], book_app , (11..-1)],
|
126
|
+
## [%r!\A/api/orders/([^./?]+)\z!, ["id"], order_app, (12..-1)],
|
127
|
+
## ]
|
128
|
+
##
|
129
|
+
@variable_endpoints = []
|
130
|
+
##
|
131
|
+
## Combined regexp of variable endpoints.
|
132
|
+
## ex:
|
133
|
+
## %r!\A/api/(?:books/[^./?]+(\z)|orders/[^./?]+(\z))\z!
|
134
|
+
##
|
135
|
+
@urlpath_rexp = nil
|
136
|
+
#
|
137
|
+
#; [!x2l32] gathers all endpoints.
|
138
|
+
builder = Builder.new(self, _enable_range)
|
139
|
+
builder.traverse_mapping(mapping) do |path, item|
|
140
|
+
@all_endpoints << [path, item]
|
141
|
+
end
|
142
|
+
#; [!l63vu] handles urlpath pattern as fixed when no urlpath params.
|
143
|
+
param_rexp = /:\w+|\(.*?\)/
|
144
|
+
pairs1, pairs2 = @all_endpoints.partition {|path, _| path =~ param_rexp }
|
145
|
+
pairs2.each {|path, item| @fixed_endpoints[path] = item }
|
146
|
+
#; [!saa1a] compiles compound urlpath regexp.
|
147
|
+
tree = builder.build_tree(pairs1)
|
148
|
+
@urlpath_rexp = builder.build_rexp(tree) do |tuple|
|
149
|
+
#; [!f1d7s] builds variable endpoint list.
|
150
|
+
@variable_endpoints << tuple
|
151
|
+
end
|
72
152
|
end
|
73
153
|
|
74
154
|
attr_reader :urlpath_rexp
|
@@ -77,13 +157,13 @@ module Rack
|
|
77
157
|
def call(env)
|
78
158
|
#; [!fpw8x] finds mapped app according to env['PATH_INFO'].
|
79
159
|
req_path = env['PATH_INFO']
|
80
|
-
|
160
|
+
obj, param_values = lookup(req_path)
|
81
161
|
#; [!wxt2g] guesses correct urlpath and redirects to it automaticaly when request path not found.
|
82
162
|
#; [!3vsua] doesn't redict automatically when request path is '/'.
|
83
|
-
if !
|
84
|
-
location = req_path
|
85
|
-
|
86
|
-
if
|
163
|
+
if ! obj && should_redirect?(env)
|
164
|
+
location = req_path.end_with?("/") ? req_path[0..-2] : req_path + "/"
|
165
|
+
obj, param_values = lookup(location)
|
166
|
+
if obj
|
87
167
|
#; [!hyk62] adds QUERY_STRING to redirect location.
|
88
168
|
qs = env['QUERY_STRING']
|
89
169
|
location = "#{location}?#{qs}" if qs && ! qs.empty?
|
@@ -91,20 +171,22 @@ module Rack
|
|
91
171
|
end
|
92
172
|
end
|
93
173
|
#; [!30x0k] returns 404 when request urlpath not found.
|
94
|
-
return error_not_found(env) unless
|
174
|
+
return error_not_found(env) unless obj
|
95
175
|
#; [!gclbs] if mapped object is a Hash...
|
96
|
-
if
|
176
|
+
if obj.is_a?(Hash)
|
97
177
|
#; [!p1fzn] invokes app mapped to request method.
|
98
178
|
#; [!5m64a] returns 405 when request method is not allowed.
|
99
179
|
#; [!ys1e2] uses GET method when HEAD is not mapped.
|
100
180
|
#; [!2hx6j] try ANY method when request method is not mapped.
|
101
|
-
dict =
|
181
|
+
dict = obj
|
102
182
|
req_meth = env['REQUEST_METHOD']
|
103
183
|
app = dict[req_meth] || (req_meth == 'HEAD' ? dict['GET'] : nil) || dict['ANY']
|
104
184
|
return error_not_allowed(env) unless app
|
185
|
+
else
|
186
|
+
app = obj
|
105
187
|
end
|
106
|
-
#; [!2c32f] stores urlpath
|
107
|
-
|
188
|
+
#; [!2c32f] stores urlpath parameter values into env['rack.urlpath_params'].
|
189
|
+
store_param_values(env, param_values)
|
108
190
|
#; [!hse47] invokes app mapped to request urlpath.
|
109
191
|
return app.call(env) # make body empty when HEAD?
|
110
192
|
end
|
@@ -116,11 +198,11 @@ module Rack
|
|
116
198
|
def lookup(req_path)
|
117
199
|
#; [!24khb] finds in fixed urlpaths at first.
|
118
200
|
#; [!iwyzd] urlpath param value is nil when found in fixed urlpaths.
|
119
|
-
obj = @
|
201
|
+
obj = @fixed_endpoints[req_path]
|
120
202
|
return obj, nil if obj
|
121
203
|
#; [!upacd] finds in variable urlpath cache if it is enabled.
|
122
204
|
#; [!1zx7t] variable urlpath cache is based on LRU.
|
123
|
-
cache = @
|
205
|
+
cache = @cache_dict
|
124
206
|
if cache && (pair = cache.delete(req_path))
|
125
207
|
cache[req_path] = pair
|
126
208
|
return pair
|
@@ -128,25 +210,25 @@ module Rack
|
|
128
210
|
#; [!vpdzn] returns nil when urlpath not found.
|
129
211
|
m = @urlpath_rexp.match(req_path)
|
130
212
|
return nil unless m
|
131
|
-
index = m.captures.
|
213
|
+
index = m.captures.index('')
|
132
214
|
return nil unless index
|
133
215
|
#; [!ijqws] returns mapped object and urlpath parameter values when urlpath found.
|
134
|
-
full_urlpath_rexp, param_names, obj, range = @
|
216
|
+
full_urlpath_rexp, param_names, obj, range = @variable_endpoints[index]
|
135
217
|
if range
|
136
|
-
## "/books/123"[7..-1] is faster than /\A\/books\/(\d+)\z/.match("/books/123")
|
218
|
+
## "/books/123"[7..-1] is faster than /\A\/books\/(\d+)\z/.match("/books/123")[1]
|
137
219
|
str = req_path[range]
|
138
|
-
|
220
|
+
values = [str]
|
139
221
|
else
|
140
222
|
m = full_urlpath_rexp.match(req_path)
|
141
|
-
|
223
|
+
values = m.captures
|
142
224
|
end
|
143
|
-
|
225
|
+
param_values = build_param_values(param_names, values)
|
144
226
|
#; [!84inr] caches result when variable urlpath cache enabled.
|
145
227
|
if cache
|
146
|
-
cache.shift() if cache.length >= @
|
147
|
-
cache[req_path] = [obj,
|
228
|
+
cache.shift() if cache.length >= @cache_size
|
229
|
+
cache[req_path] = [obj, param_values]
|
148
230
|
end
|
149
|
-
return obj,
|
231
|
+
return obj, param_values
|
150
232
|
end
|
151
233
|
|
152
234
|
alias find lookup # :nodoc: # for backward compatilibity
|
@@ -154,7 +236,7 @@ module Rack
|
|
154
236
|
## Yields pair of urlpath pattern and app.
|
155
237
|
def each(&block)
|
156
238
|
#; [!ep0pw] yields pair of urlpath pattern and app.
|
157
|
-
@
|
239
|
+
@all_endpoints.each(&block)
|
158
240
|
end
|
159
241
|
|
160
242
|
protected
|
@@ -185,170 +267,288 @@ module Rack
|
|
185
267
|
|
186
268
|
## Returns [301, {"Location"=>location, ...}, [...]]. Override in subclass if necessary.
|
187
269
|
def redirect_to(location)
|
270
|
+
#; [!9z57v] returns 301 and 'Location' header.
|
188
271
|
content = "Redirect to #{location}"
|
189
272
|
return [301, {"Content-Type"=>"text/plain", "Location"=>location}, [content]]
|
190
273
|
end
|
191
274
|
|
192
|
-
##
|
193
|
-
def
|
194
|
-
env['rack.urlpath_params']
|
275
|
+
## Stores urlpath parameter values into `env['rack.urlpath_params']`. Override if necessary.
|
276
|
+
def store_param_values(env, values)
|
277
|
+
#; [!94riv] stores urlpath param values into `env['rack.urlpath_params']`.
|
278
|
+
#; [!9he9h] env key can be changed by `env_key:` kwarg of 'JetRouter#initialize()'.
|
279
|
+
env[@env_key] = values if values
|
195
280
|
end
|
196
281
|
|
197
|
-
## Returns Hash object representing urlpath
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
282
|
+
## Returns Hash object representing urlpath parameter values. Override if necessary.
|
283
|
+
def build_param_values(names, values)
|
284
|
+
#; [!qxcis] when 'int_param:' kwarg is specified to constructor...
|
285
|
+
if (int_param_rexp = @int_param)
|
286
|
+
#; [!l6p84] converts urlpath pavam value into integer.
|
287
|
+
d = {}
|
288
|
+
for name, val in names.zip(values)
|
289
|
+
d[name] = int_param_rexp.match?(name) ? val.to_i : val
|
290
|
+
end
|
291
|
+
return d
|
292
|
+
#; [!vrbo5] else...
|
293
|
+
else
|
294
|
+
#; [!yc9n8] creates new Hash object from param names and values.
|
295
|
+
return Hash[names.zip(values)]
|
296
|
+
end
|
211
297
|
end
|
212
298
|
|
213
|
-
|
299
|
+
public
|
214
300
|
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
## ]
|
230
|
-
list = []
|
231
|
-
#
|
232
|
-
all = []
|
233
|
-
rexp_str = _compile_mapping(mapping, "", "") do |entry_point|
|
234
|
-
obj, urlpath_pat, urlpath_rexp, param_names = entry_point
|
235
|
-
all << [urlpath_pat, obj]
|
236
|
-
if urlpath_rexp
|
237
|
-
range = @enable_urlpath_param_range ? range_of_urlpath_param(urlpath_pat) : nil
|
238
|
-
list << [urlpath_rexp, param_names, obj, range]
|
239
|
-
else
|
240
|
-
dict[urlpath_pat] = obj
|
241
|
-
end
|
301
|
+
def normalize_method_mapping(dict) # called from Builder class
|
302
|
+
#; [!r7cmk] converts keys into string.
|
303
|
+
#; [!z9kww] allows 'ANY' as request method.
|
304
|
+
#; [!k7sme] raises error when unknown request method specified.
|
305
|
+
#; [!itfsd] returns new Hash object.
|
306
|
+
#; [!gd08f] if arg is an instance of Hash subclass, returns new instance of it.
|
307
|
+
request_methods = REQUEST_METHODS
|
308
|
+
#newdict = {}
|
309
|
+
newdict = dict.class.new
|
310
|
+
dict.each do |meth_sym, app|
|
311
|
+
meth_str = meth_sym.to_s
|
312
|
+
request_methods[meth_str] || meth_str == 'ANY' or
|
313
|
+
raise ArgumentError.new("#{meth_sym}: unknown request method.")
|
314
|
+
newdict[meth_str] = app
|
242
315
|
end
|
243
|
-
|
244
|
-
urlpath_rexp = Regexp.new("\\A#{rexp_str}\\z")
|
245
|
-
#; [!xzo7k] returns regexp, hash, and array.
|
246
|
-
return urlpath_rexp, dict, list, all
|
316
|
+
return newdict
|
247
317
|
end
|
248
318
|
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
319
|
+
## Returns regexp string of path parameter. Override if necessary.
|
320
|
+
def param2rexp(param) # called from Builder class
|
321
|
+
#; [!6sd9b] returns regexp string according to param name.
|
322
|
+
#; [!rfvk2] returns '\d+' if param name matched to int param regexp.
|
323
|
+
return (rexp = @int_param) && rexp.match?(param) ? '\d+' : '[^./?]+'
|
324
|
+
end
|
325
|
+
|
326
|
+
|
327
|
+
class Builder
|
328
|
+
|
329
|
+
def initialize(router, enable_range=true)
|
330
|
+
@router = router
|
331
|
+
@enable_range = enable_range
|
332
|
+
end
|
333
|
+
|
334
|
+
def traverse_mapping(mapping, &block)
|
335
|
+
_traverse_mapping(mapping, "", mapping.class, &block)
|
336
|
+
nil
|
337
|
+
end
|
338
|
+
|
339
|
+
private
|
340
|
+
|
341
|
+
def _traverse_mapping(mapping, base_path, mapping_class, &block)
|
342
|
+
#; [!9s3f0] supports both nested list mapping and nested dict mapping.
|
343
|
+
mapping.each do |sub_path, item|
|
344
|
+
full_path = base_path + sub_path
|
345
|
+
#; [!2ntnk] nested dict mapping can have subclass of Hash as handlers.
|
346
|
+
if item.class == mapping_class
|
347
|
+
#; [!dj0sh] traverses mapping recursively.
|
348
|
+
_traverse_mapping(item, full_path, mapping_class, &block)
|
349
|
+
else
|
350
|
+
#; [!j0pes] if item is a hash object, converts keys from symbol to string.
|
351
|
+
item = _normalize_method_mapping(item) if item.is_a?(Hash)
|
352
|
+
#; [!brhcs] yields block for each full path and handler.
|
353
|
+
yield full_path, item
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
def _normalize_method_mapping(dict)
|
359
|
+
return @router.normalize_method_mapping(dict)
|
360
|
+
end
|
361
|
+
|
362
|
+
public
|
363
|
+
|
364
|
+
def build_tree(entrypoint_pairs)
|
365
|
+
#; [!6oa05] builds nested hash object from mapping data.
|
366
|
+
tree = {} # tree is a nested dict
|
367
|
+
param_d = {}
|
368
|
+
entrypoint_pairs.each do |path, item|
|
369
|
+
d = tree
|
370
|
+
sb = ['\A']
|
371
|
+
pos = 0
|
372
|
+
params = []
|
373
|
+
#; [!uyupj] handles urlpath parameter such as ':id'.
|
374
|
+
#; [!j9cdy] handles optional urlpath parameter such as '(.:format)'.
|
375
|
+
path.scan(/:(\w+)|\((.*?)\)/) do
|
376
|
+
param = $1; optional = $2 # ex: $1=='id' or $2=='.:format'
|
377
|
+
m = Regexp.last_match()
|
378
|
+
str = path[pos, m.begin(0) - pos]
|
379
|
+
pos = m.end(0)
|
380
|
+
#; [!akkkx] converts urlpath param into regexp.
|
381
|
+
pat1, pat2 = _param_patterns(param, optional) do |param_|
|
382
|
+
param_.freeze
|
383
|
+
params << (param_d[param_] ||= param_)
|
384
|
+
end
|
385
|
+
#; [!po6o6] param regexp should be stored into nested dict as a Symbol.
|
386
|
+
d = _next_dict(d, str) unless str.empty?
|
387
|
+
d = (d[pat1.intern] ||= {}) # ex: pat1=='[^./?]+'
|
388
|
+
#; [!zoym3] urlpath string should be escaped.
|
389
|
+
sb << Regexp.escape(str) << pat2 # ex: pat2=='([^./?]+)'
|
261
390
|
end
|
262
|
-
#; [!
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
rexp_str << '(\z)'
|
268
|
-
entry_point = [obj, full_urlpath, full_urlpath_rexp, param_names]
|
269
|
-
#; [!l63vu] handles urlpath pattern as fixed when no urlpath params.
|
270
|
-
else # has no urlpath params
|
271
|
-
entry_point = [obj, full_urlpath, nil, nil]
|
391
|
+
#; [!o642c] remained string after param should be handled correctly.
|
392
|
+
str = pos == 0 ? path : path[pos..-1]
|
393
|
+
unless str.empty?
|
394
|
+
d = _next_dict(d, str)
|
395
|
+
sb << Regexp.escape(str) # ex: str=='.html'
|
272
396
|
end
|
273
|
-
|
397
|
+
sb << '\z'
|
398
|
+
#; [!kz8m7] range object should be included into tuple if only one param exist.
|
399
|
+
range = @enable_range ? _range_of_urlpath_param(path) : nil
|
400
|
+
#; [!c6xmp] tuple should be stored into nested dict with key 'nil'.
|
401
|
+
d[nil] = [Regexp.compile(sb.join()), params, item, range]
|
274
402
|
end
|
275
|
-
|
403
|
+
return tree
|
404
|
+
end
|
405
|
+
|
406
|
+
private
|
407
|
+
|
408
|
+
def _next_dict(d, str)
|
409
|
+
#; [!s1rzs] if new key exists in dict...
|
410
|
+
if d.key?(str)
|
411
|
+
#; [!io47b] just returns corresponding value and not change dict.
|
412
|
+
return d[str]
|
413
|
+
end
|
414
|
+
#; [!3ndpz] returns next dict.
|
415
|
+
d2 = nil # next dict
|
416
|
+
c = str[0]
|
417
|
+
found = false
|
418
|
+
d.keys.each do |key|
|
419
|
+
if found
|
420
|
+
#; [!5fh08] keeps order of keys in dict.
|
421
|
+
d[key] = d.delete(key)
|
422
|
+
#; [!4wdi7] ignores Symbol key (which represents regexp).
|
423
|
+
#; [!66sdb] ignores nil key (which represents leaf node).
|
424
|
+
elsif key.is_a?(String) && key[0] == c
|
425
|
+
found = true
|
426
|
+
prefix, rest1, rest2 = _common_prefix(key, str)
|
427
|
+
#; [!46o9b] if existing key is same as common prefix...
|
428
|
+
if rest1.empty?
|
429
|
+
#; [!4ypls] not replace existing key.
|
430
|
+
val = d[key]
|
431
|
+
d2 = _next_dict(val, rest2)
|
432
|
+
break
|
433
|
+
#; [!veq0q] if new key is same as common prefix...
|
434
|
+
elsif rest2.empty?
|
435
|
+
#; [!0tboh] replaces existing key with ney key.
|
436
|
+
val = d.delete(key)
|
437
|
+
d2 = {rest1 => val}
|
438
|
+
d[prefix] = d2
|
439
|
+
#; [!esszs] if common prefix is a part of exsting key and new key...
|
440
|
+
else
|
441
|
+
#; [!pesq0] replaces existing key with common prefix.
|
442
|
+
val = d.delete(key)
|
443
|
+
d2 = {}
|
444
|
+
d[prefix] = {rest1 => val, rest2 => d2}
|
445
|
+
end
|
446
|
+
end
|
447
|
+
end
|
448
|
+
#; [!viovl] if new key has no common prefix with existing keys...
|
449
|
+
unless found
|
450
|
+
#; [!i6smv] adds empty dict with new key.
|
451
|
+
d2 = {}
|
452
|
+
d[str] = d2
|
453
|
+
end
|
454
|
+
return d2
|
455
|
+
end
|
456
|
+
|
457
|
+
def _common_prefix(str1, str2) # ex: "/api/books/" and "/api/blog/"
|
458
|
+
#; [!86tsd] calculates common prefix of two strings.
|
459
|
+
#n = [str1.length, str2.length].min()
|
460
|
+
n1 = str1.length; n2 = str2.length
|
461
|
+
n = n1 < n2 ? n1 : n2
|
462
|
+
i = 0
|
463
|
+
while i < n && str1[i] == str2[i]
|
464
|
+
i += 1
|
465
|
+
end
|
466
|
+
#; [!1z2ii] returns common prefix and rest of strings.
|
467
|
+
prefix = str1[0...i] # ex: "/api/b"
|
468
|
+
rest1 = str1[i..-1] # ex: "ooks/"
|
469
|
+
rest2 = str2[i..-1] # ex: "log/"
|
470
|
+
return prefix, rest1, rest2
|
276
471
|
end
|
277
|
-
#; [!pv2au] deletes unnecessary urlpath regexp.
|
278
|
-
return nil if arr.empty?
|
279
|
-
#; [!bh9lo] deletes unnecessary grouping.
|
280
|
-
parent_urlpath_rexp_str, _ = compile_urlpath_pattern(parent_urlpath, false)
|
281
|
-
return "#{parent_urlpath_rexp_str}#{arr[0]}" if arr.length == 1
|
282
|
-
#; [!iza1g] adds grouping if necessary.
|
283
|
-
return "#{parent_urlpath_rexp_str}(?:#{arr.join('|')})"
|
284
|
-
end
|
285
472
|
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
#; [!
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
#; [!
|
299
|
-
if name
|
300
|
-
param_names << name
|
301
|
-
s << param_pat
|
302
|
-
#; [!4dcsa] converts '/index(.:format)' into '/index(?:\.([^./]+))?'.
|
473
|
+
def _param_patterns(param, optional, &callback)
|
474
|
+
#; [!j90mw] returns '[^./?]+' and '([^./?]+)' if param specified.
|
475
|
+
if param
|
476
|
+
optional == nil or raise "** internal error"
|
477
|
+
yield param
|
478
|
+
pat1 = _param2rexp(param) # ex: '[^./?]+'
|
479
|
+
pat2 = "(#{pat1})"
|
480
|
+
#; [!raic7] returns '(?:\.[^./?]+)?' and '(?:\.([^./?]+))?' if optional param is '(.:format)'.
|
481
|
+
elsif optional == ".:format"
|
482
|
+
yield "format"
|
483
|
+
pat1 = '(?:\.[^./?]+)?'
|
484
|
+
pat2 = '(?:\.([^./?]+))?'
|
485
|
+
#; [!69yj9] optional string can contains other params.
|
303
486
|
elsif optional
|
304
|
-
|
305
|
-
optional.scan(/(.*?)(?::(\w+))/) do |
|
306
|
-
|
307
|
-
|
487
|
+
sb = ['(?:']
|
488
|
+
optional.scan(/(.*?)(?::(\w+))/) do |str, param_|
|
489
|
+
pat = _param2rexp(param) # ex: pat == '[^./?]+'
|
490
|
+
sb << Regexp.escape(str) << "<<#{pat}>>" # ex: sb << '(?:\.<<[^./?]+>>)?'
|
491
|
+
yield param_
|
308
492
|
end
|
309
|
-
|
310
|
-
|
311
|
-
|
493
|
+
sb << Regexp.escape($' || optional)
|
494
|
+
sb << ')?'
|
495
|
+
s = sb.join()
|
496
|
+
pat1 = s.gsub('<<', '' ).gsub('>>', '' ) # ex: '(?:\.[^./?]+)?'
|
497
|
+
pat2 = s.gsub('<<', '(').gsub('>>', ')') # ex: '(?:\.([^./?]+))?'
|
312
498
|
else
|
313
|
-
raise "
|
499
|
+
raise "** internal error"
|
314
500
|
end
|
501
|
+
return pat1, pat2
|
315
502
|
end
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
return Regexp.escape(urlpath_pat), nil
|
320
|
-
else
|
321
|
-
s << Regexp.escape(urlpath_pat[pos..-1])
|
322
|
-
return s, param_names
|
503
|
+
|
504
|
+
def _param2rexp(param)
|
505
|
+
return @router.param2rexp(param) # ex: '[^./?]+'
|
323
506
|
end
|
324
|
-
end
|
325
507
|
|
326
|
-
|
327
|
-
#; [!syrdh] returns Range object when urlpath_pattern contains just one param.
|
328
|
-
#; [!skh4z] returns nil when urlpath_pattern contains more than two params.
|
329
|
-
#; [!acj5b] returns nil when urlpath_pattern contains no params.
|
330
|
-
rexp = /:\w+|\(.*?\)/
|
331
|
-
arr = urlpath_pattern.split(rexp, -1) # ex: ['/books/', '/edit']
|
332
|
-
return nil unless arr.length == 2
|
333
|
-
return (arr[0].length .. -(arr[1].length+1)) # ex: 7..-6 (Range object)
|
334
|
-
end
|
508
|
+
public
|
335
509
|
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
meth_str = meth.to_s
|
343
|
-
request_methods[meth_str] || meth_str == 'ANY' or
|
344
|
-
raise ArgumentError.new("#{meth.inspect}: unknown request method.")
|
345
|
-
newdict[meth_str] = app
|
510
|
+
def build_rexp(tree, &callback)
|
511
|
+
#; [!65yw6] converts nested dict into regexp.
|
512
|
+
sb = ['\A']
|
513
|
+
_build_rexp(tree, sb, &callback)
|
514
|
+
sb << '\z'
|
515
|
+
return Regexp.compile(sb.join())
|
346
516
|
end
|
517
|
+
|
518
|
+
private
|
519
|
+
|
520
|
+
def _build_rexp(nested_dict, sb, &b)
|
521
|
+
#; [!hs7vl] '(?:)' and '|' are added only if necessary.
|
522
|
+
sb << '(?:' if nested_dict.length > 1
|
523
|
+
nested_dict.each_with_index do |(k, v), i|
|
524
|
+
sb << '|' if i > 0
|
525
|
+
#; [!7v7yo] nil key means leaf node and yields block argument.
|
526
|
+
#; [!hda6m] string key should be escaped.
|
527
|
+
#; [!b9hxc] symbol key means regexp string.
|
528
|
+
case k
|
529
|
+
when nil ; sb << '(\z)' ; yield v
|
530
|
+
when String ; sb << Regexp.escape(k) ; _build_rexp(v, sb, &b)
|
531
|
+
when Symbol ; sb << k.to_s ; _build_rexp(v, sb, &b)
|
532
|
+
else ; raise "** internal error"
|
533
|
+
end
|
534
|
+
end
|
535
|
+
sb << ')' if nested_dict.length > 1
|
536
|
+
end
|
537
|
+
|
538
|
+
def _range_of_urlpath_param(urlpath_pattern) # ex: '/books/:id/edit'
|
539
|
+
#; [!93itq] returns nil if urlpath pattern includes optional parameters.
|
540
|
+
return nil if urlpath_pattern =~ /\(/
|
541
|
+
#; [!syrdh] returns Range object when urlpath_pattern contains just one param.
|
542
|
+
#; [!skh4z] returns nil when urlpath_pattern contains more than two params.
|
543
|
+
#; [!acj5b] returns nil when urlpath_pattern contains no params.
|
544
|
+
rexp = /:\w+/
|
545
|
+
arr = urlpath_pattern.split(rexp, -1) # ex: ['/books/', '/edit']
|
546
|
+
return nil unless arr.length == 2
|
547
|
+
return (arr[0].length .. -(arr[1].length+1)) # ex: 7..-6 (Range object)
|
548
|
+
end
|
549
|
+
|
347
550
|
end
|
348
551
|
|
349
|
-
#; [!haggu] contains available request methods.
|
350
|
-
REQUEST_METHODS = %w[GET POST PUT DELETE PATCH HEAD OPTIONS TRACE LINK UNLINK] \
|
351
|
-
.each_with_object({}) {|s, d| d[s] = s.intern }
|
352
552
|
|
353
553
|
end
|
354
554
|
|