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.
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  ###
5
- ### $Release: 1.3.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
- ## "/" => 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.1 $'.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,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 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'
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 = /:\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)
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
@@ -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.1 $".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.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, 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}, 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 "[!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
  },
@@ -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
- r1 = _range_of_urlpath_param('/books/:id')
611
- _.ok {r1} == (7..-1)
612
- _.ok {'/books/123'[r1]} == '123'
613
- r2 = _range_of_urlpath_param('/books/:id.html')
614
- _.ok {r2} == (7..-6)
615
- _.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"]
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/:comment_id')} == nil
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