rack-jet_router 1.3.0 → 1.4.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 +4 -4
- data/CHANGES.md +16 -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 +121 -62
- data/rack-jet_router.gemspec +1 -1
- data/test/builder_test.rb +136 -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,21 +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'
|
586
|
+
#; [!93itq] returns nil if urlpath pattern includes optional parameters.
|
587
|
+
return nil if urlpath_pattern =~ /\(/
|
539
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.
|
540
590
|
#; [!skh4z] returns nil when urlpath_pattern contains more than two params.
|
541
591
|
#; [!acj5b] returns nil when urlpath_pattern contains no params.
|
542
|
-
rexp =
|
543
|
-
arr = urlpath_pattern.split(rexp, -1)
|
544
|
-
|
545
|
-
|
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
|
546
605
|
end
|
547
606
|
|
548
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},
|
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
|
},
|
@@ -598,21 +672,42 @@ Oktest.scope do
|
|
598
672
|
|
599
673
|
topic '#range_of_urlpath_param()' do
|
600
674
|
|
675
|
+
spec "[!93itq] returns nil if urlpath pattern includes optional parameters." do
|
676
|
+
@builder.instance_exec(self) do |_|
|
677
|
+
r1 = _range_of_urlpath_param('/books(.:format)')
|
678
|
+
_.ok {r1} == nil
|
679
|
+
end
|
680
|
+
end
|
681
|
+
|
601
682
|
spec "[!syrdh] returns Range object when urlpath_pattern contains just one param." do
|
602
683
|
@builder.instance_exec(self) do |_|
|
603
|
-
|
604
|
-
_.ok {
|
605
|
-
_.ok {'/books/123'[
|
606
|
-
|
607
|
-
|
608
|
-
_.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"]
|
609
705
|
end
|
610
706
|
end
|
611
707
|
|
612
708
|
spec "[!skh4z] returns nil when urlpath_pattern contains more than two params." do
|
613
709
|
@builder.instance_exec(self) do |_|
|
614
|
-
_.ok {_range_of_urlpath_param('/books/:book_id/comments/:
|
615
|
-
_.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
|
616
711
|
end
|
617
712
|
end
|
618
713
|
|