rack-jet_router 1.3.1 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +10 -0
- data/README.md +80 -72
- data/bench/Gemfile +5 -4
- data/bench/Rakefile.rb +3 -3
- data/bench/bench.rb +330 -244
- data/lib/rack/jet_router.rb +119 -62
- data/rack-jet_router.gemspec +1 -1
- data/test/builder_test.rb +129 -41
- data/test/router_test.rb +49 -7
- data/test/shared.rb +2 -2
- metadata +2 -2
data/lib/rack/jet_router.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
###
|
5
|
-
### $Release: 1.
|
5
|
+
### $Release: 1.4.0 $
|
6
6
|
### $Copyright: copyright(c) 2015 kwatch@gmail.com $
|
7
7
|
### $License: MIT License $
|
8
8
|
###
|
@@ -56,22 +56,17 @@ module Rack
|
|
56
56
|
## status, headers, body = router.call(env)
|
57
57
|
##
|
58
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
59
|
## mapping = {
|
65
|
-
## "/" =>
|
60
|
+
## "/" => {GET: home_app}, # not {"GET"=>home_app}
|
66
61
|
## "/api" => {
|
67
|
-
## "/books" => {
|
68
|
-
## "" =>
|
69
|
-
## "/:id(.:format)" =>
|
70
|
-
## "/:book_id/comments/:comment_id" =>
|
62
|
+
## "/books" => { # not {"GET"=>..., "POST"=>...}
|
63
|
+
## "" => {GET: book_list_app, POST: book_create_app},
|
64
|
+
## "/:id(.:format)" => {GET: book_show_app, PUT: book_update_app},
|
65
|
+
## "/:book_id/comments/:comment_id" => {POST: comment_create_app},
|
71
66
|
## },
|
72
67
|
## },
|
73
68
|
## "/admin" => {
|
74
|
-
## "/books" =>
|
69
|
+
## "/books" => {ANY: admin_books_app}, # not {"ANY"=>...}
|
75
70
|
## },
|
76
71
|
## }
|
77
72
|
## router = Rack::JetRouter.new(mapping)
|
@@ -81,7 +76,7 @@ module Rack
|
|
81
76
|
##
|
82
77
|
class JetRouter
|
83
78
|
|
84
|
-
RELEASE = '$Release: 1.
|
79
|
+
RELEASE = '$Release: 1.4.0 $'.split()[1]
|
85
80
|
|
86
81
|
#; [!haggu] contains available request methods.
|
87
82
|
REQUEST_METHODS = %w[GET POST PUT DELETE PATCH HEAD OPTIONS TRACE LINK UNLINK] \
|
@@ -136,15 +131,33 @@ module Rack
|
|
136
131
|
#
|
137
132
|
#; [!x2l32] gathers all endpoints.
|
138
133
|
builder = Builder.new(self, _enable_range)
|
134
|
+
param_rexp = /[:*]\w+|\(.*?\)/
|
135
|
+
tmplist = []
|
139
136
|
builder.traverse_mapping(mapping) do |path, item|
|
140
137
|
@all_endpoints << [path, item]
|
138
|
+
#; [!l63vu] handles urlpath pattern as fixed when no urlpath params.
|
139
|
+
if path !~ param_rexp
|
140
|
+
@fixed_endpoints[path] = item
|
141
|
+
#; [!ec0av] treats '/foo(.html|.json)' as three fixed urlpaths.
|
142
|
+
#; [!ylyi0] stores '/foo' as fixed path when path pattern is '/foo(.:format)'.
|
143
|
+
elsif path =~ /\A([^:*\(\)]*)\(([^\(\)]+)\)\z/
|
144
|
+
@fixed_endpoints[$1] = item unless $1.empty?
|
145
|
+
arr = []
|
146
|
+
$2.split('|').each do |s|
|
147
|
+
next if s.empty?
|
148
|
+
if s.include?(':')
|
149
|
+
arr << s
|
150
|
+
else
|
151
|
+
@fixed_endpoints[$1 + s] = item
|
152
|
+
end
|
153
|
+
end
|
154
|
+
tmplist << ["#{$1}(#{arr.join('|')})", item] unless arr.empty?
|
155
|
+
else
|
156
|
+
tmplist << [path, item]
|
157
|
+
end
|
141
158
|
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
159
|
#; [!saa1a] compiles compound urlpath regexp.
|
147
|
-
tree = builder.build_tree(
|
160
|
+
tree = builder.build_tree(tmplist)
|
148
161
|
@urlpath_rexp = builder.build_rexp(tree) do |tuple|
|
149
162
|
#; [!f1d7s] builds variable endpoint list.
|
150
163
|
@variable_endpoints << tuple
|
@@ -213,11 +226,12 @@ module Rack
|
|
213
226
|
index = m.captures.index('')
|
214
227
|
return nil unless index
|
215
228
|
#; [!ijqws] returns mapped object and urlpath parameter values when urlpath found.
|
216
|
-
full_urlpath_rexp, param_names, obj, range = @variable_endpoints[index]
|
229
|
+
full_urlpath_rexp, param_names, obj, range, sep = @variable_endpoints[index]
|
217
230
|
if range
|
218
231
|
## "/books/123"[7..-1] is faster than /\A\/books\/(\d+)\z/.match("/books/123")[1]
|
219
232
|
str = req_path[range]
|
220
|
-
|
233
|
+
## `"/a/1/b/2"[3..-1].split('/b/')` is faster than `%r!\A/a/(\d+)/b/(\d+)\z!.match("/a/1/b/2").captures`
|
234
|
+
values = sep ? str.split(sep) : [str]
|
221
235
|
else
|
222
236
|
m = full_urlpath_rexp.match(req_path)
|
223
237
|
values = m.captures
|
@@ -343,7 +357,7 @@ module Rack
|
|
343
357
|
mapping.each do |sub_path, item|
|
344
358
|
full_path = base_path + sub_path
|
345
359
|
#; [!2ntnk] nested dict mapping can have subclass of Hash as handlers.
|
346
|
-
if item
|
360
|
+
if _submapping?(item, mapping_class)
|
347
361
|
#; [!dj0sh] traverses mapping recursively.
|
348
362
|
_traverse_mapping(item, full_path, mapping_class, &block)
|
349
363
|
else
|
@@ -355,6 +369,13 @@ module Rack
|
|
355
369
|
end
|
356
370
|
end
|
357
371
|
|
372
|
+
def _submapping?(item, mapping_class)
|
373
|
+
#; [!qlp3f] returns false if item is like '{GET: ...}'.
|
374
|
+
return false if item.is_a?(Hash) && item.keys.all? {|k| k.is_a?(Symbol) }
|
375
|
+
#; [!1pmtl] returns true only if item class is same as mapping_class.
|
376
|
+
return item.class == mapping_class
|
377
|
+
end
|
378
|
+
|
358
379
|
def _normalize_method_mapping(dict)
|
359
380
|
return @router.normalize_method_mapping(dict)
|
360
381
|
end
|
@@ -364,47 +385,66 @@ module Rack
|
|
364
385
|
def build_tree(entrypoint_pairs)
|
365
386
|
#; [!6oa05] builds nested hash object from mapping data.
|
366
387
|
tree = {} # tree is a nested dict
|
367
|
-
|
388
|
+
pnames_d = {}
|
368
389
|
entrypoint_pairs.each do |path, item|
|
369
390
|
d = tree
|
370
391
|
sb = ['\A']
|
371
|
-
|
372
|
-
|
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.
|
392
|
+
pnames = _parse_path(path, sb, pnames_d) do |str, pat1|
|
393
|
+
#; [!po6o6] param regexp should be stored into nested dict as a Regexp.
|
386
394
|
d = _next_dict(d, str) unless str.empty?
|
387
|
-
d = (d[pat1.
|
388
|
-
#; [!zoym3] urlpath string should be escaped.
|
389
|
-
sb << Regexp.escape(str) << pat2 # ex: pat2=='([^./?]+)'
|
390
|
-
end
|
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'
|
395
|
+
d = (d[Regexp.compile(pat1).freeze] ||= {}) if pat1 # ex: pat1=='[^./?]+'
|
396
396
|
end
|
397
397
|
sb << '\z'
|
398
|
+
rexp = Regexp.compile(sb.join())
|
398
399
|
#; [!kz8m7] range object should be included into tuple if only one param exist.
|
399
|
-
|
400
|
+
if @enable_range
|
401
|
+
range, separator = _range_of_urlpath_param(path)
|
402
|
+
else
|
403
|
+
range = separator = nil
|
404
|
+
end
|
400
405
|
#; [!c6xmp] tuple should be stored into nested dict with key 'nil'.
|
401
|
-
d[nil] = [
|
406
|
+
d[nil] = [rexp, pnames, item, range, separator]
|
402
407
|
end
|
403
408
|
return tree
|
404
409
|
end
|
405
410
|
|
406
411
|
private
|
407
412
|
|
413
|
+
def _parse_path(path, sb, pnames_d, &b)
|
414
|
+
pos = 0
|
415
|
+
pnames = []
|
416
|
+
#; [!uyupj] handles urlpath parameter such as ':id'.
|
417
|
+
#; [!k7oxt] handles urlpath parameter such as ':filepath'.
|
418
|
+
#; [!j9cdy] handles optional urlpath parameter such as '(.:format)'.
|
419
|
+
path.scan(/([:*]\w+)|\((.*?)\)/) do
|
420
|
+
param = $1; optional = $2 # ex: $1=='id' or $2=='.:format'
|
421
|
+
m = Regexp.last_match()
|
422
|
+
str = path[pos, m.begin(0) - pos]
|
423
|
+
pos = m.end(0)
|
424
|
+
#; [!akkkx] converts urlpath param into regexp.
|
425
|
+
#; [!lwgt6] handles '|' (OR) pattern in '()' such as '(.html|.json)'.
|
426
|
+
pat1, pat2 = _param_patterns(param, optional) do |pname|
|
427
|
+
pname.freeze
|
428
|
+
pnames << (pnames_d[pname] ||= pname)
|
429
|
+
end
|
430
|
+
yield str, pat1
|
431
|
+
#; [!zoym3] urlpath string should be escaped.
|
432
|
+
sb << Regexp.escape(str) << pat2 # ex: pat2=='([^./?]+)'
|
433
|
+
#; [!2axyy] raises error if '*path' param is not at end of path.
|
434
|
+
if param =~ /\A\*/
|
435
|
+
pos == path.length or
|
436
|
+
raise ArgumentError.new("#{path}: Invalid path parameter ('#{param}' should be at end of the path).")
|
437
|
+
end
|
438
|
+
end
|
439
|
+
#; [!o642c] remained string after param should be handled correctly.
|
440
|
+
str = pos == 0 ? path : path[pos..-1]
|
441
|
+
unless str.empty?
|
442
|
+
yield str, nil
|
443
|
+
sb << Regexp.escape(str) # ex: str=='.html'
|
444
|
+
end
|
445
|
+
return pnames
|
446
|
+
end
|
447
|
+
|
408
448
|
def _next_dict(d, str)
|
409
449
|
#; [!s1rzs] if new key exists in dict...
|
410
450
|
if d.key?(str)
|
@@ -419,7 +459,7 @@ module Rack
|
|
419
459
|
if found
|
420
460
|
#; [!5fh08] keeps order of keys in dict.
|
421
461
|
d[key] = d.delete(key)
|
422
|
-
#; [!4wdi7] ignores
|
462
|
+
#; [!4wdi7] ignores non-string key (which represents regexp).
|
423
463
|
#; [!66sdb] ignores nil key (which represents leaf node).
|
424
464
|
elsif key.is_a?(String) && key[0] == c
|
425
465
|
found = true
|
@@ -472,10 +512,13 @@ module Rack
|
|
472
512
|
|
473
513
|
def _param_patterns(param, optional, &callback)
|
474
514
|
#; [!j90mw] returns '[^./?]+' and '([^./?]+)' if param specified.
|
515
|
+
#; [!jbvyb] returns '.*' and '(.*)' if param is like '*filepath'.
|
475
516
|
if param
|
476
517
|
optional == nil or raise "** internal error"
|
477
|
-
|
478
|
-
|
518
|
+
param =~ /\A([:*])(\w+)\z/ or raise "** internal error"
|
519
|
+
pname = $2
|
520
|
+
yield pname
|
521
|
+
pat1 = $1 == '*' ? '.*' : _param2rexp(pname) # ex: '[^./?]+'
|
479
522
|
pat2 = "(#{pat1})"
|
480
523
|
#; [!raic7] returns '(?:\.[^./?]+)?' and '(?:\.([^./?]+))?' if optional param is '(.:format)'.
|
481
524
|
elsif optional == ".:format"
|
@@ -485,12 +528,16 @@ module Rack
|
|
485
528
|
#; [!69yj9] optional string can contains other params.
|
486
529
|
elsif optional
|
487
530
|
sb = ['(?:']
|
488
|
-
optional
|
489
|
-
|
490
|
-
sb <<
|
491
|
-
|
531
|
+
#; [!oh9c6] optional string can have '|' (OR).
|
532
|
+
optional.split('|').each_with_index do |string, i|
|
533
|
+
sb << '|' if i > 0
|
534
|
+
string.scan(/(.*?)(?::(\w+))/) do |str, pname|
|
535
|
+
pat = _param2rexp(pname) # ex: pat == '[^./?]+'
|
536
|
+
sb << Regexp.escape(str) << "<<#{pat}>>" # ex: sb << '(?:\.<<[^./?]+>>)?'
|
537
|
+
yield pname
|
538
|
+
end
|
539
|
+
sb << Regexp.escape($' || string)
|
492
540
|
end
|
493
|
-
sb << Regexp.escape($' || optional)
|
494
541
|
sb << ')?'
|
495
542
|
s = sb.join()
|
496
543
|
pat1 = s.gsub('<<', '' ).gsub('>>', '' ) # ex: '(?:\.[^./?]+)?'
|
@@ -528,23 +575,33 @@ module Rack
|
|
528
575
|
case k
|
529
576
|
when nil ; sb << '(\z)' ; yield v
|
530
577
|
when String ; sb << Regexp.escape(k) ; _build_rexp(v, sb, &b)
|
531
|
-
when
|
578
|
+
when Regexp ; sb << k.source ; _build_rexp(v, sb, &b)
|
532
579
|
else ; raise "** internal error"
|
533
580
|
end
|
534
581
|
end
|
535
582
|
sb << ')' if nested_dict.length > 1
|
536
583
|
end
|
537
584
|
|
538
|
-
def _range_of_urlpath_param(urlpath_pattern) # ex: '/books/:id/
|
585
|
+
def _range_of_urlpath_param(urlpath_pattern) # ex: '/books/:id/comments/:comment_id.json'
|
539
586
|
#; [!93itq] returns nil if urlpath pattern includes optional parameters.
|
540
587
|
return nil if urlpath_pattern =~ /\(/
|
541
588
|
#; [!syrdh] returns Range object when urlpath_pattern contains just one param.
|
589
|
+
#; [!elsdx] returns Range and separator string when urlpath_pattern contains two params.
|
542
590
|
#; [!skh4z] returns nil when urlpath_pattern contains more than two params.
|
543
591
|
#; [!acj5b] returns nil when urlpath_pattern contains no params.
|
544
|
-
rexp =
|
545
|
-
arr = urlpath_pattern.split(rexp, -1)
|
546
|
-
|
547
|
-
|
592
|
+
rexp = /[:*]\w+/
|
593
|
+
arr = urlpath_pattern.split(rexp, -1) # ex: ['/books/', '/comments/', '.json']
|
594
|
+
case arr.length
|
595
|
+
when 2 # ex: arr == ['/books/', '.json']
|
596
|
+
range = arr[0].length .. -(arr[1].length+1)
|
597
|
+
return range, nil # ex: (7..-6), nil
|
598
|
+
when 3 # ex: arr == ['/books/', '/comments/', '.json']
|
599
|
+
return nil if arr[1].empty?
|
600
|
+
range = arr[0].length .. -(arr[2].length+1)
|
601
|
+
return range, arr[1] # ex: (7..-6), '/comments/'
|
602
|
+
else
|
603
|
+
return nil
|
604
|
+
end
|
548
605
|
end
|
549
606
|
|
550
607
|
end
|
data/rack-jet_router.gemspec
CHANGED
data/test/builder_test.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
|
3
3
|
###
|
4
|
-
### $Release: 1.
|
4
|
+
### $Release: 1.4.0 $
|
5
5
|
### $Copyright: copyright(c) 2015 kwatch@gmail.com $
|
6
6
|
### $License: MIT License $
|
7
7
|
###
|
@@ -25,6 +25,8 @@ Oktest.scope do
|
|
25
25
|
comment_create_api = proc {|env| [200, {}, ["comment_create_api"]]}
|
26
26
|
comment_update_api = proc {|env| [200, {}, ["comment_update_api"]]}
|
27
27
|
#
|
28
|
+
staticfile_api = proc {|env| [200, {}, ["staticfile_api"]]}
|
29
|
+
#
|
28
30
|
|
29
31
|
def _find(d, k)
|
30
32
|
return d.key?(k) ? d[k] : _find(d.to_a[0][1], k)
|
@@ -49,21 +51,21 @@ Oktest.scope do
|
|
49
51
|
id = '[^./?]+'
|
50
52
|
ok {dict} == {
|
51
53
|
"/api/books/" => {
|
52
|
-
|
54
|
+
/[^.\/?]+/ => {
|
53
55
|
nil => [/\A\/api\/books\/(#{id})\z/,
|
54
|
-
["id"], book_show_api, 11..-1],
|
56
|
+
["id"], book_show_api, 11..-1, nil],
|
55
57
|
"/" => {
|
56
58
|
"edit" => {
|
57
59
|
nil => [/\A\/api\/books\/(#{id})\/edit\z/,
|
58
|
-
["id"], book_edit_api, 11..-6],
|
60
|
+
["id"], book_edit_api, 11..-6, nil],
|
59
61
|
},
|
60
62
|
"comments" => {
|
61
63
|
nil => [/\A\/api\/books\/(#{id})\/comments\z/,
|
62
|
-
["book_id"], comment_create_api, 11..-10],
|
64
|
+
["book_id"], comment_create_api, 11..-10, nil],
|
63
65
|
"/" => {
|
64
|
-
|
66
|
+
/[^.\/?]+/ => {
|
65
67
|
nil => [/\A\/api\/books\/(#{id})\/comments\/(#{id})\z/,
|
66
|
-
["book_id", "comment_id"], comment_update_api,
|
68
|
+
["book_id", "comment_id"], comment_update_api, (11..-1), '/comments/'],
|
67
69
|
},
|
68
70
|
},
|
69
71
|
},
|
@@ -82,14 +84,14 @@ Oktest.scope do
|
|
82
84
|
id = '[^./?]+'
|
83
85
|
ok {dict} == {
|
84
86
|
"/api/books/" => {
|
85
|
-
|
87
|
+
/[^.\/?]+/ => {
|
86
88
|
"/comments" => {
|
87
89
|
nil => [%r`\A/api/books/(#{id})/comments\z`,
|
88
|
-
["book_id"], {"POST"=>comment_create_api}, (11..-10)],
|
90
|
+
["book_id"], {"POST"=>comment_create_api}, (11..-10), nil],
|
89
91
|
"/" => {
|
90
|
-
|
92
|
+
/[^.\/?]+/ => {
|
91
93
|
nil => [%r`\A/api/books/(#{id})/comments/(#{id})\z`,
|
92
|
-
["book_id", "id"], {"PUT"=>comment_update_api},
|
94
|
+
["book_id", "id"], {"PUT"=>comment_update_api}, (11..-1), '/comments/'],
|
93
95
|
},
|
94
96
|
},
|
95
97
|
},
|
@@ -98,6 +100,20 @@ Oktest.scope do
|
|
98
100
|
}
|
99
101
|
end
|
100
102
|
|
103
|
+
spec "[!k7oxt] handles urlpath parameter such as ':filepath'." do
|
104
|
+
endpoint_pairs = [
|
105
|
+
["/static/*filepath" , {"GET"=>staticfile_api}],
|
106
|
+
]
|
107
|
+
dict = @builder.build_tree(endpoint_pairs)
|
108
|
+
ok {dict} == {
|
109
|
+
"/static/" => {
|
110
|
+
/.*/ => {
|
111
|
+
nil => [/\A\/static\/(.*)\z/, ["filepath"], {"GET"=>staticfile_api}, (8..-1), nil],
|
112
|
+
},
|
113
|
+
},
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
101
117
|
spec "[!j9cdy] handles optional urlpath parameter such as '(.:format)'." do
|
102
118
|
endpoint_pairs = [
|
103
119
|
["/api/books(.:format)" , {"GET"=>book_list_api}],
|
@@ -107,14 +123,14 @@ Oktest.scope do
|
|
107
123
|
id = '[^./?]+'
|
108
124
|
ok {dict} == {
|
109
125
|
"/api/books" => {
|
110
|
-
|
126
|
+
/(?:\.[^.\/?]+)?/ => {
|
111
127
|
nil => [%r`\A/api/books(?:\.(#{id}))?\z`,
|
112
|
-
["format"], {"GET"=>book_list_api}, nil]},
|
128
|
+
["format"], {"GET"=>book_list_api}, nil, nil]},
|
113
129
|
"/" => {
|
114
|
-
|
115
|
-
|
130
|
+
/[^.\/?]+/ => {
|
131
|
+
/(?:\.[^.\/?]+)?/ => {
|
116
132
|
nil => [%r`\A/api/books/(#{id})(?:\.(#{id}))?\z`,
|
117
|
-
["id", "format"], {"GET"=>book_show_api}, nil],
|
133
|
+
["id", "format"], {"GET"=>book_show_api}, nil, nil],
|
118
134
|
},
|
119
135
|
},
|
120
136
|
},
|
@@ -132,12 +148,22 @@ Oktest.scope do
|
|
132
148
|
ok {tuple[0]} == %r`\A/api/books/(#{id})\z`
|
133
149
|
end
|
134
150
|
|
135
|
-
spec "[!
|
151
|
+
spec "[!lwgt6] handles '|' (OR) pattern in '()' such as '(.html|.json)'." do
|
152
|
+
endpoint_pairs = [
|
153
|
+
["/api/books/:id(.html|.json)", book_show_api],
|
154
|
+
]
|
155
|
+
dict = @builder.build_tree(endpoint_pairs)
|
156
|
+
tuple = _find(dict, nil)
|
157
|
+
id = '[^./?]+'
|
158
|
+
ok {tuple[0]} == %r`\A/api/books/(#{id})(?:\.html|\.json)?\z`
|
159
|
+
end
|
160
|
+
|
161
|
+
spec "[!po6o6] param regexp should be stored into nested dict as a Regexp." do
|
136
162
|
endpoint_pairs = [
|
137
163
|
["/api/books/:id", book_show_api],
|
138
164
|
]
|
139
165
|
dict = @builder.build_tree(endpoint_pairs)
|
140
|
-
ok {dict["/api/books/"].keys()} == [
|
166
|
+
ok {dict["/api/books/"].keys()} == [/[^.\/?]+/]
|
141
167
|
end
|
142
168
|
|
143
169
|
spec "[!zoym3] urlpath string should be escaped." do
|
@@ -150,6 +176,15 @@ Oktest.scope do
|
|
150
176
|
ok {tuple[0]} == %r`\A/api/books\.dir\.tmp/(#{id})\.tar\.gz\z`
|
151
177
|
end
|
152
178
|
|
179
|
+
spec "[!2axyy] raises error if '*path' param is not at end of path." do
|
180
|
+
endpoint_pairs = [
|
181
|
+
["/static/*filepath.css", staticfile_api],
|
182
|
+
]
|
183
|
+
pr = proc { @builder.build_tree(endpoint_pairs) }
|
184
|
+
ok {pr}.raise?(ArgumentError,
|
185
|
+
"/static/*filepath.css: Invalid path parameter ('*filepath' should be at end of the path).")
|
186
|
+
end
|
187
|
+
|
153
188
|
spec "[!o642c] remained string after param should be handled correctly." do
|
154
189
|
endpoint_pairs = [
|
155
190
|
["/api/books/:id(.:format)(.gz)", book_show_api],
|
@@ -168,7 +203,7 @@ Oktest.scope do
|
|
168
203
|
tuple = _find(dict, nil)
|
169
204
|
id = '[^./?]+'
|
170
205
|
ok {tuple} == [%r`\A/api/books/(#{id})\.json\z`,
|
171
|
-
["id"], book_show_api, (11..-6)]
|
206
|
+
["id"], book_show_api, (11..-6), nil]
|
172
207
|
end
|
173
208
|
|
174
209
|
spec "[!c6xmp] tuple should be stored into nested dict with key 'nil'." do
|
@@ -179,10 +214,10 @@ Oktest.scope do
|
|
179
214
|
id = '[^./?]+'
|
180
215
|
ok {dict} == {
|
181
216
|
"/api/books/" => {
|
182
|
-
|
217
|
+
/[^.\/?]+/ => {
|
183
218
|
".json" => {
|
184
219
|
nil => [%r`\A/api/books/(#{id})\.json\z`,
|
185
|
-
["id"], book_show_api, (11..-6)],
|
220
|
+
["id"], book_show_api, (11..-6), nil],
|
186
221
|
},
|
187
222
|
},
|
188
223
|
},
|
@@ -324,6 +359,29 @@ Oktest.scope do
|
|
324
359
|
end
|
325
360
|
|
326
361
|
|
362
|
+
topic '#_submapping?()' do
|
363
|
+
|
364
|
+
spec "[!qlp3f] returns false if item is like '{GET: ...}'." do
|
365
|
+
x = @builder.instance_eval { _submapping?({GET: 1, POST: 2}, Hash) }
|
366
|
+
ok {x} == false
|
367
|
+
x = @builder.instance_eval { _submapping?({"GET"=>1}, Hash) }
|
368
|
+
ok {x} == true
|
369
|
+
end
|
370
|
+
|
371
|
+
spec "[!1pmtl] returns true only if item class is same as mapping_class." do
|
372
|
+
x = @builder.instance_eval { _submapping?({"GET"=>1}, Hash) }
|
373
|
+
ok {x} == true
|
374
|
+
x = @builder.instance_eval { _submapping?(["GET", 1], Array) }
|
375
|
+
ok {x} == true
|
376
|
+
x = @builder.instance_eval { _submapping?({"GET"=>1}, Array) }
|
377
|
+
ok {x} == false
|
378
|
+
x = @builder.instance_eval { _submapping?(Map("GET"=>1), Array) }
|
379
|
+
ok {x} == false
|
380
|
+
end
|
381
|
+
|
382
|
+
end
|
383
|
+
|
384
|
+
|
327
385
|
topic '#_next_dict()' do
|
328
386
|
|
329
387
|
case_when "[!s1rzs] if new key exists in dict..." do
|
@@ -358,13 +416,13 @@ Oktest.scope do
|
|
358
416
|
ok {dict.keys} == ["aa1", "bb", "cc1"]
|
359
417
|
end
|
360
418
|
|
361
|
-
spec "[!4wdi7] ignores
|
362
|
-
dict = {"aa1" => {"aa2"=>10},
|
419
|
+
spec "[!4wdi7] ignores non-string key (which represents regexp)." do
|
420
|
+
dict = {"aa1" => {"aa2"=>10}, /bb1/ => {"bb2"=>20}}
|
363
421
|
d2 = @builder.instance_eval { _next_dict(dict, "bb1") }
|
364
422
|
ok {d2} == {}
|
365
423
|
ok {dict} == {
|
366
424
|
"aa1" => {"aa2"=>10},
|
367
|
-
|
425
|
+
/bb1/ => {"bb2"=>20},
|
368
426
|
"bb1" => {},
|
369
427
|
}
|
370
428
|
ok {dict["bb1"]}.same?(d2)
|
@@ -477,12 +535,20 @@ Oktest.scope do
|
|
477
535
|
|
478
536
|
spec "[!j90mw] returns '[^./?]+' and '([^./?]+)' if param specified." do
|
479
537
|
x = nil
|
480
|
-
s1, s2 = @builder.instance_eval { _param_patterns("id", nil) {|a| x = a } }
|
538
|
+
s1, s2 = @builder.instance_eval { _param_patterns(":id", nil) {|a| x = a } }
|
481
539
|
ok {s1} == '[^./?]+'
|
482
540
|
ok {s2} == '([^./?]+)'
|
483
541
|
ok {x} == "id"
|
484
542
|
end
|
485
543
|
|
544
|
+
spec "[!jbvyb] returns '.*' and '(.*)' if param is like '*filepath'." do
|
545
|
+
x = nil
|
546
|
+
s1, s2 = @builder.instance_eval { _param_patterns("*filepath", nil) {|a| x = a } }
|
547
|
+
ok {s1} == '.*'
|
548
|
+
ok {s2} == '(.*)'
|
549
|
+
ok {x} == "filepath"
|
550
|
+
end
|
551
|
+
|
486
552
|
spec "[!raic7] returns '(?:\.[^./?]+)?' and '(?:\.([^./?]+))?' if optional param is '(.:format)'." do
|
487
553
|
x = nil
|
488
554
|
s1, s2 = @builder.instance_eval { _param_patterns(nil, ".:format") {|a| x = a } }
|
@@ -499,6 +565,14 @@ Oktest.scope do
|
|
499
565
|
ok {arr} == ["yr", "mo", "dy"]
|
500
566
|
end
|
501
567
|
|
568
|
+
spec "[!oh9c6] optional string can have '|' (OR)." do
|
569
|
+
arr = []
|
570
|
+
s1, s2 = @builder.instance_eval { _param_patterns(nil, ".html|.json") {|a| arr << a } }
|
571
|
+
ok {s1} == '(?:\.html|\.json)?'
|
572
|
+
ok {s2} == '(?:\.html|\.json)?'
|
573
|
+
ok {arr} == []
|
574
|
+
end
|
575
|
+
|
502
576
|
end
|
503
577
|
|
504
578
|
|
@@ -507,12 +581,12 @@ Oktest.scope do
|
|
507
581
|
spec "[!65yw6] converts nested dict into regexp." do
|
508
582
|
dict = {
|
509
583
|
"/api/books/" => {
|
510
|
-
|
584
|
+
/[^.\/?]+/ => {
|
511
585
|
nil => [],
|
512
586
|
"/comments" => {
|
513
587
|
nil => [],
|
514
588
|
"/" => {
|
515
|
-
|
589
|
+
/[^.\/?]+/ => {
|
516
590
|
nil => [],
|
517
591
|
},
|
518
592
|
},
|
@@ -528,10 +602,10 @@ Oktest.scope do
|
|
528
602
|
spec "[!hs7vl] '(?:)' and '|' are added only if necessary." do
|
529
603
|
dict = {
|
530
604
|
"/api/books/" => {
|
531
|
-
|
605
|
+
/[^.\/?]+/ => {
|
532
606
|
"/comments" => {
|
533
607
|
"/" => {
|
534
|
-
|
608
|
+
/[^.\/?]+/ => {
|
535
609
|
nil => [],
|
536
610
|
},
|
537
611
|
},
|
@@ -547,11 +621,11 @@ Oktest.scope do
|
|
547
621
|
spec "[!7v7yo] nil key means leaf node and yields block argument." do
|
548
622
|
dict = {
|
549
623
|
"/api/books/" => {
|
550
|
-
|
624
|
+
/[^.\/?]+/ => {
|
551
625
|
nil => [9820],
|
552
626
|
"/comments" => {
|
553
627
|
"/" => {
|
554
|
-
|
628
|
+
/[^.\/?]+/ => {
|
555
629
|
nil => [1549],
|
556
630
|
},
|
557
631
|
},
|
@@ -569,7 +643,7 @@ Oktest.scope do
|
|
569
643
|
spec "[!hda6m] string key should be escaped." do
|
570
644
|
dict = {
|
571
645
|
"/api/books/" => {
|
572
|
-
|
646
|
+
/[^.\/?]+/ => {
|
573
647
|
".json" => {
|
574
648
|
nil => [],
|
575
649
|
}
|
@@ -584,7 +658,7 @@ Oktest.scope do
|
|
584
658
|
spec "[!b9hxc] symbol key means regexp string." do
|
585
659
|
dict = {
|
586
660
|
"/api/books/" => {
|
587
|
-
|
661
|
+
/\d+/ => {
|
588
662
|
nil => [],
|
589
663
|
},
|
590
664
|
},
|
@@ -607,19 +681,33 @@ Oktest.scope do
|
|
607
681
|
|
608
682
|
spec "[!syrdh] returns Range object when urlpath_pattern contains just one param." do
|
609
683
|
@builder.instance_exec(self) do |_|
|
610
|
-
|
611
|
-
_.ok {
|
612
|
-
_.ok {'/books/123'[
|
613
|
-
|
614
|
-
|
615
|
-
_.ok {
|
684
|
+
t = _range_of_urlpath_param('/books/:id')
|
685
|
+
_.ok {t} == [(7..-1), nil]
|
686
|
+
_.ok {'/books/123'[t[0]]} == '123'
|
687
|
+
#
|
688
|
+
t = _range_of_urlpath_param('/books/:id.html')
|
689
|
+
_.ok {t} == [(7..-6), nil]
|
690
|
+
_.ok {'/books/4567.html'[t[0]]} == '4567'
|
691
|
+
end
|
692
|
+
end
|
693
|
+
|
694
|
+
spec "[!elsdx] returns Range and separator string when urlpath_pattern contains two params." do
|
695
|
+
@builder.instance_exec(self) do |_|
|
696
|
+
t = _range_of_urlpath_param('/books/:id/comments/:comment_id.json')
|
697
|
+
_.ok {t} == [(7..-6), '/comments/']
|
698
|
+
_.ok {'/books/123/comments/456.json'[t[0]]} == "123/comments/456"
|
699
|
+
_.ok {'/books/123/comments/456.json'[t[0]].split(t[1])} == ["123", "456"]
|
700
|
+
#
|
701
|
+
t = _range_of_urlpath_param('/books/:id/comments/:comment_id')
|
702
|
+
_.ok {t} == [(7..-1), '/comments/']
|
703
|
+
_.ok {'/books/123/comments/456'[t[0]]} == "123/comments/456"
|
704
|
+
_.ok {'/books/123/comments/456'[t[0]].split(t[1])} == ["123", "456"]
|
616
705
|
end
|
617
706
|
end
|
618
707
|
|
619
708
|
spec "[!skh4z] returns nil when urlpath_pattern contains more than two params." do
|
620
709
|
@builder.instance_exec(self) do |_|
|
621
|
-
_.ok {_range_of_urlpath_param('/books/:book_id/comments/:
|
622
|
-
_.ok {_range_of_urlpath_param('/books/:id(:format)')} == nil
|
710
|
+
_.ok {_range_of_urlpath_param('/books/:book_id/comments/:c_id/foo/:foo_id')} == nil
|
623
711
|
end
|
624
712
|
end
|
625
713
|
|