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.
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  ###
5
- ### $Release: 1.3.0 $
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
- ## "/" => Map(GET: home_app),
60
+ ## "/" => {GET: home_app}, # not {"GET"=>home_app}
66
61
  ## "/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),
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" => Map(ANY: admin_books_app),
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.3.0 $'.split()[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(pairs1)
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
- values = [str]
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.class == mapping_class
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
- param_d = {}
388
+ pnames_d = {}
368
389
  entrypoint_pairs.each do |path, item|
369
390
  d = tree
370
391
  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.
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.intern] ||= {}) # ex: 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
- range = @enable_range ? _range_of_urlpath_param(path) : nil
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] = [Regexp.compile(sb.join()), params, item, range]
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 Symbol key (which represents regexp).
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
- yield param
478
- pat1 = _param2rexp(param) # ex: '[^./?]+'
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.scan(/(.*?)(?::(\w+))/) do |str, param_|
489
- pat = _param2rexp(param) # ex: pat == '[^./?]+'
490
- sb << Regexp.escape(str) << "<<#{pat}>>" # ex: sb << '(?:\.<<[^./?]+>>)?'
491
- yield param_
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 Symbol ; sb << k.to_s ; _build_rexp(v, sb, &b)
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/edit'
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 = /:\w+|\(.*?\)/
543
- arr = urlpath_pattern.split(rexp, -1) # ex: ['/books/', '/edit']
544
- return nil unless arr.length == 2
545
- return (arr[0].length .. -(arr[1].length+1)) # ex: 7..-6 (Range object)
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "rack-jet_router"
5
- spec.version = "$Release: 1.3.0 $".split()[1]
5
+ spec.version = "$Release: 1.4.0 $".split()[1]
6
6
  spec.author = "kwatch"
7
7
  spec.email = "kwatch@gmail.com"
8
8
  spec.platform = Gem::Platform::RUBY
data/test/builder_test.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
3
  ###
4
- ### $Release: 1.3.0 $
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, nil],
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}, nil],
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}, (10..-1)]},
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 "[!po6o6] param regexp should be stored into nested dict as a Symbol." do
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 Symbol key (which represents regexp)." do
362
- dict = {"aa1" => {"aa2"=>10}, :"bb1" => {"bb2"=>20}}
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
- :"bb1" => {"bb2"=>20},
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
- :'\d+' => {
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
- r1 = _range_of_urlpath_param('/books/:id')
604
- _.ok {r1} == (7..-1)
605
- _.ok {'/books/123'[r1]} == '123'
606
- r2 = _range_of_urlpath_param('/books/:id.html')
607
- _.ok {r2} == (7..-6)
608
- _.ok {'/books/4567.html'[r2]} == '4567'
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/:comment_id')} == nil
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