rack-jet_router 1.2.0 → 1.3.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,26 +2,29 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "rack-jet_router"
5
- spec.version = '$Release: 1.2.0 $'.split()[1]
6
- spec.authors = ["makoto kuwata"]
7
- spec.email = ["kwa(at)kuwata-lab.com"]
8
-
5
+ spec.version = "$Release: 1.3.0 $".split()[1]
6
+ spec.author = "kwatch"
7
+ spec.email = "kwatch@gmail.com"
8
+ spec.platform = Gem::Platform::RUBY
9
9
  spec.summary = "Super-fast router class for Rack"
10
- spec.description = <<'END'
11
- Super-fast router class for Rack application, derived from Keight.rb.
12
- END
10
+ spec.description = <<~"END"
11
+ Super-fast router class for Rack application, derived from Keight.rb.
12
+
13
+ See #{spec.homepage} for details.
14
+ END
13
15
  spec.homepage = "https://github.com/kwatch/rack-jet_router"
14
- spec.license = "MIT-License"
16
+ spec.license = "MIT"
15
17
 
16
- spec.files = Dir[*%w[
17
- README.md MIT-LICENSE Rakefile rack-jet_router.gemspec
18
- lib/**/*.rb
19
- test/test_helper.rb test/**/*_test.rb
20
- ]]
21
- spec.require_paths = ["lib"]
18
+ spec.files = Dir[
19
+ "README.md", "MIT-LICENSE", "CHANGES.md",
20
+ "#{spec.name}.gemspec",
21
+ "lib/**/*.rb", "test/**/*.rb",
22
+ "bench/bench.rb", "bench/Gemfile", "bench/Rakefile.rb",
23
+ ]
24
+ spec.require_path = "lib"
25
+ spec.test_files = Dir["test/**/*_test.rb"] # or: ["test/run_all.rb"]
22
26
 
23
- spec.required_ruby_version = '>= 2.0'
24
- spec.add_development_dependency "bundler"
25
- spec.add_development_dependency "minitest"
26
- spec.add_development_dependency "minitest-ok"
27
+ spec.required_ruby_version = ">= 2.4"
28
+ spec.add_development_dependency "oktest" , "~> 1"
29
+ spec.add_development_dependency "benchmarker" , "~> 1"
27
30
  end
@@ -0,0 +1,631 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ ###
4
+ ### $Release: 1.3.0 $
5
+ ### $Copyright: copyright(c) 2015 kwatch@gmail.com $
6
+ ### $License: MIT License $
7
+ ###
8
+
9
+ require_relative './shared'
10
+
11
+
12
+ Oktest.scope do
13
+
14
+
15
+ topic Rack::JetRouter::Builder do
16
+
17
+ book_list_api = proc {|env| [200, {}, ["book_list_api"]]}
18
+ book_create_api = proc {|env| [200, {}, ["book_create_api"]]}
19
+ book_new_api = proc {|env| [200, {}, ["book_new_api"]]}
20
+ book_show_api = proc {|env| [200, {}, ["book_show_api"]]}
21
+ book_update_api = proc {|env| [200, {}, ["book_update_api"]]}
22
+ book_delete_api = proc {|env| [200, {}, ["book_delete_api"]]}
23
+ book_edit_api = proc {|env| [200, {}, ["book_edit_api"]]}
24
+ #
25
+ comment_create_api = proc {|env| [200, {}, ["comment_create_api"]]}
26
+ comment_update_api = proc {|env| [200, {}, ["comment_update_api"]]}
27
+ #
28
+
29
+ def _find(d, k)
30
+ return d.key?(k) ? d[k] : _find(d.to_a[0][1], k)
31
+ end
32
+
33
+ before do
34
+ router = Rack::JetRouter.new([])
35
+ @builder = Rack::JetRouter::Builder.new(router)
36
+ end
37
+
38
+
39
+ topic '#build_tree()' do
40
+
41
+ spec "[!6oa05] builds nested hash object from mapping data." do
42
+ endpoint_pairs = [
43
+ ['/api/books/:id' , book_show_api],
44
+ ['/api/books/:id/edit', book_edit_api],
45
+ ['/api/books/:book_id/comments' , comment_create_api],
46
+ ['/api/books/:book_id/comments/:comment_id', comment_update_api],
47
+ ]
48
+ dict = @builder.build_tree(endpoint_pairs)
49
+ id = '[^./?]+'
50
+ ok {dict} == {
51
+ "/api/books/" => {
52
+ :'[^./?]+' => {
53
+ nil => [/\A\/api\/books\/(#{id})\z/,
54
+ ["id"], book_show_api, 11..-1],
55
+ "/" => {
56
+ "edit" => {
57
+ nil => [/\A\/api\/books\/(#{id})\/edit\z/,
58
+ ["id"], book_edit_api, 11..-6],
59
+ },
60
+ "comments" => {
61
+ nil => [/\A\/api\/books\/(#{id})\/comments\z/,
62
+ ["book_id"], comment_create_api, 11..-10],
63
+ "/" => {
64
+ :'[^./?]+' => {
65
+ nil => [/\A\/api\/books\/(#{id})\/comments\/(#{id})\z/,
66
+ ["book_id", "comment_id"], comment_update_api, nil],
67
+ },
68
+ },
69
+ },
70
+ },
71
+ },
72
+ },
73
+ }
74
+ end
75
+
76
+ spec "[!uyupj] handles urlpath parameter such as ':id'." do
77
+ endpoint_pairs = [
78
+ ["/api/books/:book_id/comments" , {"POST"=>comment_create_api}],
79
+ ["/api/books/:book_id/comments/:id", {"PUT"=>comment_update_api}],
80
+ ]
81
+ dict = @builder.build_tree(endpoint_pairs)
82
+ id = '[^./?]+'
83
+ ok {dict} == {
84
+ "/api/books/" => {
85
+ :"[^./?]+" => {
86
+ "/comments" => {
87
+ nil => [%r`\A/api/books/(#{id})/comments\z`,
88
+ ["book_id"], {"POST"=>comment_create_api}, (11..-10)],
89
+ "/" => {
90
+ :"[^./?]+" => {
91
+ nil => [%r`\A/api/books/(#{id})/comments/(#{id})\z`,
92
+ ["book_id", "id"], {"PUT"=>comment_update_api}, nil],
93
+ },
94
+ },
95
+ },
96
+ },
97
+ },
98
+ }
99
+ end
100
+
101
+ spec "[!j9cdy] handles optional urlpath parameter such as '(.:format)'." do
102
+ endpoint_pairs = [
103
+ ["/api/books(.:format)" , {"GET"=>book_list_api}],
104
+ ["/api/books/:id(.:format)", {"GET"=>book_show_api}],
105
+ ]
106
+ dict = @builder.build_tree(endpoint_pairs)
107
+ id = '[^./?]+'
108
+ ok {dict} == {
109
+ "/api/books" => {
110
+ :"(?:\\.[^./?]+)?" => {
111
+ nil => [%r`\A/api/books(?:\.(#{id}))?\z`,
112
+ ["format"], {"GET"=>book_list_api}, (10..-1)]},
113
+ "/" => {
114
+ :"[^./?]+" => {
115
+ :"(?:\\.[^./?]+)?" => {
116
+ nil => [%r`\A/api/books/(#{id})(?:\.(#{id}))?\z`,
117
+ ["id", "format"], {"GET"=>book_show_api}, nil],
118
+ },
119
+ },
120
+ },
121
+ },
122
+ }
123
+ end
124
+
125
+ spec "[!akkkx] converts urlpath param into regexp." do
126
+ endpoint_pairs = [
127
+ ["/api/books/:id", book_show_api],
128
+ ]
129
+ dict = @builder.build_tree(endpoint_pairs)
130
+ tuple = _find(dict, nil)
131
+ id = '[^./?]+'
132
+ ok {tuple[0]} == %r`\A/api/books/(#{id})\z`
133
+ end
134
+
135
+ spec "[!po6o6] param regexp should be stored into nested dict as a Symbol." do
136
+ endpoint_pairs = [
137
+ ["/api/books/:id", book_show_api],
138
+ ]
139
+ dict = @builder.build_tree(endpoint_pairs)
140
+ ok {dict["/api/books/"].keys()} == [:'[^./?]+']
141
+ end
142
+
143
+ spec "[!zoym3] urlpath string should be escaped." do
144
+ endpoint_pairs = [
145
+ ["/api/books.dir.tmp/:id.tar.gz", book_show_api],
146
+ ]
147
+ dict = @builder.build_tree(endpoint_pairs)
148
+ tuple = _find(dict, nil)
149
+ id = '[^./?]+'
150
+ ok {tuple[0]} == %r`\A/api/books\.dir\.tmp/(#{id})\.tar\.gz\z`
151
+ end
152
+
153
+ spec "[!o642c] remained string after param should be handled correctly." do
154
+ endpoint_pairs = [
155
+ ["/api/books/:id(.:format)(.gz)", book_show_api],
156
+ ]
157
+ dict = @builder.build_tree(endpoint_pairs)
158
+ tuple = _find(dict, nil)
159
+ id = '[^./?]+'
160
+ ok {tuple[0]} == %r`\A/api/books/(#{id})(?:\.(#{id}))?(?:\.gz)?\z`
161
+ end
162
+
163
+ spec "[!kz8m7] range object should be included into tuple if only one param exist." do
164
+ endpoint_pairs = [
165
+ ["/api/books/:id.json", book_show_api],
166
+ ]
167
+ dict = @builder.build_tree(endpoint_pairs)
168
+ tuple = _find(dict, nil)
169
+ id = '[^./?]+'
170
+ ok {tuple} == [%r`\A/api/books/(#{id})\.json\z`,
171
+ ["id"], book_show_api, (11..-6)]
172
+ end
173
+
174
+ spec "[!c6xmp] tuple should be stored into nested dict with key 'nil'." do
175
+ endpoint_pairs = [
176
+ ["/api/books/:id.json", book_show_api],
177
+ ]
178
+ dict = @builder.build_tree(endpoint_pairs)
179
+ id = '[^./?]+'
180
+ ok {dict} == {
181
+ "/api/books/" => {
182
+ :"[^./?]+" => {
183
+ ".json" => {
184
+ nil => [%r`\A/api/books/(#{id})\.json\z`,
185
+ ["id"], book_show_api, (11..-6)],
186
+ },
187
+ },
188
+ },
189
+ }
190
+ end
191
+
192
+ end
193
+
194
+
195
+ topic '#traverse_mapping()' do
196
+
197
+ spec "[!9s3f0] supports both nested list mapping and nested dict mapping." do
198
+ expected = [
199
+ ["/api/books" , book_list_api],
200
+ ["/api/books/new", book_new_api ],
201
+ ["/api/books/:id", book_show_api],
202
+ ]
203
+ #
204
+ mapping1 = [
205
+ ['/api' , [
206
+ ['/books' , [
207
+ ['' , book_list_api],
208
+ ['/new' , book_new_api],
209
+ ['/:id' , book_show_api],
210
+ ]],
211
+ ]],
212
+ ]
213
+ actuals1 = []
214
+ @builder.traverse_mapping(mapping1) {|*args| actuals1 << args }
215
+ ok {actuals1} == expected
216
+ #
217
+ mapping2 = {
218
+ '/api' => {
219
+ '/books' => {
220
+ '' => book_list_api,
221
+ '/new' => book_new_api,
222
+ '/:id' => book_show_api,
223
+ },
224
+ },
225
+ }
226
+ actuals2 = []
227
+ @builder.traverse_mapping(mapping2) {|*args| actuals2 << args }
228
+ ok {actuals2} == expected
229
+ end
230
+
231
+ spec "[!2ntnk] nested dict mapping can have subclass of Hash as handlers." do
232
+ ok {Map(GET: 1)}.is_a?(Map)
233
+ ok {Map(GET: 1)}.is_a?(Hash)
234
+ ok {Map} < Hash
235
+ #
236
+ mapping = {
237
+ '/api' => {
238
+ '/books' => {
239
+ '' => Map(GET: book_list_api),
240
+ '/new' => Map(GET: book_new_api),
241
+ '/:id' => Map(GET: book_show_api, PUT: book_edit_api),
242
+ },
243
+ },
244
+ }
245
+ actuals = []
246
+ @builder.traverse_mapping(mapping) {|*args| actuals << args }
247
+ ok {actuals} == [
248
+ ["/api/books" , Map.new.update("GET"=>book_list_api)],
249
+ ["/api/books/new", Map.new.update("GET"=>book_new_api) ],
250
+ ["/api/books/:id", Map.new.update("GET"=>book_show_api, "PUT"=>book_edit_api)],
251
+ ]
252
+ end
253
+
254
+ spec "[!dj0sh] traverses mapping recursively." do
255
+ mapping = [
256
+ ['/api' , [
257
+ ['/books' , [
258
+ ['' , book_list_api],
259
+ ['/new' , book_new_api],
260
+ ['/:id' , book_show_api],
261
+ ['/:id/edit' , book_edit_api],
262
+ ]],
263
+ ['/books/:book_id/comments', [
264
+ ['' , comment_create_api],
265
+ ['/:comment_id' , comment_update_api],
266
+ ]],
267
+ ]],
268
+ ]
269
+ actuals = []
270
+ @builder.traverse_mapping(mapping) {|*args| actuals << args }
271
+ ok {actuals} == [
272
+ ["/api/books" , book_list_api],
273
+ ["/api/books/new" , book_new_api],
274
+ ["/api/books/:id" , book_show_api],
275
+ ["/api/books/:id/edit", book_edit_api],
276
+ ["/api/books/:book_id/comments" , comment_create_api],
277
+ ["/api/books/:book_id/comments/:comment_id", comment_update_api],
278
+ ]
279
+ end
280
+
281
+ spec "[!j0pes] if item is a hash object, converts keys from symbol to string." do
282
+ mapping = [
283
+ ['/api' , [
284
+ ['/books' , [
285
+ ['/new' , {GET: book_new_api}],
286
+ ['/:id' , {GET: book_show_api, DELETE: book_delete_api}],
287
+ ]],
288
+ ]],
289
+ ]
290
+ actuals = []
291
+ @builder.traverse_mapping(mapping) {|path, item| actuals << [path, item] }
292
+ ok {actuals} == [
293
+ ["/api/books/new", {"GET"=>book_new_api}],
294
+ ["/api/books/:id", {"GET"=>book_show_api, "DELETE"=>book_delete_api}],
295
+ ]
296
+ end
297
+
298
+ spec "[!brhcs] yields block for each full path and handler." do
299
+ mapping = {
300
+ '/api' => {
301
+ '/books' => {
302
+ '' => book_list_api,
303
+ '/new' => book_new_api,
304
+ '/:id' => book_show_api,
305
+ '/:id/edit' => book_edit_api,
306
+ },
307
+ '/books/:book_id/comments' => {
308
+ '' => comment_create_api,
309
+ '/:comment_id' => comment_update_api,
310
+ },
311
+ },
312
+ }
313
+ actuals = []
314
+ @builder.traverse_mapping(mapping) {|*args| actuals << args }
315
+ ok {actuals} == [
316
+ ["/api/books" , book_list_api],
317
+ ["/api/books/new" , book_new_api],
318
+ ["/api/books/:id" , book_show_api],
319
+ ["/api/books/:id/edit", book_edit_api],
320
+ ["/api/books/:book_id/comments" , comment_create_api],
321
+ ["/api/books/:book_id/comments/:comment_id", comment_update_api],
322
+ ]
323
+ end
324
+ end
325
+
326
+
327
+ topic '#_next_dict()' do
328
+
329
+ case_when "[!s1rzs] if new key exists in dict..." do
330
+
331
+ spec "[!io47b] just returns corresponding value and not change dict." do
332
+ dict = {"a" => {"b" => 10}}
333
+ d2 = @builder.instance_eval { _next_dict(dict, "a") }
334
+ ok {d2} == {"b" => 10}
335
+ ok {dict} == {"a" => {"b" => 10}}
336
+ end
337
+
338
+ end
339
+
340
+ spec "[!3ndpz] returns next dict." do
341
+ dict = {"aa1" => {"aa2"=>10}}
342
+ d2 = @builder.instance_eval { _next_dict(dict, "aa8") }
343
+ ok {d2} == {}
344
+ ok {dict} == {"aa" => {"1" => {"aa2"=>10}, "8" => {}}}
345
+ ok {dict["aa"]["8"]}.same?(d2)
346
+ end
347
+
348
+ spec "[!5fh08] keeps order of keys in dict." do
349
+ dict = {"aa1" => {"aa2"=>10}, "bb1" => {"bb2"=>20}, "cc1" => {"cc2"=>30}}
350
+ d2 = @builder.instance_eval { _next_dict(dict, "bb8") }
351
+ ok {d2} == {}
352
+ ok {dict} == {
353
+ "aa1" => {"aa2"=>10},
354
+ "bb" => {"1" => {"bb2"=>20}, "8"=>{}},
355
+ "cc1" => {"cc2"=>30},
356
+ }
357
+ ok {dict["bb"]["8"]}.same?(d2)
358
+ ok {dict.keys} == ["aa1", "bb", "cc1"]
359
+ end
360
+
361
+ spec "[!4wdi7] ignores Symbol key (which represents regexp)." do
362
+ dict = {"aa1" => {"aa2"=>10}, :"bb1" => {"bb2"=>20}}
363
+ d2 = @builder.instance_eval { _next_dict(dict, "bb1") }
364
+ ok {d2} == {}
365
+ ok {dict} == {
366
+ "aa1" => {"aa2"=>10},
367
+ :"bb1" => {"bb2"=>20},
368
+ "bb1" => {},
369
+ }
370
+ ok {dict["bb1"]}.same?(d2)
371
+ end
372
+
373
+ spec "[!66sdb] ignores nil key (which represents leaf node)." do
374
+ dict = {"aa1" => {"aa2" => 10}, nil => ["foo"]}
375
+ d2 = @builder.instance_eval { _next_dict(dict, "mm1") }
376
+ ok {d2} == {}
377
+ ok {dict} == {"aa1" => {"aa2" => 10}, nil => ["foo"], "mm1" => {}}
378
+ ok {dict["mm1"]}.same?(d2)
379
+ end
380
+
381
+ case_when "[!46o9b] if existing key is same as common prefix..." do
382
+
383
+ spec "[!4ypls] not replace existing key." do
384
+ dict = {"aa1" => {"aa2" => 10}, "bb1" => {"bb2" => 20}}
385
+ aa1 = dict["aa1"]
386
+ d2 = @builder.instance_eval { _next_dict(dict, "aa1mm2") }
387
+ ok {d2} == {}
388
+ ok {dict} == {
389
+ "aa1" => {"aa2" => 10, "mm2" => d2},
390
+ "bb1" => {"bb2" => 20},
391
+ }
392
+ ok {dict["aa1"]}.same?(aa1)
393
+ end
394
+
395
+ end
396
+
397
+ case_when "[!veq0q] if new key is same as common prefix..." do
398
+
399
+ spec "[!0tboh] replaces existing key with ney key." do
400
+ dict = {"aa1" => {"aa2" => 10}, "bb1" => {"bb2" => 20}}
401
+ aa1 = dict["aa1"]
402
+ d2 = @builder.instance_eval { _next_dict(dict, "aa") }
403
+ ok {d2} == {"1" => {"aa2" => 10}}
404
+ ok {dict} == {
405
+ "aa" => {"1" => {"aa2" => 10}},
406
+ "bb1" => {"bb2" => 20},
407
+ }
408
+ ok {dict["aa"]["1"]}.same?(aa1)
409
+ end
410
+
411
+ end
412
+
413
+ case_when "[!esszs] if common prefix is a part of exsting key and new key..." do
414
+
415
+ spec "[!pesq0] replaces existing key with common prefix." do
416
+ dict = {"aa1" => {"aa2"=>10}, "bb1" => {"bb2"=>20}, "cc1" => {"cc2"=>30}}
417
+ bb1 = dict["bb1"]
418
+ d2 = @builder.instance_eval { _next_dict(dict, "bb7") }
419
+ ok {d2} == {}
420
+ ok {dict} == {
421
+ "aa1" => {"aa2"=>10},
422
+ "bb" => {"1" => {"bb2"=>20}, "7" => {}},
423
+ "cc1" => {"cc2"=>30},
424
+ }
425
+ ok {dict["bb"]["7"]}.same?(d2)
426
+ end
427
+
428
+ end
429
+
430
+ case_when "[!viovl] if new key has no common prefix with existing keys..." do
431
+
432
+ spec "[!i6smv] adds empty dict with new key." do
433
+ dict = {"aa1" => {"aa2"=>10}, "bb1" => {"bb2"=>20}, "cc1" => {"cc2"=>30}}
434
+ d2 = @builder.instance_eval { _next_dict(dict, "mm1") }
435
+ ok {dict} == {
436
+ "aa1" => {"aa2"=>10},
437
+ "bb1" => {"bb2"=>20},
438
+ "cc1" => {"cc2"=>30},
439
+ "mm1" => {},
440
+ }
441
+ ok {dict["mm1"]}.same?(d2)
442
+ end
443
+
444
+ end
445
+
446
+ end
447
+
448
+
449
+ topic '#_common_prefix()' do
450
+
451
+ spec "[!1z2ii] returns common prefix and rest of strings." do
452
+ t = @builder.instance_eval { _common_prefix("baar", "bazz") }
453
+ prefix, rest1, rest2 = t
454
+ ok {prefix} == "ba"
455
+ ok {rest1} == "ar"
456
+ ok {rest2} == "zz"
457
+ end
458
+
459
+ spec "[!86tsd] calculates common prefix of two strings." do
460
+ t = @builder.instance_eval { _common_prefix("bar", "barkuz") }
461
+ prefix, rest1, rest2 = t
462
+ ok {prefix} == "bar"
463
+ ok {rest1} == ""
464
+ ok {rest2} == "kuz"
465
+ #
466
+ t = @builder.instance_eval { _common_prefix("barrab", "bar") }
467
+ prefix, rest1, rest2 = t
468
+ ok {prefix} == "bar"
469
+ ok {rest1} == "rab"
470
+ ok {rest2} == ""
471
+ end
472
+
473
+ end
474
+
475
+
476
+ topic '#_param_patterns()' do
477
+
478
+ spec "[!j90mw] returns '[^./?]+' and '([^./?]+)' if param specified." do
479
+ x = nil
480
+ s1, s2 = @builder.instance_eval { _param_patterns("id", nil) {|a| x = a } }
481
+ ok {s1} == '[^./?]+'
482
+ ok {s2} == '([^./?]+)'
483
+ ok {x} == "id"
484
+ end
485
+
486
+ spec "[!raic7] returns '(?:\.[^./?]+)?' and '(?:\.([^./?]+))?' if optional param is '(.:format)'." do
487
+ x = nil
488
+ s1, s2 = @builder.instance_eval { _param_patterns(nil, ".:format") {|a| x = a } }
489
+ ok {s1} == '(?:\.[^./?]+)?'
490
+ ok {s2} == '(?:\.([^./?]+))?'
491
+ ok {x} == "format"
492
+ end
493
+
494
+ spec "[!69yj9] optional string can contains other params." do
495
+ arr = []
496
+ s1, s2 = @builder.instance_eval { _param_patterns(nil, ":yr-:mo-:dy") {|a| arr << a } }
497
+ ok {s1} == '(?:[^./?]+\-[^./?]+\-[^./?]+)?'
498
+ ok {s2} == '(?:([^./?]+)\-([^./?]+)\-([^./?]+))?'
499
+ ok {arr} == ["yr", "mo", "dy"]
500
+ end
501
+
502
+ end
503
+
504
+
505
+ topic '#build_rexp()' do
506
+
507
+ spec "[!65yw6] converts nested dict into regexp." do
508
+ dict = {
509
+ "/api/books/" => {
510
+ :"[^./?]+" => {
511
+ nil => [],
512
+ "/comments" => {
513
+ nil => [],
514
+ "/" => {
515
+ :"[^./?]+" => {
516
+ nil => [],
517
+ },
518
+ },
519
+ },
520
+ },
521
+ },
522
+ }
523
+ x = @builder.build_rexp(dict) { }
524
+ id = '[^./?]+'
525
+ ok {x} == %r`\A/api/books/#{id}(?:(\z)|/comments(?:(\z)|/#{id}(\z)))\z`
526
+ end
527
+
528
+ spec "[!hs7vl] '(?:)' and '|' are added only if necessary." do
529
+ dict = {
530
+ "/api/books/" => {
531
+ :"[^./?]+" => {
532
+ "/comments" => {
533
+ "/" => {
534
+ :"[^./?]+" => {
535
+ nil => [],
536
+ },
537
+ },
538
+ },
539
+ },
540
+ },
541
+ }
542
+ x = @builder.build_rexp(dict) { }
543
+ id = '[^./?]+'
544
+ ok {x} == %r`\A/api/books/#{id}/comments/#{id}(\z)\z`
545
+ end
546
+
547
+ spec "[!7v7yo] nil key means leaf node and yields block argument." do
548
+ dict = {
549
+ "/api/books/" => {
550
+ :"[^./?]+" => {
551
+ nil => [9820],
552
+ "/comments" => {
553
+ "/" => {
554
+ :"[^./?]+" => {
555
+ nil => [1549],
556
+ },
557
+ },
558
+ },
559
+ },
560
+ },
561
+ }
562
+ actuals = []
563
+ x = @builder.build_rexp(dict) {|a| actuals << a }
564
+ id = '[^./?]+'
565
+ ok {x} == %r`\A/api/books/#{id}(?:(\z)|/comments/#{id}(\z))\z`
566
+ ok {actuals} == [[9820], [1549]]
567
+ end
568
+
569
+ spec "[!hda6m] string key should be escaped." do
570
+ dict = {
571
+ "/api/books/" => {
572
+ :"[^./?]+" => {
573
+ ".json" => {
574
+ nil => [],
575
+ }
576
+ }
577
+ }
578
+ }
579
+ x = @builder.build_rexp(dict) { }
580
+ id = '[^./?]+'
581
+ ok {x} == %r`\A/api/books/#{id}\.json(\z)\z`
582
+ end
583
+
584
+ spec "[!b9hxc] symbol key means regexp string." do
585
+ dict = {
586
+ "/api/books/" => {
587
+ :'\d+' => {
588
+ nil => [],
589
+ },
590
+ },
591
+ }
592
+ x = @builder.build_rexp(dict) { }
593
+ ok {x} == %r`\A/api/books/\d+(\z)\z`
594
+ end
595
+
596
+ end
597
+
598
+
599
+ topic '#range_of_urlpath_param()' do
600
+
601
+ spec "[!syrdh] returns Range object when urlpath_pattern contains just one param." do
602
+ @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'
609
+ end
610
+ end
611
+
612
+ spec "[!skh4z] returns nil when urlpath_pattern contains more than two params." do
613
+ @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
616
+ end
617
+ end
618
+
619
+ spec "[!acj5b] returns nil when urlpath_pattern contains no params." do
620
+ @builder.instance_exec(self) do |_|
621
+ _.ok {_range_of_urlpath_param('/books')} == nil
622
+ end
623
+ end
624
+
625
+ end
626
+
627
+
628
+ end
629
+
630
+
631
+ end