keight 0.1.0 → 0.2.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.
@@ -0,0 +1,13 @@
1
+ # README
2
+
3
+ See https://github.com/kwatch/keight/tree/ruby for details.
4
+
5
+
6
+ ## Setup
7
+
8
+ $ rake setup:install
9
+
10
+
11
+ ## Test
12
+
13
+ $ rake # or: rake test
@@ -1,21 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
- require 'keight'
3
+ require_relative './main'
4
4
 
5
- ## create $config object
6
- require_relative 'config'
7
-
8
- ## get $urlpath_mapping
9
- require_relative 'config/urlpath_mapping'
10
-
11
- ## create application object
12
- opts = $config.get_all(:k8_rackapp_) # ex: {urlpath_cache_size: 1000}
13
- app = K8::RackApplication.new($urlpath_mapping, opts)
14
- $urlpath_mapping = nil
15
-
16
- ## cookie store session
17
- require 'rack/session/cookie'
18
- use Rack::Session::Cookie, $config.get_all(:session_)
19
-
20
- ## start application
21
- run app
5
+ # start application
6
+ run $main_app
@@ -27,12 +27,13 @@ app/template/ template files
27
27
  app/template/_layout.html.eruby default layout template
28
28
  app/template/welcome.html.eruby welcome page template
29
29
  static/ static files
30
- static/lib/ library such as jquery
31
- static/lib/jquery/
32
- static/lib/jquery/1.11.3
33
- static/lib/modernizr/
34
- static/lib/modernizr/2.8.3
30
+ static/lib/ libraries such as jquery
31
+ test/ test scripts
32
+ test/api/
33
+ test/api/hello_test.rb
34
+ test/test_helper.rb
35
35
  tmp/ temporary directory
36
36
  tmp/upload/ for uploaded files
37
37
  .gitignore for Git
38
-
38
+ Gemfile gems list
39
+ main.rb create Rack app
@@ -0,0 +1,22 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'keight'
4
+
5
+ ## create $config object
6
+ require_relative 'config'
7
+
8
+ ## get $urlpath_mapping
9
+ require_relative 'config/urlpath_mapping'
10
+
11
+ ## create application object
12
+ opts = $config.get_all(:k8_rackapp_) # ex: {urlpath_cache_size: 1000}
13
+ app = K8::RackApplication.new($urlpath_mapping, opts)
14
+ $urlpath_mapping = nil
15
+ $k8_app = app
16
+
17
+ ## cookie store session
18
+ require 'rack/session/cookie'
19
+ app = Rack::Session::Cookie.new(app, $config.get_all(:session_))
20
+
21
+ ## export as $main_app
22
+ $main_app = app
File without changes
@@ -0,0 +1,27 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require_relative '../test_helper'
4
+
5
+
6
+ http = Rack::TestApp.wrap($main_app)
7
+
8
+
9
+ describe HelloAPI do
10
+
11
+
12
+ describe 'GET /api/hello' do
13
+
14
+ it "returns JSON data." do
15
+ r = http.GET('/api/hello')
16
+ result = http.GET('/api/hello')
17
+ r = result
18
+ ok {r.status} == 200
19
+ ok {r.content_type} == "application/json"
20
+ #ok {r.body_json} == {"message"=>"Hello"}
21
+ ok {r.body_json} == {"action"=>"index", "query"=>{}}
22
+ end
23
+
24
+ end
25
+
26
+
27
+ end
@@ -0,0 +1,9 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'minitest/spec'
4
+ require 'minitest/autorun'
5
+ require 'minitest/ok'
6
+ require 'rack'
7
+ require 'rack/test_app'
8
+
9
+ require_relative '../main'
@@ -5,6 +5,7 @@ $LOAD_PATH << "test" unless $LOAD_PATH.include?("test")
5
5
 
6
6
  require 'stringio'
7
7
 
8
+ require 'rack/test_app'
8
9
  require 'oktest'
9
10
 
10
11
  require 'keight'
@@ -92,7 +93,7 @@ end
92
93
  Oktest.scope do
93
94
 
94
95
  def new_env(meth="GET", path="/", opts={})
95
- return K8::Mock.new_env(meth, path, opts)
96
+ return Rack::TestApp.new_env(meth, path, opts)
96
97
  end
97
98
 
98
99
 
@@ -374,6 +375,26 @@ Oktest.scope do
374
375
  end
375
376
 
376
377
 
378
+ topic '#method()' do
379
+
380
+ fixture :req do
381
+ K8::Request.new({"REQUEST_METHOD"=>"POST"})
382
+ end
383
+
384
+ spec "[!084jo] returns current request method when argument is not specified." do
385
+ |req|
386
+ ok {req.method} == :POST
387
+ end
388
+
389
+ spec "[!gwskf] calls Object#method() when argument specified." do
390
+ |req|
391
+ ok {req.method(:method)} != :POST
392
+ ok {req.method(:method)}.is_a?(Method)
393
+ end
394
+
395
+ end
396
+
397
+
377
398
  topic '#header()' do
378
399
 
379
400
  spec "[!1z7wj] returns http header value from environment." do
@@ -388,24 +409,6 @@ Oktest.scope do
388
409
  end
389
410
 
390
411
 
391
- topic '#method()' do
392
-
393
- spec "[!tp595] returns :GET, :POST, :PUT, ... when argument is not passed." do
394
- ok {K8::Request.new(new_env('GET', '/')).method} == :GET
395
- ok {K8::Request.new(new_env('POST', '/')).method} == :POST
396
- ok {K8::Request.new(new_env('PUT', '/')).method} == :PUT
397
- ok {K8::Request.new(new_env('DELETE', '/')).method} == :DELETE
398
- end
399
-
400
- spec "[!49f51] returns Method object when argument is passed." do
401
- req = K8::Request.new(new_env('GET', '/'))
402
- ok {req.method('env')}.is_a?(Method)
403
- ok {req.method('env').call()}.same?(req.env)
404
- end
405
-
406
- end
407
-
408
-
409
412
  topic '#request_method' do
410
413
 
411
414
  spec "[!y8eos] returns env['REQUEST_METHOD'] as string." do
@@ -636,7 +639,7 @@ Oktest.scope do
636
639
  topic '#cookies' do
637
640
 
638
641
  spec "[!c9pwr] parses cookie data and returns it as hash object." do
639
- req = K8::Request.new(new_env('POST', '/', cookie: "aaa=homhom; bbb=madmad"))
642
+ req = K8::Request.new(new_env('POST', '/', cookies: "aaa=homhom; bbb=madmad"))
640
643
  ok {req.cookies} == {"aaa"=>"homhom", "bbb"=>"madmad"}
641
644
  end
642
645
 
@@ -785,6 +788,49 @@ Oktest.scope do
785
788
  end
786
789
 
787
790
 
791
+ topic '._build_action_info()' do
792
+
793
+ spec "[!ordhc] build ActionInfo objects for each action methods." do
794
+ infos = BooksAction._build_action_info('/api/books')
795
+ #
796
+ ok {infos[:do_index]}.is_a?(K8::ActionInfo)
797
+ ok {infos[:do_index].method} == :GET
798
+ ok {infos[:do_index].urlpath} == '/api/books/'
799
+ #
800
+ ok {infos[:do_update]}.is_a?(K8::ActionInfo)
801
+ ok {infos[:do_update].method} == :PUT
802
+ ok {infos[:do_update].urlpath(123)} == '/api/books/123'
803
+ end
804
+
805
+ end
806
+
807
+
808
+ topic '.[]' do
809
+
810
+ spec "[!1tq8z] returns ActionInfo object corresponding to action method." do
811
+ BooksAction._build_action_info('/api/books')
812
+ cls = BooksAction
813
+ #
814
+ ok {cls[:do_create]}.is_a?(K8::ActionInfo)
815
+ ok {cls[:do_create].method} == :POST
816
+ ok {cls[:do_create].urlpath} == '/api/books/'
817
+ #
818
+ ok {cls[:do_show]}.is_a?(K8::ActionInfo)
819
+ ok {cls[:do_show].method} == :GET
820
+ ok {cls[:do_show].urlpath(123)} == '/api/books/123'
821
+ end
822
+
823
+ spec "[!6g2iw] returns nil when not mounted yet." do
824
+ class ExampleClass2 < K8::BaseAction
825
+ mapping '', :GET=>:do_index
826
+ def do_index; end
827
+ end
828
+ ok {ExampleClass2[:do_index]} == nil
829
+ end
830
+
831
+ end
832
+
833
+
788
834
  end
789
835
 
790
836
 
@@ -1397,6 +1443,83 @@ Oktest.scope do
1397
1443
  end
1398
1444
 
1399
1445
 
1446
+ topic K8::ActionInfo do
1447
+
1448
+
1449
+ topic '.create()' do
1450
+
1451
+ spec "[!1nk0i] replaces urlpath parameters with '%s'." do
1452
+ info = K8::ActionInfo.create('GET', '/books/{id}/comments/{comment_id}')
1453
+ actual = info.instance_variable_get('@urlpath_format')
1454
+ ok {actual} == '/books/%s/comments/%s'
1455
+ #
1456
+ info = K8::ActionInfo.create('GET', '/books')
1457
+ actual = info.instance_variable_get('@urlpath_format')
1458
+ ok {actual} == '/books'
1459
+ end
1460
+
1461
+ spec "[!a7fqv] replaces '%' with'%%'." do
1462
+ info = K8::ActionInfo.create('GET', '/books%9A%9B/{id}')
1463
+ actual = info.instance_variable_get('@urlpath_format')
1464
+ ok {actual} == '/books%%9A%%9B/%s'
1465
+ end
1466
+
1467
+ spec "[!btt2g] returns ActionInfoN object when number of urlpath parameter <= 4." do
1468
+ info = K8::ActionInfo.create('GET', '/books')
1469
+ ok {info}.is_a?(K8::ActionInfo0)
1470
+ ok {info.urlpath} == '/books'
1471
+ ok {->{ info.urlpath('a') }}.raise?(ArgumentError, /^wrong number of arguments \((1 for 0|given 1, expected 0)\)$/)
1472
+ #
1473
+ info = K8::ActionInfo.create('GET', '/books/{id}')
1474
+ ok {info}.is_a?(K8::ActionInfo1)
1475
+ ok {info.urlpath('a')} == '/books/a'
1476
+ ok {->{ info.urlpath() }}.raise?(ArgumentError, /^wrong number of arguments \((0 for 1|given 0, expected 1)\)$/)
1477
+ #
1478
+ info = K8::ActionInfo.create('GET', '/books/{id}/comments/{comment_id}')
1479
+ ok {info}.is_a?(K8::ActionInfo2)
1480
+ ok {info.urlpath('a', 'b')} == '/books/a/comments/b'
1481
+ ok {->{info.urlpath('a')}}.raise?(ArgumentError, /^wrong number of arguments \((1 for 2|given 1, expected 2)\)$/)
1482
+ #
1483
+ info = K8::ActionInfo.create('GET', '/books/{id}/{title}/{code}')
1484
+ ok {info}.is_a?(K8::ActionInfo3)
1485
+ ok {info.urlpath('a', 'b', 'c')} == '/books/a/b/c'
1486
+ ok {->{info.urlpath(1,2)}}.raise?(ArgumentError, /^wrong number of arguments \((2 for 3|given 2, expected 3)\)$/)
1487
+ #
1488
+ info = K8::ActionInfo.create('GET', '/books/{id}/{title}/{code}/{ref}')
1489
+ ok {info}.is_a?(K8::ActionInfo4)
1490
+ ok {info.urlpath('a', 'b', 'c', 'd')} == '/books/a/b/c/d'
1491
+ ok {->{info.urlpath}}.raise?(ArgumentError, /^wrong number of arguments \((0 for 4|given 0, expected 4)\)$/)
1492
+ end
1493
+
1494
+ spec "[!x5yx2] returns ActionInfo object when number of urlpath parameter > 4." do
1495
+ info = K8::ActionInfo.create('GET', '/books/{id}/{title}/{code}/{ref}/{x}')
1496
+ ok {info}.is_a?(K8::ActionInfo)
1497
+ ok {info.urlpath('a', 'b', 'c', 'd', 'e')} == "/books/a/b/c/d/e"
1498
+ #
1499
+ ok {->{info.urlpath('a','b','c')}}.raise?(ArgumentError, "too few arguments")
1500
+ end
1501
+
1502
+ end
1503
+
1504
+
1505
+ topic '#form_action_attr()' do
1506
+
1507
+ spec "[!qyhkm] returns '/api/books/123' when method is POST." do
1508
+ info = K8::ActionInfo.create('POST', '/api/books/{id}')
1509
+ ok {info.form_action_attr(123)} == '/api/books/123'
1510
+ end
1511
+
1512
+ spec "[!kogyx] returns '/api/books/123?_method=PUT' when method is not POST." do
1513
+ info = K8::ActionInfo.create('PUT', '/api/books/{id}')
1514
+ ok {info.form_action_attr(123)} == '/api/books/123?_method=PUT'
1515
+ end
1516
+
1517
+ end
1518
+
1519
+
1520
+ end
1521
+
1522
+
1400
1523
  topic K8::DefaultPatterns do
1401
1524
 
1402
1525
 
@@ -1463,6 +1586,42 @@ Oktest.scope do
1463
1586
  end
1464
1587
 
1465
1588
 
1589
+ topic K8::DEFAULT_PATTERNS do
1590
+
1591
+ spec "[!i51id] registers '\d+' as default pattern of param 'id' or /_id\z/." do
1592
+ pat, proc_ = K8::DEFAULT_PATTERNS.lookup('id')
1593
+ ok {pat} == '\d+'
1594
+ ok {proc_.call("123")} == 123
1595
+ pat, proc_ = K8::DEFAULT_PATTERNS.lookup('book_id')
1596
+ ok {pat} == '\d+'
1597
+ ok {proc_.call("123")} == 123
1598
+ end
1599
+
1600
+ spec "[!2g08b] registers '(?:\.\w+)?' as default pattern of param 'ext'." do
1601
+ pat, proc_ = K8::DEFAULT_PATTERNS.lookup('ext')
1602
+ ok {pat} == '(?:\.\w+)?'
1603
+ ok {proc_} == nil
1604
+ end
1605
+
1606
+ spec "[!8x5mp] registers '\d\d\d\d-\d\d-\d\d' as default pattern of param 'date' or /_date\z/." do
1607
+ pat, proc_ = K8::DEFAULT_PATTERNS.lookup('date')
1608
+ ok {pat} == '\d\d\d\d-\d\d-\d\d'
1609
+ ok {proc_.call("2014-12-24")} == Date.new(2014, 12, 24)
1610
+ pat, proc_ = K8::DEFAULT_PATTERNS.lookup('birth_date')
1611
+ ok {pat} == '\d\d\d\d-\d\d-\d\d'
1612
+ ok {proc_.call("2015-02-14")} == Date.new(2015, 2, 14)
1613
+ end
1614
+
1615
+ spec "[!wg9vl] raises 404 error when invalid date (such as 2012-02-30)." do
1616
+ pat, proc_ = K8::DEFAULT_PATTERNS.lookup('date')
1617
+ pr = proc { proc_.call('2012-02-30') }
1618
+ ok {pr}.raise?(K8::HttpException, "2012-02-30: invalid date.")
1619
+ ok {pr.exception.status_code} == 404
1620
+ end
1621
+
1622
+ end
1623
+
1624
+
1466
1625
  topic K8::ActionMethodMapping do
1467
1626
 
1468
1627
  fixture :mapping do
@@ -1520,673 +1679,681 @@ Oktest.scope do
1520
1679
  end
1521
1680
 
1522
1681
 
1523
- topic K8::ActionClassMapping do
1682
+ topic K8::ActionMapping do
1524
1683
 
1525
- fixture :mapping do
1526
- K8::ActionClassMapping.new
1527
- end
1528
-
1529
- fixture :proc_obj1 do
1530
- _, proc_obj = K8::DEFAULT_PATTERNS.lookup('id')
1531
- proc_obj
1532
- end
1533
1684
 
1534
- fixture :proc_obj2 do
1535
- _, proc_obj = K8::DEFAULT_PATTERNS.lookup('book_id')
1536
- proc_obj
1537
- end
1538
-
1539
- topic '#mount()' do
1685
+ topic '#initialize()' do
1540
1686
 
1541
- fixture :testapi_books do
1542
- Dir.mkdir 'testapi' unless File.exist? 'testapi'
1543
- at_end do
1544
- Dir.glob('testapi/*').each {|f| File.unlink f }
1545
- Dir.rmdir 'testapi'
1687
+ spec "[!buj0d] prepares LRU cache if cache size specified." do
1688
+ mapping = K8::ActionMapping.new([], urlpath_cache_size: 3)
1689
+ mapping.instance_exec(self) do |_|
1690
+ _.ok {@urlpath_cache_size} == 3
1691
+ _.ok {@urlpath_lru_cache} == {}
1546
1692
  end
1547
- File.open('testapi/books.rb', 'w') do |f|
1548
- f << <<-'END'
1549
- require 'keight'
1550
- #
1551
- class MyBooksAPI < K8::Action
1552
- mapping '', :GET=>:do_index
1553
- def do_index; ''; end
1554
- class MyError < Exception
1555
- end
1556
- end
1557
- #
1558
- module Admin
1559
- class Admin::BooksAPI < K8::Action
1560
- mapping '', :GET=>:do_index
1561
- def do_index; ''; end
1562
- end
1563
- end
1564
- END
1693
+ #
1694
+ mapping = K8::ActionMapping.new([], urlpath_cache_size: 0)
1695
+ mapping.instance_exec(self) do |_|
1696
+ _.ok {@urlpath_cache_size} == 0
1697
+ _.ok {@urlpath_lru_cache} == nil
1565
1698
  end
1566
- './testapi/books:BooksAction'
1567
1699
  end
1568
1700
 
1569
- spec "[!flb11] mounts action class to urlpath." do
1570
- |mapping|
1571
- mapping.mount '/books', BooksAction
1572
- arr = mapping.instance_variable_get('@mappings')
1573
- ok {arr}.is_a?(Array)
1574
- ok {arr.length} == 1
1575
- ok {arr[0]}.is_a?(Array)
1576
- ok {arr[0].length} == 2
1577
- ok {arr[0][0]} == '/books'
1578
- ok {arr[0][1]} == BooksAction
1701
+ spec "[!wsz8g] compiles urlpath mapping passed." do
1702
+ mapping = K8::ActionMapping.new([
1703
+ ['/api/books', BooksAction],
1704
+ ])
1705
+ mapping.instance_exec(self) do |_|
1706
+ _.ok {@urlpath_rexp} == %r'\A/api/books(?:/\d+(\z)|/\d+/edit(\z))\z'
1707
+ _.ok {@fixed_endpoints.keys} == ['/api/books/', '/api/books/new']
1708
+ _.ok {@variable_endpoints.map{|x| x[0]}} == ['/api/books/{id}', '/api/books/{id}/edit']
1709
+ end
1579
1710
  end
1580
1711
 
1581
- spec "[!4l8xl] can accept array of pairs of urlpath and action class." do
1582
- |mapping|
1583
- mapping.mount '/api', [
1584
- ['/books', BooksAction],
1585
- ]
1586
- arr = mapping.instance_variable_get('@mappings')
1587
- ok {arr} == [
1588
- ['/api', [
1589
- ['/books', BooksAction],
1590
- ]],
1591
- ]
1712
+ spec "[!34o67] keyword arg 'enable_urlpath_param_range' controls to generate range object or not." do
1713
+ arr = [['/books', BooksAction]]
1714
+ #
1715
+ mapping1 = K8::ActionMapping.new(arr, enable_urlpath_param_range: true)
1716
+ mapping1.instance_exec(self) do |_|
1717
+ tuple = @variable_endpoints.find {|a| a[0] == '/books/{id}' }
1718
+ _.ok {tuple[-1]} == (7..-1)
1719
+ end
1720
+ #
1721
+ mapping2 = K8::ActionMapping.new(arr, enable_urlpath_param_range: false)
1722
+ mapping2.instance_exec(self) do |_|
1723
+ tuple = @variable_endpoints.find {|a| a[0] == '/books/{id}' }
1724
+ _.ok {tuple[-1]} == nil
1725
+ end
1592
1726
  end
1593
1727
 
1594
- case_when "[!ne804] when target class name is string..." do
1728
+ end
1595
1729
 
1596
- spec "[!9brqr] raises error when string format is invalid." do
1597
- |mapping, testapi_books|
1598
- pr = proc { mapping.mount '/books', 'books.MyBooksAPI' }
1599
- ok {pr}.raise?(ArgumentError, "mount('books.MyBooksAPI'): expected 'file/path:ClassName'.")
1600
- end
1601
1730
 
1602
- spec "[!jpg56] loads file." do
1603
- |mapping, testapi_books|
1604
- pr = proc { mapping.mount '/books', './testapi/books:MyBooksAPI' }
1605
- ok {pr}.NOT.raise?(Exception)
1606
- ok {MyBooksAPI}.is_a?(Class)
1607
- end
1731
+ topic '#compile()' do
1608
1732
 
1609
- spec "[!vaazw] raises error when failed to load file." do
1610
- |mapping, testapi_books|
1611
- pr = proc { mapping.mount '/books', './testapi/books999:MyBooksAPI' }
1612
- ok {pr}.raise?(ArgumentError, "mount('./testapi/books999:MyBooksAPI'): failed to require file.")
1613
- end
1733
+ fixture :proc1 do
1734
+ proc {|x| x.to_i }
1735
+ end
1614
1736
 
1615
- spec "[!eiovd] raises original LoadError when it raises in loading file." do
1616
- |mapping, testapi_books|
1617
- filepath = './testapi/books7.rb'
1618
- ok {filepath}.NOT.exist?
1619
- File.open(filepath, 'w') {|f| f << "require 'homhom7'\n" }
1620
- pr = proc { mapping.mount '/books', './testapi/books7:MyBooks7API' }
1621
- ok {pr}.raise?(LoadError, "cannot load such file -- homhom7")
1622
- end
1737
+ fixture :proc2 do
1738
+ proc {|x| x.to_i }
1739
+ end
1623
1740
 
1624
- spec "[!au27n] finds target class." do
1625
- |mapping, testapi_books|
1626
- pr = proc { mapping.mount '/books', './testapi/books:MyBooksAPI' }
1627
- ok {pr}.NOT.raise?(Exception)
1628
- ok {MyBooksAPI}.is_a?(Class)
1629
- ok {MyBooksAPI} < K8::Action
1630
- #
1631
- pr = proc { mapping.mount '/books', './testapi/books:Admin::BooksAPI' }
1632
- ok {pr}.NOT.raise?(Exception)
1633
- ok {Admin::BooksAPI}.is_a?(Class)
1634
- ok {Admin::BooksAPI} < K8::Action
1635
- end
1741
+ fixture :mapping do
1742
+ |proc1, proc2|
1743
+ dp = K8::DefaultPatterns.new
1744
+ dp.register('id', '\d+', &proc1)
1745
+ dp.register(/_id\z/, '\d+', &proc2)
1746
+ K8::ActionMapping.new([
1747
+ ['/api', [
1748
+ ['/books', BooksAction],
1749
+ ['/books/{book_id}', BookCommentsAction],
1750
+ ]],
1751
+ ], default_patterns: dp)
1752
+ end
1636
1753
 
1637
- spec "[!k9bpm] raises error when target class not found." do
1638
- |mapping, testapi_books|
1639
- pr = proc { mapping.mount '/books', './testapi/books:MyBooksAPI999' }
1640
- ok {pr}.raise?(ArgumentError, "mount('./testapi/books:MyBooksAPI999'): no such action class.")
1754
+ spec "[!6f3vl] compiles urlpath mapping." do
1755
+ |mapping|
1756
+ mapping.instance_exec(self) do |_|
1757
+ _.ok {@urlpath_rexp}.is_a?(Regexp)
1758
+ _.ok {@urlpath_rexp} == Regexp.compile('
1759
+ \A/api
1760
+ (?: /books
1761
+ (?: /\d+(\z) | /\d+/edit(\z) )
1762
+ | /books/\d+
1763
+ (?: /comments(\z) | /comments/\d+(\z) )
1764
+ )
1765
+ \z'.gsub(/\s/, ''))
1766
+ _.ok {@fixed_endpoints.keys} == ["/api/books/", "/api/books/new"]
1767
+ _.ok {@variable_endpoints.map{|x| x[0..2] }} == [
1768
+ ["/api/books/{id}", BooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}],
1769
+ ["/api/books/{id}/edit", BooksAction, {:GET=>:do_edit}],
1770
+ ["/api/books/{book_id}/comments", BookCommentsAction, {:GET=>:do_comments}],
1771
+ ["/api/books/{book_id}/comments/{comment_id}", BookCommentsAction, {:GET=>:do_comment}],
1772
+ ]
1641
1773
  end
1774
+ end
1642
1775
 
1643
- spec "[!t6key] raises error when target class is not an action class." do
1644
- |mapping, testapi_books|
1645
- pr = proc { mapping.mount '/books', './testapi/books:MyBooksAPI::MyError' }
1646
- ok {pr}.raise?(ArgumentError, "mount('./testapi/books:MyBooksAPI::MyError'): not an action class.")
1776
+ spec "[!w45ad] can compile nested array." do
1777
+ |mapping, proc1, proc2|
1778
+ mapping.instance_exec(self) do |_|
1779
+ _.ok {@urlpath_rexp} == Regexp.compile('
1780
+ \A /api
1781
+ (?: /books
1782
+ (?: /\d+(\z) | /\d+/edit(\z) )
1783
+ | /books/\d+
1784
+ (?: /comments(\z) | /comments/\d+(\z) )
1785
+ )
1786
+ \z'.gsub(/\s/, ''))
1787
+ _.ok {@variable_endpoints} == [
1788
+ ["/api/books/{id}",
1789
+ BooksAction,
1790
+ {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete},
1791
+ /\A\/api\/books\/(\d+)\z/,
1792
+ ["id"], [proc1], (11..-1),
1793
+ ],
1794
+ ["/api/books/{id}/edit",
1795
+ BooksAction,
1796
+ {:GET=>:do_edit},
1797
+ /\A\/api\/books\/(\d+)\/edit\z/,
1798
+ ["id"], [proc1], (11..-6),
1799
+ ],
1800
+ ["/api/books/{book_id}/comments",
1801
+ BookCommentsAction,
1802
+ {:GET=>:do_comments},
1803
+ /\A\/api\/books\/(\d+)\/comments\z/,
1804
+ ["book_id"], [proc2], (11..-10),
1805
+ ],
1806
+ ["/api/books/{book_id}/comments/{comment_id}",
1807
+ BookCommentsAction,
1808
+ {:GET=>:do_comment},
1809
+ /\A\/api\/books\/(\d+)\/comments\/(\d+)\z/,
1810
+ ["book_id", "comment_id"], [proc2, proc2], nil,
1811
+ ],
1812
+ ]
1813
+ _.ok {@fixed_endpoints} == {
1814
+ "/api/books/" =>["/api/books/", BooksAction, {:GET=>:do_index, :POST=>:do_create}],
1815
+ "/api/books/new"=>["/api/books/new", BooksAction, {:GET=>:do_new}],
1816
+ }
1647
1817
  end
1818
+ end
1648
1819
 
1820
+ spec "[!z2iax] classifies urlpath contains any parameter as variable one." do
1821
+ |mapping, proc1, proc2|
1822
+ mapping.instance_exec(self) do |_|
1823
+ _.ok {@variable_endpoints} == [
1824
+ ["/api/books/{id}",
1825
+ BooksAction,
1826
+ {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete},
1827
+ /\A\/api\/books\/(\d+)\z/,
1828
+ ["id"], [proc1], (11..-1),
1829
+ ],
1830
+ ["/api/books/{id}/edit",
1831
+ BooksAction,
1832
+ {:GET=>:do_edit},
1833
+ /\A\/api\/books\/(\d+)\/edit\z/,
1834
+ ["id"], [proc1], (11..-6),
1835
+ ],
1836
+ ["/api/books/{book_id}/comments",
1837
+ BookCommentsAction,
1838
+ {:GET=>:do_comments},
1839
+ /\A\/api\/books\/(\d+)\/comments\z/,
1840
+ ["book_id"], [proc2], (11..-10),
1841
+ ],
1842
+ ["/api/books/{book_id}/comments/{comment_id}",
1843
+ BookCommentsAction,
1844
+ {:GET=>:do_comment},
1845
+ /\A\/api\/books\/(\d+)\/comments\/(\d+)\z/,
1846
+ ["book_id", "comment_id"], [proc2, proc2], nil,
1847
+ ],
1848
+ ]
1849
+ end
1649
1850
  end
1650
1851
 
1651
- spec "[!lvxyx] raises error when not an action class." do
1852
+ spec "[!rvdes] classifies urlpath contains no parameters as fixed one." do
1652
1853
  |mapping|
1653
- pr = proc { mapping.mount '/api', String }
1654
- ok {pr}.raise?(ArgumentError, "mount('/api'): Action class expected but got: String")
1854
+ mapping.instance_exec(self) do |_|
1855
+ _.ok {@fixed_endpoints} == {
1856
+ "/api/books/" => ["/api/books/", BooksAction, {:GET=>:do_index, :POST=>:do_create}],
1857
+ "/api/books/new" => ["/api/books/new", BooksAction, {:GET=>:do_new}],
1858
+ }
1859
+ end
1655
1860
  end
1656
1861
 
1657
- spec "[!30cib] raises error when action method is not defined in action class." do
1862
+ spec "[!6xwhq] builds action infos for each action methods." do
1863
+ class Ex_6xwhq < K8::Action
1864
+ mapping '', :GET=>:do_index, :POST=>:do_create
1865
+ mapping '/{id}', :GET=>:do_show, :PUT=>:do_update
1866
+ def do_index; end
1867
+ def do_create; end
1868
+ def do_show(id); end
1869
+ def do_update(id); end
1870
+ end
1871
+ ok {Ex_6xwhq[:do_create]} == nil
1872
+ ok {Ex_6xwhq[:do_update]} == nil
1873
+ #
1874
+ K8::ActionMapping.new([
1875
+ ['/test', [
1876
+ ['/example4', Ex_6xwhq],
1877
+ ]],
1878
+ ])
1879
+ #
1880
+ ok {Ex_6xwhq[:do_create]} != nil
1881
+ ok {Ex_6xwhq[:do_create].method} == :POST
1882
+ ok {Ex_6xwhq[:do_create].urlpath} == '/test/example4'
1883
+ ok {Ex_6xwhq[:do_update]} != nil
1884
+ ok {Ex_6xwhq[:do_update].method} == :PUT
1885
+ ok {Ex_6xwhq[:do_update].urlpath(123)} == '/test/example4/123'
1886
+ end
1887
+
1888
+ spec "[!wd2eb] accepts subclass of Action class." do
1889
+ _, proc1 = K8::DEFAULT_PATTERNS.lookup('id')
1890
+ _, proc2 = K8::DEFAULT_PATTERNS.lookup('book_id')
1891
+ mapping = K8::ActionMapping.new([
1892
+ ['/api/books', BooksAction],
1893
+ ['/api/books/{book_id}', BookCommentsAction],
1894
+ ])
1895
+ mapping.instance_exec(self) do |_|
1896
+ _.ok {@urlpath_rexp} == Regexp.compile('
1897
+ \A (?: /api/books
1898
+ (?: /\d+(\z) | /\d+/edit(\z) )
1899
+ | /api/books/\d+
1900
+ (?: /comments(\z) | /comments/\d+(\z) )
1901
+ )
1902
+ \z'.gsub(/\s/, ''))
1903
+ _.ok {@fixed_endpoints} == {
1904
+ "/api/books/" =>["/api/books/", BooksAction, {:GET=>:do_index, :POST=>:do_create}],
1905
+ "/api/books/new"=>["/api/books/new", BooksAction, {:GET=>:do_new}],
1906
+ }
1907
+ _.ok {@variable_endpoints} == [
1908
+ ["/api/books/{id}",
1909
+ BooksAction,
1910
+ {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete},
1911
+ /\A\/api\/books\/(\d+)\z/,
1912
+ ["id"], [proc1], (11..-1),
1913
+ ],
1914
+ ["/api/books/{id}/edit",
1915
+ BooksAction,
1916
+ {:GET=>:do_edit},
1917
+ /\A\/api\/books\/(\d+)\/edit\z/,
1918
+ ["id"], [proc1], (11..-6),
1919
+ ],
1920
+ ["/api/books/{book_id}/comments",
1921
+ BookCommentsAction,
1922
+ {:GET=>:do_comments},
1923
+ /\A\/api\/books\/(\d+)\/comments\z/,
1924
+ ["book_id"], [proc2], (11..-10),
1925
+ ],
1926
+ ["/api/books/{book_id}/comments/{comment_id}",
1927
+ BookCommentsAction,
1928
+ {:GET=>:do_comment},
1929
+ /\A\/api\/books\/(\d+)\/comments\/(\d+)\z/,
1930
+ ["book_id", "comment_id"], [proc2, proc2], nil,
1931
+ ],
1932
+ ]
1933
+ end
1934
+ end
1935
+
1936
+ spec "[!ue766] raises error when action method is not defined in action class." do
1658
1937
  |mapping|
1659
1938
  class ExampleAction3 < K8::Action
1660
1939
  mapping '', :GET=>:do_index, :POST=>:do_create
1661
1940
  def do_index; end
1662
1941
  end
1663
- pr = proc { mapping.mount '/example3', ExampleAction3 }
1942
+ pr = proc { K8::ActionMapping.new([['/example3', ExampleAction3]]) }
1664
1943
  expected_msg = ":POST=>:do_create: unknown action method in ExampleAction3."
1665
1944
  ok {pr}.raise?(K8::UnknownActionMethodError, expected_msg)
1666
1945
  end
1667
1946
 
1668
- spec "[!w8mee] returns self." do
1669
- |mapping|
1670
- ret = mapping.mount '/books', BooksAction
1671
- ok {ret}.same?(mapping)
1672
- end
1673
-
1674
- end
1675
-
1676
-
1677
- topic '#traverse()' do
1678
-
1679
- spec "[!ds0fp] yields with event (:enter, :map or :exit)." do
1680
- mapping = K8::ActionClassMapping.new
1681
- mapping.mount '/api', [
1682
- ['/books', BooksAction],
1683
- ['/books/{book_id}/comments', BookCommentsAction],
1684
- ]
1685
- mapping.mount '/admin', [
1686
- ['/books', AdminBooksAction],
1687
- ]
1947
+ spec "[!l2kz5] requires library when filepath and classname specified." do
1948
+ dirname = "test_l2kz5"
1949
+ filename = dirname + "/sample.rb"
1950
+ content = <<-END
1951
+ require 'keight'
1952
+ module Ex_l2kz5
1953
+ class Example_l2kz5 < K8::Action
1954
+ mapping '', :GET=>:do_index
1955
+ mapping '/{id}', :GET=>:do_show
1956
+ def do_index; end
1957
+ def do_show(id); end
1958
+ end
1959
+ end
1960
+ END
1961
+ Dir.mkdir(dirname) unless File.directory?(dirname)
1962
+ File.open(filename, 'w') {|f| f << content }
1963
+ at_end { File.unlink filename; Dir.rmdir dirname }
1688
1964
  #
1689
- arr = []
1690
- mapping.traverse do |*args|
1691
- arr << args
1965
+ _, proc1 = K8::DEFAULT_PATTERNS.lookup('id')
1966
+ _, proc2 = K8::DEFAULT_PATTERNS.lookup('book_id')
1967
+ mapping = K8::ActionMapping.new([
1968
+ ['/api/example', './test_l2kz5/sample:Ex_l2kz5::Example_l2kz5'],
1969
+ ])
1970
+ mapping.instance_exec(self) do |_|
1971
+ _.ok {@fixed_endpoints} == {
1972
+ "/api/example"=>["/api/example", Ex_l2kz5::Example_l2kz5, {:GET=>:do_index}],
1973
+ }
1974
+ _.ok {@variable_endpoints} == [
1975
+ ["/api/example/{id}", Ex_l2kz5::Example_l2kz5, {:GET=>:do_show}, /\A\/api\/example\/(\d+)\z/, ["id"], [proc1], (13..-1)],
1976
+ ]
1692
1977
  end
1693
- ok {arr[0]} == [:enter, "", "/api", [["/books", BooksAction], ["/books/{book_id}/comments", BookCommentsAction]], nil]
1694
- ok {arr[1]} == [:enter, "/api", "/books", BooksAction, nil]
1695
- ok {arr[2]} == [:map, "/api/books", "/", BooksAction, {:GET=>:do_index, :POST=>:do_create}]
1696
- ok {arr[3]} == [:map, "/api/books", "/new", BooksAction, {:GET=>:do_new}]
1697
- ok {arr[4]} == [:map, "/api/books", "/{id}", BooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}]
1698
- ok {arr[5]} == [:map, "/api/books", "/{id}/edit", BooksAction, {:GET=>:do_edit}]
1699
- ok {arr[6]} == [:exit, "/api", "/books", BooksAction, nil]
1700
- ok {arr[7]} == [:enter, "/api", "/books/{book_id}/comments", BookCommentsAction, nil]
1701
- ok {arr[8]} == [:map, "/api/books/{book_id}/comments", "/comments", BookCommentsAction, {:GET=>:do_comments}]
1702
- ok {arr[9]} == [:map, "/api/books/{book_id}/comments", "/comments/{comment_id}", BookCommentsAction, {:GET=>:do_comment}]
1703
- ok {arr[10]} == [:exit, "/api", "/books/{book_id}/comments", BookCommentsAction, nil]
1704
- ok {arr[11]} == [:exit, "", "/api", [["/books", BooksAction], ["/books/{book_id}/comments", BookCommentsAction]], nil]
1705
- ok {arr[12]} == [:enter, "", "/admin", [["/books", AdminBooksAction]], nil]
1706
- ok {arr[13]} == [:enter, "/admin", "/books", AdminBooksAction, nil]
1707
- ok {arr[14]} == [:map, "/admin/books", "/", AdminBooksAction, {:GET=>:do_index, :POST=>:do_create}]
1708
- ok {arr[15]} == [:map, "/admin/books", "/new", AdminBooksAction, {:GET=>:do_new}]
1709
- ok {arr[16]} == [:map, "/admin/books", "/{id}", AdminBooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}]
1710
- ok {arr[17]} == [:map, "/admin/books", "/{id}/edit", AdminBooksAction, {:GET=>:do_edit}]
1711
- ok {arr[18]} == [:exit, "/admin", "/books", AdminBooksAction, nil]
1712
- ok {arr[19]} == [:exit, "", "/admin", [["/books", AdminBooksAction]], nil]
1713
- ok {arr[20]} == nil
1714
1978
  end
1715
1979
 
1716
- end
1717
-
1718
-
1719
- topic '#each_mapping()' do
1720
-
1721
- spec "[!driqt] yields full urlpath pattern, action class and action methods." do
1722
- mapping = K8::ActionClassMapping.new
1723
- mapping.mount '/api', [
1724
- ['/books', BooksAction],
1725
- ['/books/{book_id}', BookCommentsAction],
1726
- ]
1727
- mapping.mount '/admin', [
1728
- ['/books', AdminBooksAction],
1729
- ]
1730
- #
1731
- arr = []
1732
- mapping.each_mapping do |*args|
1733
- arr << args
1980
+ spec "[!irt5g] raises TypeError when unknown object specified." do
1981
+ pr = proc do
1982
+ mapping = K8::ActionMapping.new([
1983
+ ['/api/example', {:GET=>:do_index}],
1984
+ ])
1734
1985
  end
1735
- ok {arr} == [
1736
- ["/api/books/", BooksAction, {:GET=>:do_index, :POST=>:do_create}],
1737
- ["/api/books/new", BooksAction, {:GET=>:do_new}],
1738
- ["/api/books/{id}", BooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}],
1739
- ["/api/books/{id}/edit", BooksAction, {:GET=>:do_edit}],
1740
- #
1741
- ["/api/books/{book_id}/comments", BookCommentsAction, {:GET=>:do_comments}],
1742
- ["/api/books/{book_id}/comments/{comment_id}", BookCommentsAction, {:GET=>:do_comment}],
1743
- #
1744
- ["/admin/books/", AdminBooksAction, {:GET=>:do_index, :POST=>:do_create}],
1745
- ["/admin/books/new", AdminBooksAction, {:GET=>:do_new}],
1746
- ["/admin/books/{id}", AdminBooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}],
1747
- ["/admin/books/{id}/edit", AdminBooksAction, {:GET=>:do_edit}],
1748
- ]
1986
+ ok {pr}.raise?(TypeError, "Action class or nested array expected, but got {:GET=>:do_index}")
1749
1987
  end
1750
1988
 
1751
- end
1752
-
1753
-
1754
- end
1755
-
1756
-
1757
- topic K8::ActionFinder do
1758
-
1759
- fixture :router do |class_mapping, default_patterns|
1760
- K8::ActionFinder.new(class_mapping, default_patterns, urlpath_cache_size: 0)
1761
- end
1762
-
1763
- fixture :class_mapping do
1764
- mapping = K8::ActionClassMapping.new
1765
- mapping.mount '/api', [
1766
- ['/books', BooksAction],
1767
- ['/books/{book_id}', BookCommentsAction],
1768
- ]
1769
- mapping.mount '/admin', [
1770
- ['/books', AdminBooksAction],
1771
- ]
1772
- mapping
1773
- end
1774
-
1775
- fixture :default_patterns do |proc_obj1, proc_obj2|
1776
- default_patterns = K8::DefaultPatterns.new
1777
- default_patterns.register('id', '\d+', &proc_obj1)
1778
- default_patterns.register(/_id\z/, '\d+', &proc_obj2)
1779
- default_patterns
1780
- end
1781
-
1782
- fixture :proc_obj1 do
1783
- proc {|x| x.to_i }
1784
- end
1989
+ spec "[!bcgc9] skips classes which have only fixed urlpaths." do
1990
+ klass = Class.new(K8::Action) do
1991
+ mapping '/', :GET=>:do_index
1992
+ mapping '/new', :GET=>:do_new
1993
+ def do_index; end
1994
+ def do_new; end
1995
+ end
1996
+ mapping = K8::ActionMapping.new([
1997
+ ['/api', [
1998
+ ['/books', BooksAction],
1999
+ ['/samples', klass],
2000
+ ['/books/{book_id}', BookCommentsAction],
2001
+ ]],
2002
+ ])
2003
+ mapping.instance_exec(self) do |_|
2004
+ _.ok {@urlpath_rexp} == Regexp.compile('
2005
+ \A /api
2006
+ (?: /books
2007
+ (?:/\d+(\z)|/\d+/edit(\z))
2008
+ | /books/\d+
2009
+ (?:/comments(\z)|/comments/\d+(\z))
2010
+ )
2011
+ \z'.gsub(/\s+/, ''))
2012
+ _.ok {@fixed_endpoints['/api/samples/']} == ["/api/samples/", klass, {:GET=>:do_index}]
2013
+ _.ok {@fixed_endpoints['/api/samples/new']} == ["/api/samples/new", klass, {:GET=>:do_new}]
2014
+ end
2015
+ end
2016
+
2017
+ spec "[!169ad] removes unnecessary grouping." do
2018
+ klass = Class.new(K8::Action) do
2019
+ mapping '/{id}', :GET=>:do_show
2020
+ def do_show(id); end
2021
+ end
2022
+ mapping = K8::ActionMapping.new([
2023
+ ['/api', [
2024
+ ['/test', klass],
2025
+ ]],
2026
+ ])
2027
+ mapping.instance_exec(self) do |_|
2028
+ #_.ok {@urlpath_rexp} == %r'\A(?:/api(?:/test(?:/\d+(\z))))\z'
2029
+ _.ok {@urlpath_rexp} == %r'\A/api/test/\d+(\z)\z'
2030
+ end
2031
+ end
1785
2032
 
1786
- fixture :proc_obj2 do
1787
- proc {|x| x.to_i }
1788
2033
  end
1789
2034
 
1790
2035
 
1791
- topic '#initialize()' do
2036
+ topic '#lookup()' do
1792
2037
 
1793
- spec "[!dnu4q] calls '#_construct()'." do
1794
- |router|
1795
- ok {router.instance_variable_get('@rexp')} != nil
1796
- ok {router.instance_variable_get('@list')} != nil
1797
- ok {router.instance_variable_get('@dict')} != nil
2038
+ fixture :proc1 do
2039
+ proc {|x| x.to_i }
1798
2040
  end
1799
2041
 
1800
- spec "[!wb9l8] enables urlpath cache when urlpath_cache_size > 0." do
1801
- |class_mapping, default_patterns|
1802
- args = [class_mapping, default_patterns]
1803
- router = K8::ActionFinder.new(*args, urlpath_cache_size: 1)
1804
- ok {router.instance_variable_get('@urlpath_cache')} == {}
1805
- router = K8::ActionFinder.new(*args, urlpath_cache_size: 0)
1806
- ok {router.instance_variable_get('@urlpath_cache')} == nil
2042
+ fixture :mapping do
2043
+ |proc1|
2044
+ dp = K8::DefaultPatterns.new
2045
+ dp.register('id', '\d+', &proc1)
2046
+ dp.register(/_id$/, '\d+', &proc1)
2047
+ K8::ActionMapping.new([
2048
+ ['/api', [
2049
+ ['/books', BooksAction],
2050
+ ['/books/{book_id}', BookCommentsAction],
2051
+ ]],
2052
+ ], default_patterns: dp, urlpath_cache_size: 3)
1807
2053
  end
1808
2054
 
1809
- end
1810
-
1811
-
1812
- topic '#_compile()' do
1813
-
1814
- spec "[!izsbp] compiles urlpath pattern into regexp string and param names." do
1815
- |router, proc_obj1|
1816
- router.instance_exec(self) do |_|
1817
- ret = _compile('/', '\A', '\z', true)
1818
- _.ok {ret} == ['\A/\z', [], []]
1819
- ret = _compile('/books', '\A', '\z', true)
1820
- _.ok {ret} == ['\A/books\z', [], []]
1821
- ret = _compile('/books/{id:\d*}', '\A', '\z', true)
1822
- _.ok {ret} == ['\A/books/(\d*)\z', ["id"], [nil]]
1823
- ret = _compile('/books/{id}/authors/{name}', '\A', '\z', true)
1824
- _.ok {ret} == ['\A/books/(\d+)/authors/([^/]+?)\z', ["id", "name"], [proc_obj1, nil]]
1825
- end
2055
+ spec "[!jyxlm] returns action class and methods, parameter names and values." do
2056
+ |mapping|
2057
+ tuple = mapping.lookup('/api/books/123')
2058
+ ok {tuple} == [BooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}, ['id'], [123]]
2059
+ tuple = mapping.lookup('/api/books/123/comments/999')
2060
+ ok {tuple} == [BookCommentsAction, {:GET=>:do_comment}, ['book_id', 'comment_id'], [123, 999]]
1826
2061
  end
1827
2062
 
1828
- spec "[!olps9] allows '{}' in regular expression." do
1829
- |router|
1830
- router.instance_exec(self) do |_|
1831
- ret = _compile('/log/{date:\d{4}-\d{2}-\d{2}}', '', '', true)
1832
- _.ok {ret} == ['/log/(\d{4}-\d{2}-\d{2})', ["date"], [nil]]
2063
+ spec "[!j34yh] finds from fixed urlpaths at first." do
2064
+ |mapping|
2065
+ mapping.instance_exec(self) do |_|
2066
+ _.ok {lookup('/books')} == nil
2067
+ tuple = @fixed_endpoints['/api/books/']
2068
+ _.ok {tuple} != nil
2069
+ @fixed_endpoints['/books'] = tuple
2070
+ expected = [BooksAction, {:GET=>:do_index, :POST=>:do_create}, [], []]
2071
+ _.ok {lookup('/books')} != nil
2072
+ _.ok {lookup('/books')} == expected
2073
+ _.ok {lookup('/api/books/')} == expected
1833
2074
  end
1834
2075
  end
1835
2076
 
1836
- spec "[!vey08] uses grouping when 4th argument is true." do
1837
- |router, proc_obj1|
1838
- router.instance_exec(self) do |_|
1839
- ret = _compile('/books/{id:\d*}', '\A', '\z', true)
1840
- _.ok {ret} == ['\A/books/(\d*)\z', ["id"], [nil]]
1841
- ret = _compile('/books/{id}/authors/{name}', '\A', '\z', true)
1842
- _.ok {ret} == ['\A/books/(\d+)/authors/([^/]+?)\z', ["id", "name"], [proc_obj1, nil]]
1843
- end
2077
+ spec "[!95q61] finds from variable urlpath patterns when not found in fixed ones." do
2078
+ |mapping|
2079
+ ok {mapping.lookup('/api/books/123')} == \
2080
+ [
2081
+ BooksAction,
2082
+ {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete},
2083
+ ["id"],
2084
+ [123],
2085
+ ]
2086
+ ok {mapping.lookup('/api/books/123/comments/999')} == \
2087
+ [
2088
+ BookCommentsAction,
2089
+ {:GET=>:do_comment},
2090
+ ["book_id", "comment_id"],
2091
+ [123, 999],
2092
+ ]
1844
2093
  end
1845
2094
 
1846
- spec "[!2zil2] don't use grouping when 4th argument is false." do
1847
- |router, proc_obj1|
1848
- router.instance_exec(self) do |_|
1849
- ret = _compile('/books/{id:\d*}', '\A', '\z', false)
1850
- _.ok {ret} == ['\A/books/\d*\z', ["id"], [nil]]
1851
- ret = _compile('/books/{id}/authors/{name}', '\A', '\z', false)
1852
- _.ok {ret} == ['\A/books/\d+/authors/[^/]+?\z', ["id", "name"], [proc_obj1, nil]]
1853
- end
2095
+ spec "[!sos5i] returns nil when request path not matched to urlpath patterns." do
2096
+ |mapping|
2097
+ ok {mapping.lookup('/api/booking')} == nil
1854
2098
  end
1855
2099
 
1856
- spec %q"[!rda92] ex: '/{id:\d+}' -> '/(\d+)'" do
1857
- |router|
1858
- router.instance_exec(self) do |_|
1859
- ret = _compile('/api/{ver:\d+}', '', '', true)
1860
- _.ok {ret} == ['/api/(\d+)', ["ver"], [nil]]
1861
- end
2100
+ spec "[!1k1k5] converts urlpath param values by converter procs." do
2101
+ |mapping|
2102
+ tuple = mapping.lookup('/api/books/123')
2103
+ ok {tuple[2..3]} == [['id'], [123]]
2104
+ tuple = mapping.lookup('/api/books/123/comments/999')
2105
+ ok {tuple[2..3]} == [['book_id', 'comment_id'], [123, 999]]
1862
2106
  end
1863
2107
 
1864
- spec %q"[!jyz2g] ex: '/{:\d+}' -> '/\d+'" do
1865
- |router|
1866
- router.instance_exec(self) do |_|
1867
- ret = _compile('/api/{:\d+}', '', '', true)
1868
- _.ok {ret} == ['/api/\d+', [], []]
2108
+ spec "[!uqwr7] stores result into cache if cache is enabled." do
2109
+ |mapping|
2110
+ tuple = mapping.lookup('/api/books/111')
2111
+ mapping.instance_exec(self) do |_|
2112
+ _.ok {@urlpath_lru_cache} == {'/api/books/111' => tuple}
1869
2113
  end
1870
2114
  end
1871
2115
 
1872
- spec %q"[!hy3y5] ex: '/{:xx|yy}' -> '/(?:xx|yy)'" do
1873
- |router|
1874
- router.instance_exec(self) do |_|
1875
- ret = _compile('/api/{:2014|2015}', '', '', true)
1876
- _.ok {ret} == ['/api/(?:2014|2015)', [], []]
2116
+ spec "[!3ps5g] deletes item from cache when cache size over limit." do
2117
+ |mapping|
2118
+ mapping.lookup('/api/books/1')
2119
+ mapping.lookup('/api/books/2')
2120
+ mapping.lookup('/api/books/3')
2121
+ mapping.lookup('/api/books/4')
2122
+ mapping.lookup('/api/books/5')
2123
+ mapping.instance_exec(self) do |_|
2124
+ _.ok {@urlpath_lru_cache.length} == 3
1877
2125
  end
1878
2126
  end
1879
2127
 
1880
- spec %q"[!gunsm] ex: '/{id:xx|yy}' -> '/(xx|yy)'" do
1881
- |router|
1882
- router.instance_exec(self) do |_|
1883
- ret = _compile('/api/{year:2014|2015}', '', '', true)
1884
- _.ok {ret} == ['/api/(2014|2015)', ["year"], [nil]]
2128
+ spec "[!uqwr7] uses LRU as cache algorithm." do
2129
+ |mapping|
2130
+ mapping.instance_exec(self) do |_|
2131
+ t1 = lookup('/api/books/1')
2132
+ t2 = lookup('/api/books/2')
2133
+ t3 = lookup('/api/books/3')
2134
+ _.ok {@urlpath_lru_cache.values} == [t1, t2, t3]
2135
+ t4 = lookup('/api/books/4')
2136
+ _.ok {@urlpath_lru_cache.values} == [t2, t3, t4]
2137
+ t5 = lookup('/api/books/5')
2138
+ _.ok {@urlpath_lru_cache.values} == [t3, t4, t5]
2139
+ #
2140
+ lookup('/api/books/4')
2141
+ _.ok {@urlpath_lru_cache.values} == [t3, t5, t4]
2142
+ lookup('/api/books/3')
2143
+ _.ok {@urlpath_lru_cache.values} == [t5, t4, t3]
1885
2144
  end
1886
2145
  end
1887
2146
 
1888
2147
  end
1889
2148
 
1890
2149
 
1891
- topic '#_construct()' do
1892
-
1893
- spec "[!956fi] builds regexp object for variable urlpaths (= containing urlpath params)." do
1894
- |router|
1895
- rexp = router.instance_variable_get('@rexp')
1896
- ok {rexp}.is_a?(Regexp)
1897
- ok {rexp.source} == '
1898
- \A
1899
- (?:
1900
- /api
1901
- (?:
1902
- /books
1903
- (?: /\d+(\z) | /\d+/edit(\z) )
1904
- |
1905
- /books/\d+
1906
- (?: /comments(\z) | /comments/\d+(\z) )
1907
- )
1908
- |
1909
- /admin
1910
- (?:
1911
- /books
1912
- (?: /\d+(\z) | /\d+/edit(\z) )
1913
- )
1914
- )
1915
- '.gsub(/\s+/, '')
1916
- end
1917
-
1918
- spec "[!6tgj5] builds dict of fixed urlpaths (= no urlpath params)." do
1919
- |router|
1920
- dict = router.instance_variable_get('@dict')
1921
- ok {dict} == {
1922
- '/api/books/' => [BooksAction, {:GET=>:do_index, :POST=>:do_create}],
1923
- '/api/books/new' => [BooksAction, {:GET=>:do_new}],
1924
- '/admin/books/' => [AdminBooksAction, {:GET=>:do_index, :POST=>:do_create}],
1925
- '/admin/books/new' => [AdminBooksAction, {:GET=>:do_new}],
1926
- }
1927
- end
2150
+ topic '#_compile_urlpath_pat()' do
1928
2151
 
1929
- spec "[!sl9em] builds list of variable urlpaths (= containing urlpath params)." do
1930
- |router, proc_obj1, proc_obj2|
1931
- list = router.instance_variable_get('@list')
1932
- ok {list}.is_a?(Array)
1933
- ok {list.length} == 6
1934
- ok {list[0]} == [
1935
- /\A\/api\/books\/(\d+)\z/,
1936
- ["id"], [proc_obj1],
1937
- BooksAction,
1938
- {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete},
1939
- ]
1940
- ok {list[1]} == [
1941
- /\A\/api\/books\/(\d+)\/edit\z/,
1942
- ["id"], [proc_obj1],
1943
- BooksAction,
1944
- {:GET=>:do_edit},
1945
- ]
1946
- ok {list[2]} == [
1947
- /\A\/api\/books\/(\d+)\/comments\z/,
1948
- ["book_id"], [proc_obj2],
1949
- BookCommentsAction,
1950
- {:GET=>:do_comments},
1951
- ]
1952
- ok {list[3]} == [
1953
- /\A\/api\/books\/(\d+)\/comments\/(\d+)\z/,
1954
- ["book_id", "comment_id"], [proc_obj2, proc_obj2],
1955
- BookCommentsAction,
1956
- {:GET=>:do_comment},
1957
- ]
1958
- ok {list[4]} == [
1959
- /\A\/admin\/books\/(\d+)\z/,
1960
- ["id"], [proc_obj1],
1961
- AdminBooksAction,
1962
- {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete},
1963
- ]
1964
- ok {list[5]} == [
1965
- /\A\/admin\/books\/(\d+)\/edit\z/,
1966
- ["id"], [proc_obj1],
1967
- AdminBooksAction,
1968
- {:GET=>:do_edit},
1969
- ]
1970
- ok {list[6]} == nil
2152
+ fixture :proc1 do
2153
+ proc {|x| x.to_i }
1971
2154
  end
1972
2155
 
1973
- end
1974
-
1975
-
1976
- topic '#find()' do
1977
-
1978
- spec "[!ndktw] returns action class, action methods, urlpath names and values." do
1979
- |router|
1980
- ok {router.find('/api/books/')} == [
1981
- BooksAction, {:GET=>:do_index, :POST=>:do_create}, [], [],
1982
- ]
1983
- ok {router.find('/api/books/123')} == [
1984
- BooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}, ["id"], [123],
1985
- ]
2156
+ fixture :default_patterns do
2157
+ |proc1|
2158
+ x = K8::DefaultPatterns.new
2159
+ x.register('id', '\d+', &proc1)
2160
+ x.register(/_id$/, '\d+', &proc1)
2161
+ x
1986
2162
  end
1987
2163
 
1988
- spec "[!p18w0] urlpath params are empty when matched to fixed urlpath pattern." do
1989
- |router|
1990
- ok {router.find('/admin/books/')} == [
1991
- AdminBooksAction, {:GET=>:do_index, :POST=>:do_create}, [], [],
1992
- ]
2164
+ spec "[!awfgs] returns regexp string, param names, and converter procs." do
2165
+ |default_patterns, proc1|
2166
+ mapping = K8::ActionMapping.new([], default_patterns: default_patterns)
2167
+ mapping.instance_exec(self) do |_|
2168
+ #
2169
+ actual = _compile_urlpath_pat('/books/{id}')
2170
+ _.ok {actual} == ['/books/\d+', ['id'], [proc1]]
2171
+ #
2172
+ actual = _compile_urlpath_pat('/books/{book_id}/comments/{comment_id}')
2173
+ _.ok {actual} == ['/books/\d+/comments/\d+', ['book_id', 'comment_id'], [proc1, proc1]]
2174
+ #
2175
+ actual = _compile_urlpath_pat('/books/{id:[0-9]+}')
2176
+ _.ok {actual} == ['/books/[0-9]+', ['id'], [nil]]
2177
+ end
1993
2178
  end
1994
2179
 
1995
- spec "[!t6yk0] urlpath params are not empty when matched to variable urlpath apttern." do
1996
- |router|
1997
- ok {router.find('/admin/books/123')} == [
1998
- AdminBooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}, ["id"], [123],
1999
- ]
2000
- ok {router.find('/api/books/123/comments/999')} == [
2001
- BookCommentsAction, {:GET=>:do_comment}, ["book_id", "comment_id"], [123, 999],
2002
- ]
2180
+ spec "[!bi7gr] captures urlpath params when 2nd argument is truthy." do
2181
+ |default_patterns, proc1|
2182
+ mapping = K8::ActionMapping.new([], default_patterns: default_patterns)
2183
+ mapping.instance_exec(self) do |_|
2184
+ actual = _compile_urlpath_pat('/books/{id}', true)
2185
+ _.ok {actual} == ['/books/(\d+)', ['id'], [proc1]]
2186
+ #
2187
+ actual = _compile_urlpath_pat('/books/{book_id}/comments/{comment_id}', true)
2188
+ _.ok {actual} == ['/books/(\d+)/comments/(\d+)', ['book_id', 'comment_id'], [proc1, proc1]]
2189
+ #
2190
+ actual = _compile_urlpath_pat('/books/{id:[0-9]+}', true)
2191
+ _.ok {actual} == ['/books/([0-9]+)', ['id'], [nil]]
2192
+ end
2003
2193
  end
2004
2194
 
2005
- spec "[!0o3fe] converts urlpath param values according to default patterns." do
2006
- |router|
2007
- ok {router.find('/api/books/123')[-1]} == [123]
2008
- ok {router.find('/api/books/123/comments/999')[-1]} == [123, 999]
2195
+ spec "[!mprbx] ex: '/{id:x|y}' -> '/(x|y)', '/{:x|y}' -> '/(?:x|y)'" do
2196
+ |default_patterns|
2197
+ mapping = K8::ActionMapping.new([], default_patterns: default_patterns)
2198
+ mapping.instance_exec(self) do |_|
2199
+ _.ok {_compile_urlpath_pat('/item/{key:x|y}', true)} == ['/item/(x|y)', ['key'], [nil]]
2200
+ _.ok {_compile_urlpath_pat('/item/{key:x|y}', false)} == ['/item/(?:x|y)', ['key'], [nil]]
2201
+ _.ok {_compile_urlpath_pat('/item/{:x|y}', true)} == ['/item/(?:x|y)', [], []]
2202
+ _.ok {_compile_urlpath_pat('/item/{:x|y}', false)} == ['/item/(?:x|y)', [], []]
2203
+ end
2009
2204
  end
2010
2205
 
2011
- spec "[!ps5jm] returns nil when not matched to any urlpath patterns." do
2012
- |router|
2013
- ok {router.find('/admin/authors')} == nil
2206
+ spec "[!iln54] param names and conveter procs are nil when no urlpath params." do
2207
+ |default_patterns|
2208
+ mapping = K8::ActionMapping.new([], default_patterns: default_patterns)
2209
+ mapping.instance_exec(self) do |_|
2210
+ actual = _compile_urlpath_pat('/books/new')
2211
+ _.ok {actual} == ['/books/new', nil, nil]
2212
+ end
2014
2213
  end
2015
2214
 
2016
- spec "[!gzy2w] fetches variable urlpath from LRU cache if LRU cache is enabled." do
2017
- |class_mapping, default_patterns|
2018
- router = K8::ActionFinder.new(class_mapping, default_patterns, urlpath_cache_size: 3)
2019
- router.instance_exec(self) do |_|
2020
- arr1 = find('/api/books/1')
2021
- arr2 = find('/api/books/2')
2022
- arr3 = find('/api/books/3')
2023
- _.ok {@urlpath_cache.keys} == ['/api/books/1', '/api/books/2', '/api/books/3']
2024
- #
2025
- _.ok {find('/api/books/2')} == arr2
2026
- _.ok {@urlpath_cache.keys} == ['/api/books/1', '/api/books/3', '/api/books/2']
2027
- _.ok {find('/api/books/1')} == arr1
2028
- _.ok {@urlpath_cache.keys} == ['/api/books/3', '/api/books/2', '/api/books/1']
2215
+ spec "[!lhtiz] skips empty param name." do
2216
+ |default_patterns, proc1|
2217
+ K8::ActionMapping.new([], default_patterns: default_patterns).instance_exec(self) do |_|
2218
+ actual = _compile_urlpath_pat('/api/{:\d+}/books')
2219
+ _.ok {actual} == ['/api/\d+/books', [], []]
2220
+ actual = _compile_urlpath_pat('/api/{:\d+}/books/{id}')
2221
+ _.ok {actual} == ['/api/\d+/books/\d+', ['id'], [proc1]]
2029
2222
  end
2030
2223
  end
2031
2224
 
2032
- spec "[!v2zbx] caches variable urlpath into LRU cache if cache is enabled." do
2033
- |class_mapping, default_patterns|
2034
- router = K8::ActionFinder.new(class_mapping, default_patterns, urlpath_cache_size: 3)
2035
- router.instance_exec(self) do |_|
2036
- arr1 = find('/api/books/1')
2037
- arr2 = find('/api/books/2')
2038
- _.ok {@urlpath_cache.keys} == ['/api/books/1', '/api/books/2']
2039
- _.ok {find('/api/books/1')} == arr1
2040
- _.ok {find('/api/books/2')} == arr2
2225
+ spec "[!66zas] skips param name starting with '_'." do
2226
+ |default_patterns, proc1|
2227
+ K8::ActionMapping.new([], default_patterns: default_patterns).instance_exec(self) do |_|
2228
+ actual = _compile_urlpath_pat('/api/{_ver:\d+}/books')
2229
+ _.ok {actual} == ['/api/\d+/books', [], []]
2230
+ actual = _compile_urlpath_pat('/api/{_ver:\d+}/books/{id}')
2231
+ _.ok {actual} == ['/api/\d+/books/\d+', ['id'], [proc1]]
2041
2232
  end
2042
2233
  end
2043
2234
 
2044
- spec "[!nczw6] LRU cache size doesn't growth over max cache size." do
2045
- |class_mapping, default_patterns|
2046
- router = K8::ActionFinder.new(class_mapping, default_patterns, urlpath_cache_size: 3)
2047
- router.instance_exec(self) do |_|
2048
- arr1 = find('/api/books/1')
2049
- arr2 = find('/api/books/2')
2050
- arr3 = find('/api/books/3')
2051
- arr3 = find('/api/books/4')
2052
- arr3 = find('/api/books/5')
2053
- _.ok {@urlpath_cache.length} == 3
2054
- _.ok {@urlpath_cache.keys} == ['/api/books/3', '/api/books/4', '/api/books/5']
2235
+ spec "[!92jcn] '{' and '}' are available in urlpath param pattern." do
2236
+ |default_patterns, proc1|
2237
+ K8::ActionMapping.new([], default_patterns: default_patterns).instance_exec(self) do |_|
2238
+ actual = _compile_urlpath_pat('/blog/{date:\d{4}-\d{2}-\d{2}}')
2239
+ _.ok {actual} == ['/blog/\d{4}-\d{2}-\d{2}', ['date'], [nil]]
2055
2240
  end
2056
2241
  end
2057
2242
 
2058
2243
  end
2059
2244
 
2060
2245
 
2061
- end
2062
-
2063
-
2064
- topic K8::ActionRouter do
2065
-
2066
-
2067
- topic '#initialize()' do
2246
+ topic '#_require_action_class()' do
2068
2247
 
2069
- spec "[!l1elt] saves finder options." do
2070
- router = K8::ActionRouter.new(urlpath_cache_size: 100)
2071
- router.instance_exec(self) do |_|
2072
- _.ok {@finder_opts} == {:urlpath_cache_size=>100}
2248
+ spec "[!px9jy] requires file and finds class object." do
2249
+ filename = 'test_px9jy.rb'
2250
+ content = "class Ex_px9jy < K8::Action; end\n"
2251
+ File.open(filename, 'w') {|f| f << content }
2252
+ at_end { File.unlink filename }
2253
+ K8::ActionMapping.new([]).instance_exec(self) do |_|
2254
+ _.ok { _require_action_class './test_px9jy:Ex_px9jy' } == Ex_px9jy
2073
2255
  end
2074
2256
  end
2075
2257
 
2076
- end
2077
-
2078
-
2079
- topic '#register()' do
2080
-
2081
- spec "[!boq80] registers urlpath param pattern and converter." do
2082
- router = K8::ActionRouter.new()
2083
- router.register(/_hex\z/, '[a-f0-9]+') {|x| x.to_i(16) }
2084
- router.instance_exec(self) do |_|
2085
- ret = @default_patterns.lookup('code_hex')
2086
- _.ok {ret.length} == 2
2087
- _.ok {ret[0]} == '[a-f0-9]+'
2088
- _.ok {ret[1]}.is_a?(Proc)
2089
- _.ok {ret[1].call('ff')} == 255
2258
+ spec "[!dlcks] don't rescue LoadError when it is not related to argument." do
2259
+ filename = 'test_dlcks.rb'
2260
+ content = "require 'homhomhom'; class Ex_dlcks < K8::Action; end\n"
2261
+ File.open(filename, 'w') {|f| f << content }
2262
+ at_end { File.unlink filename }
2263
+ K8::ActionMapping.new([]).instance_exec(self) do |_|
2264
+ pr = proc { _require_action_class './test_dlcks:Ex_dlcks' }
2265
+ _.ok {pr}.raise?(LoadError, "cannot load such file -- homhomhom")
2266
+ _.ok {pr.exception.path} == "homhomhom"
2090
2267
  end
2091
2268
  end
2092
2269
 
2093
- end
2094
-
2095
-
2096
- topic '#mount()' do
2097
-
2098
- spec "[!uc996] mouts action class to urlpath." do
2099
- router = K8::ActionRouter.new()
2100
- router.mount('/api/books', BooksAction)
2101
- ret = router.find('/api/books/')
2102
- ok {ret} != nil
2103
- ok {ret[0]} == BooksAction
2270
+ spec "[!mngjz] raises error when failed to load file." do
2271
+ filename = 'test_mngjz.rb'
2272
+ K8::ActionMapping.new([]).instance_exec(self) do |_|
2273
+ pr = proc { _require_action_class './test_mngjz:Ex_mngjz' }
2274
+ _.ok {pr}.raise?(LoadError, "'./test_mngjz:Ex_mngjz': cannot load './test_mngjz'.")
2275
+ end
2104
2276
  end
2105
2277
 
2106
- spec "[!trs6w] removes finder object." do
2107
- router = K8::ActionRouter.new()
2108
- router.instance_exec(self) do |_|
2109
- @finder = true
2110
- _.ok {@finder} == true
2111
- mount('/api/books', BooksAction)
2112
- _.ok {@finder} == nil
2278
+ spec "[!8n6pf] class name may have module prefix name." do
2279
+ filename = 'test_8n6pf.rb'
2280
+ content = "module Ex_8n6pf; class Sample < K8::Action; end; end\n"
2281
+ File.open(filename, 'w') {|f| f << content }
2282
+ at_end { File.unlink filename }
2283
+ K8::ActionMapping.new([]).instance_exec(self) do |_|
2284
+ _.ok { _require_action_class './test_8n6pf:Ex_8n6pf::Sample' } == Ex_8n6pf::Sample
2113
2285
  end
2114
2286
  end
2115
2287
 
2116
- end
2117
-
2288
+ spec "[!6lv7l] raises error when action class not found." do
2289
+ filename = 'test_6lv7l.rb'
2290
+ content = "module Ex_6lv7l; class Sample_6lv7l < K8::Action; end; end\n"
2291
+ File.open(filename, 'w') {|f| f << content }
2292
+ at_end { File.unlink filename }
2293
+ K8::ActionMapping.new([]).instance_exec(self) do |_|
2294
+ pr = proc { _require_action_class './test_6lv7l:Ex_6lv7l::Sample' }
2295
+ _.ok {pr}.raise?(NameError, "'./test_6lv7l:Ex_6lv7l::Sample': class not found (Ex_6lv7l::Sample).")
2296
+ end
2297
+ end
2118
2298
 
2119
- topic '#each_mapping()' do
2299
+ spec "[!thf7t] raises TypeError when not a class." do
2300
+ filename = 'test_thf7t.rb'
2301
+ content = "Ex_thf7t = 'XXX'\n"
2302
+ File.open(filename, 'w') {|f| f << content }
2303
+ at_end { File.unlink filename }
2304
+ K8::ActionMapping.new([]).instance_exec(self) do |_|
2305
+ pr = proc { _require_action_class './test_thf7t:Ex_thf7t' }
2306
+ _.ok {pr}.raise?(TypeError, "'./test_thf7t:Ex_thf7t': class name expected but got \"XXX\".")
2307
+ end
2308
+ end
2120
2309
 
2121
- spec "[!2kq9h] yields with full urlpath pattern, action class and action methods." do
2122
- router = K8::ActionRouter.new()
2123
- router.mount '/api', [
2124
- ['/books', BooksAction],
2125
- ['/books/{book_id}', BookCommentsAction],
2126
- ]
2127
- router.mount '/admin', [
2128
- ['/books', AdminBooksAction],
2129
- ]
2130
- arr = []
2131
- router.each_mapping do |*args|
2132
- arr << args
2310
+ spec "[!yqcgx] raises TypeError when not a subclass of K8::Action." do
2311
+ filename = 'test_yqcgx.rb'
2312
+ content = "class Ex_yqcgx; end\n"
2313
+ File.open(filename, 'w') {|f| f << content }
2314
+ at_end { File.unlink filename }
2315
+ K8::ActionMapping.new([]).instance_exec(self) do |_|
2316
+ pr = proc { _require_action_class './test_yqcgx:Ex_yqcgx' }
2317
+ _.ok {pr}.raise?(TypeError, "'./test_yqcgx:Ex_yqcgx': expected subclass of K8::Action but not.")
2133
2318
  end
2134
- ok {arr} == [
2135
- ["/api/books/", BooksAction, {:GET=>:do_index, :POST=>:do_create}],
2136
- ["/api/books/new", BooksAction, {:GET=>:do_new}],
2137
- ["/api/books/{id}", BooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}],
2138
- ["/api/books/{id}/edit", BooksAction, {:GET=>:do_edit}],
2139
- ["/api/books/{book_id}/comments", BookCommentsAction, {:GET=>:do_comments}],
2140
- ["/api/books/{book_id}/comments/{comment_id}", BookCommentsAction, {:GET=>:do_comment}],
2141
- ["/admin/books/", AdminBooksAction, {:GET=>:do_index, :POST=>:do_create}],
2142
- ["/admin/books/new", AdminBooksAction, {:GET=>:do_new}],
2143
- ["/admin/books/{id}", AdminBooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}],
2144
- ["/admin/books/{id}/edit", AdminBooksAction, {:GET=>:do_edit}],
2145
- ]
2146
2319
  end
2147
2320
 
2148
2321
  end
2149
2322
 
2150
2323
 
2151
- topic '#find()' do
2324
+ topic '#each()' do
2152
2325
 
2153
- spec "[!zsuzg] creates finder object automatically if necessary." do
2154
- router = K8::ActionRouter.new(urlpath_cache_size: 99)
2155
- router.mount '/api/books', BooksAction
2156
- router.instance_exec(self) do |_|
2157
- _.ok {@finder} == nil
2158
- find('/api/books/123')
2159
- _.ok {@finder} != nil
2160
- _.ok {@finder}.is_a?(K8::ActionFinder)
2161
- end
2326
+ fixture :mapping do
2327
+ K8::ActionMapping.new([
2328
+ ['/api', [
2329
+ ['/books', BooksAction],
2330
+ ['/books/{book_id}', BookCommentsAction],
2331
+ ]],
2332
+ ])
2162
2333
  end
2163
2334
 
2164
- spec "[!9u978] urlpath_cache_size keyword argument will be passed to router oubject." do
2165
- router = K8::ActionRouter.new(urlpath_cache_size: 99)
2166
- router.mount '/api/books', BooksAction
2167
- router.instance_exec(self) do |_|
2168
- find('/api/books/123')
2169
- _.ok {@finder.instance_variable_get('@urlpath_cache_size')} == 99
2170
- end
2335
+ fixture :expected_tuples do
2336
+ [
2337
+ ['/api/books/', BooksAction, {:GET=>:do_index, :POST=>:do_create}],
2338
+ ['/api/books/new', BooksAction, {:GET=>:do_new}],
2339
+ ['/api/books/{id}', BooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}],
2340
+ ['/api/books/{id}/edit', BooksAction, {:GET=>:do_edit}],
2341
+ ['/api/books/{book_id}/comments', BookCommentsAction, {:GET=>:do_comments}],
2342
+ ['/api/books/{book_id}/comments/{comment_id}', BookCommentsAction, {:GET=>:do_comment}],
2343
+ ]
2171
2344
  end
2172
2345
 
2173
- spec "[!m9klu] returns action class, action methods, urlpath param names and values." do
2174
- router = K8::ActionRouter.new(urlpath_cache_size: 99)
2175
- router.register('id', '\d+') {|x| x.to_i }
2176
- router.mount '/api', [
2177
- ['/books', BooksAction],
2178
- ['/books/{book_id}', BookCommentsAction],
2179
- ]
2180
- router.mount '/admin', [
2181
- ['/books', AdminBooksAction],
2182
- ]
2183
- ret = router.find('/admin/books/123')
2184
- ok {ret} == [
2185
- AdminBooksAction,
2186
- {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete},
2187
- ["id"],
2188
- [123],
2189
- ]
2346
+ spec "[!7ynne] yields each urlpath pattern, action class and action methods." do
2347
+ |mapping, expected_tuples|
2348
+ arr = []
2349
+ mapping.each {|x,y,z| arr << [x, y, z] }
2350
+ ok {arr} == expected_tuples
2351
+ end
2352
+
2353
+ spec "[!2gwru] returns Enumerator if block is not provided." do
2354
+ |mapping, expected_tuples|
2355
+ ok {mapping.each}.is_a?(Enumerator)
2356
+ ok {mapping.each.map {|x,y,z| [x, y, z] }} == expected_tuples
2190
2357
  end
2191
2358
 
2192
2359
  end
@@ -2198,17 +2365,17 @@ Oktest.scope do
2198
2365
  topic K8::RackApplication do
2199
2366
 
2200
2367
  fixture :app do
2201
- app = K8::RackApplication.new
2202
- app.mount '/api', [
2203
- ['/books', BooksAction],
2204
- ]
2205
- app
2368
+ K8::RackApplication.new([
2369
+ ['/api', [
2370
+ ['/books', BooksAction],
2371
+ ]],
2372
+ ])
2206
2373
  end
2207
2374
 
2208
2375
 
2209
2376
  topic '#initialize()' do
2210
2377
 
2211
- spec "[!vkp65] mounts urlpath mappings if provided." do
2378
+ spec "[!vkp65] mounts urlpath mappings." do
2212
2379
  mapping = [
2213
2380
  ['/books' , BooksAction],
2214
2381
  ['/books/{id}/comments' , BookCommentsAction],
@@ -2247,78 +2414,13 @@ Oktest.scope do
2247
2414
  end
2248
2415
 
2249
2416
 
2250
- topic '#init_default_param_patterns()' do
2251
-
2252
- spec "[!i51id] registers '\d+' as default pattern of param 'id' or /_id\z/." do
2253
- |app|
2254
- app.instance_exec(self) do |_|
2255
- pat, proc_ = @router.default_patterns.lookup('id')
2256
- _.ok {pat} == '\d+'
2257
- _.ok {proc_.call("123")} == 123
2258
- pat, proc_ = @router.default_patterns.lookup('book_id')
2259
- _.ok {pat} == '\d+'
2260
- _.ok {proc_.call("123")} == 123
2261
- end
2262
- end
2263
-
2264
- spec "[!2g08b] registers '(?:\.\w+)?' as default pattern of param 'ext'." do
2265
- |app|
2266
- app.instance_exec(self) do |_|
2267
- pat, proc_ = @router.default_patterns.lookup('ext')
2268
- _.ok {pat} == '(?:\.\w+)?'
2269
- _.ok {proc_} == nil
2270
- end
2271
- end
2272
-
2273
- spec "[!8x5mp] registers '\d\d\d\d-\d\d-\d\d' as default pattern of param 'date' or /_date\z/." do
2274
- |app|
2275
- app.instance_exec(self) do |_|
2276
- pat, proc_ = @router.default_patterns.lookup('date')
2277
- _.ok {pat} == '\d\d\d\d-\d\d-\d\d'
2278
- _.ok {proc_.call("2014-12-24")} == Date.new(2014, 12, 24)
2279
- pat, proc_ = @router.default_patterns.lookup('birth_date')
2280
- _.ok {pat} == '\d\d\d\d-\d\d-\d\d'
2281
- _.ok {proc_.call("2015-02-14")} == Date.new(2015, 2, 14)
2282
- end
2283
- end
2284
-
2285
- spec "[!wg9vl] raises 404 error when invalid date (such as 2012-02-30)." do
2286
- |app|
2287
- app.instance_exec(self) do |_|
2288
- pat, proc_ = @router.default_patterns.lookup('date')
2289
- pr = proc { proc_.call('2012-02-30') }
2290
- _.ok {pr}.raise?(K8::HttpException, "2012-02-30: invalid date.")
2291
- _.ok {pr.exception.status_code} == 404
2292
- end
2293
- end
2294
-
2295
- end
2296
-
2297
-
2298
- topic '#mount()' do
2299
-
2300
- spec "[!zwva6] mounts action class to urlpath pattern." do
2301
- |app|
2302
- app.mount('/admin/books', AdminBooksAction)
2303
- ret = app.find('/admin/books/123')
2304
- ok {ret} == [
2305
- AdminBooksAction,
2306
- {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete},
2307
- ["id"],
2308
- [123],
2309
- ]
2310
- end
2311
-
2312
- end
2313
-
2314
-
2315
- topic '#find()' do
2417
+ topic '#lookup()' do
2316
2418
 
2317
2419
  spec "[!o0rnr] returns action class, action methods, urlpath names and values." do
2318
2420
  |app|
2319
- ret = app.find('/api/books/')
2421
+ ret = app.lookup('/api/books/')
2320
2422
  ok {ret} == [BooksAction, {:GET=>:do_index, :POST=>:do_create}, [], []]
2321
- ret = app.find('/api/books/123')
2423
+ ret = app.lookup('/api/books/123')
2322
2424
  ok {ret} == [BooksAction, {:GET=>:do_show, :PUT=>:do_update, :DELETE=>:do_delete}, ["id"], [123]]
2323
2425
  end
2324
2426
 
@@ -2348,6 +2450,50 @@ Oktest.scope do
2348
2450
  ok {body} == ["<index>"]
2349
2451
  end
2350
2452
 
2453
+ spec "[!eb2ms] returns 301 when urlpath not found but found with tailing '/'." do
2454
+ |app|
2455
+ env = new_env("GET", "/api/books")
2456
+ status, headers, body = app.call(env)
2457
+ ok {status} == 301
2458
+ ok {headers['Location']} == "/api/books/"
2459
+ ok {body} == []
2460
+ end
2461
+
2462
+ spec "[!02dow] returns 301 when urlpath not found but found without tailing '/'." do
2463
+ |app|
2464
+ env = new_env("GET", "/api/books/123/")
2465
+ status, headers, body = app.call(env)
2466
+ ok {status} == 301
2467
+ ok {headers['Location']} == "/api/books/123"
2468
+ ok {body} == []
2469
+ end
2470
+
2471
+ spec "[!2a9c9] adds query string to 'Location' header." do
2472
+ |app|
2473
+ env = new_env("GET", "/api/books", query: 'x=1&y=2')
2474
+ status, headers, body = app.call(env)
2475
+ ok {status} == 301
2476
+ ok {headers['Location']} == "/api/books/?x=1&y=2"
2477
+ #
2478
+ env = new_env("GET", "/api/books/123/", query: 'x=3&y=4')
2479
+ status, headers, body = app.call(env)
2480
+ ok {status} == 301
2481
+ ok {headers['Location']} == "/api/books/123?x=3&y=4"
2482
+ end
2483
+
2484
+ spec "[!vz07j] redirects only when request method is GET or HEAD." do
2485
+ |app|
2486
+ env = new_env("HEAD", "/api/books")
2487
+ status, headers, body = app.call(env)
2488
+ ok {status} == 301
2489
+ ok {headers['Location']} == "/api/books/"
2490
+ #
2491
+ env = new_env("POST", "/api/books")
2492
+ status, headers, body = app.call(env)
2493
+ ok {status} == 404
2494
+ ok {headers['Location']} == nil
2495
+ end
2496
+
2351
2497
  end
2352
2498
 
2353
2499
 
@@ -2864,307 +3010,6 @@ Oktest.scope do
2864
3010
  end
2865
3011
 
2866
3012
 
2867
- topic K8::Mock do
2868
-
2869
-
2870
- topic '.new_env()' do
2871
-
2872
- spec "[!c779l] raises ArgumentError when both form and json are specified." do
2873
- pr = proc { K8::Mock.new_env(form: "x=1", json: {"y"=>2}) }
2874
- ok {pr}.raise?(ArgumentError, "new_env(): not allowed both 'form' and 'json' at a time.")
2875
- end
2876
-
2877
- spec "[!gko8g] 'multipart:' kwarg accepts Hash object (which is converted into multipart data)." do
2878
- env = K8::Mock.new_env(multipart: {"a"=>10, "b"=>20})
2879
- ok {env['CONTENT_TYPE']} =~ /\Amultipart\/form-data; *boundary=/
2880
- env['CONTENT_TYPE'] =~ /\Amultipart\/form-data; *boundary=(.+)/
2881
- boundary = $1
2882
- cont_len = Integer(env['CONTENT_LENGTH'])
2883
- params, files = K8::Util.parse_multipart(env['rack.input'], boundary, cont_len)
2884
- ok {params} == {"a"=>"10", "b"=>"20"}
2885
- ok {files} == {}
2886
- end
2887
-
2888
- end
2889
-
2890
-
2891
- end
2892
-
2893
-
2894
- topic K8::Mock::MultipartBuilder do
2895
-
2896
-
2897
- topic '#initialize()' do
2898
-
2899
- spec "[!ajfgl] sets random string as boundary when boundary is nil." do
2900
- arr = []
2901
- 1000.times do
2902
- mp = K8::Mock::MultipartBuilder.new(nil)
2903
- ok {mp.boundary} != nil
2904
- ok {mp.boundary}.is_a?(String)
2905
- arr << mp.boundary
2906
- end
2907
- ok {arr.sort.uniq.length} == 1000
2908
- end
2909
-
2910
- end
2911
-
2912
-
2913
- topic '#add()' do
2914
-
2915
- spec "[!tp4bk] detects content type from filename when filename is not nil." do
2916
- mp = K8::Mock::MultipartBuilder.new
2917
- mp.add("name1", "value1")
2918
- mp.add("name2", "value2", "foo.csv")
2919
- mp.add("name3", "value3", "bar.csv", "text/plain")
2920
- ok {mp.instance_variable_get('@params')} == [
2921
- ["name1", "value1", nil, nil],
2922
- ["name2", "value2", "foo.csv", "text/comma-separated-values"],
2923
- ["name3", "value3", "bar.csv", "text/plain"],
2924
- ]
2925
- end
2926
-
2927
- end
2928
-
2929
-
2930
- topic '#add_file()' do
2931
-
2932
- fixture :data_dir do
2933
- File.join(File.dirname(__FILE__), 'data')
2934
- end
2935
-
2936
- fixture :filename1 do |data_dir|
2937
- File.join(data_dir, 'example1.png')
2938
- end
2939
-
2940
- fixture :filename2 do |data_dir|
2941
- File.join(data_dir, 'example1.jpg')
2942
- end
2943
-
2944
- fixture :multipart_data do |data_dir|
2945
- fname = File.join(data_dir, 'multipart.form')
2946
- File.open(fname, 'rb') {|f| f.read }
2947
- end
2948
-
2949
-
2950
- spec "[!uafqa] detects content type from filename when content type is not provided." do
2951
- |filename1, filename2|
2952
- file1 = File.open(filename1)
2953
- file2 = File.open(filename2)
2954
- at_end { [file1, file2].each {|f| f.close() unless f.closed? } }
2955
- mp = K8::Mock::MultipartBuilder.new
2956
- mp.add_file('image1', file1)
2957
- mp.add_file('image2', file2)
2958
- mp.instance_exec(self) do |_|
2959
- _.ok {@params[0][2]} == "example1.png"
2960
- _.ok {@params[0][3]} == "image/png"
2961
- _.ok {@params[1][2]} == "example1.jpg"
2962
- _.ok {@params[1][3]} == "image/jpeg"
2963
- end
2964
- end
2965
-
2966
- spec "[!b5811] reads file content and adds it as param value." do
2967
- |filename1, filename2, multipart_data|
2968
- file1 = File.open(filename1)
2969
- file2 = File.open(filename2)
2970
- at_end { [file1, file2].each {|f| f.close() unless f.closed? } }
2971
- boundary = '---------------------------68927884511827559971471404947'
2972
- mp = K8::Mock::MultipartBuilder.new(boundary)
2973
- mp.add('text1', "test1")
2974
- mp.add('text2', "日本語\r\nあいうえお\r\n")
2975
- mp.add_file('file1', file1)
2976
- mp.add_file('file2', file2)
2977
- ok {mp.to_s} == multipart_data
2978
- end
2979
-
2980
- spec "[!36bsu] closes opened file automatically." do
2981
- |filename1, filename2, multipart_data|
2982
- file1 = File.open(filename1)
2983
- file2 = File.open(filename2)
2984
- at_end { [file1, file2].each {|f| f.close() unless f.closed? } }
2985
- ok {file1.closed?} == false
2986
- ok {file2.closed?} == false
2987
- mp = K8::Mock::MultipartBuilder.new()
2988
- mp.add_file('file1', file1)
2989
- mp.add_file('file2', file2)
2990
- ok {file1.closed?} == true
2991
- ok {file2.closed?} == true
2992
- end
2993
-
2994
- end
2995
-
2996
-
2997
- topic '#to_s()' do
2998
-
2999
- spec "[!61gc4] returns multipart form string." do
3000
- mp = K8::Mock::MultipartBuilder.new("abc123")
3001
- mp.add("name1", "value1")
3002
- mp.add("name2", "value2", "foo.txt", "text/plain")
3003
- s = mp.to_s
3004
- ok {s} == [
3005
- "--abc123\r\n",
3006
- "Content-Disposition: form-data; name=\"name1\"\r\n",
3007
- "\r\n",
3008
- "value1\r\n",
3009
- "--abc123\r\n",
3010
- "Content-Disposition: form-data; name=\"name2\"; filename=\"foo.txt\"\r\n",
3011
- "Content-Type: text/plain\r\n",
3012
- "\r\n",
3013
- "value2\r\n",
3014
- "--abc123--\r\n",
3015
- ].join()
3016
- #
3017
- params, files = K8::Util.parse_multipart(StringIO.new(s), "abc123", s.length)
3018
- begin
3019
- ok {params} == {'name1'=>"value1", 'name2'=>"foo.txt"}
3020
- ok {files.keys} == ['name2']
3021
- ok {files['name2'].filename} == "foo.txt"
3022
- ensure
3023
- fpath = files['name2'].tmp_filepath
3024
- File.unlink(fpath) if File.exist?(fpath)
3025
- end
3026
- end
3027
-
3028
- end
3029
-
3030
-
3031
- end
3032
-
3033
-
3034
- topic K8::Mock::TestApp do
3035
-
3036
-
3037
- topic '#request()' do
3038
-
3039
- spec "[!4xpwa] creates env object and calls app with it." do
3040
- rackapp = proc {|env|
3041
- body = [
3042
- "PATH_INFO: #{env['PATH_INFO']}\n",
3043
- "QUERY_STRING: #{env['QUERY_STRING']}\n",
3044
- "HTTP_COOKIE: #{env['HTTP_COOKIE']}\n",
3045
- ]
3046
- [200, {"Content-Type"=>"text/plain"}, body]
3047
- }
3048
- http = K8::Mock::TestApp.new(rackapp)
3049
- resp = http.GET('/foo', query: {"x"=>123}, cookie: {"k"=>"v"})
3050
- ok {resp.status} == 200
3051
- ok {resp.headers} == {"Content-Type"=>"text/plain"}
3052
- ok {resp.body} == [
3053
- "PATH_INFO: /foo\n",
3054
- "QUERY_STRING: x=123\n",
3055
- "HTTP_COOKIE: k=v\n",
3056
- ]
3057
- end
3058
-
3059
- end
3060
-
3061
- end
3062
-
3063
-
3064
- topic K8::Mock::TestResponse do
3065
-
3066
-
3067
- topic '#body_binary' do
3068
-
3069
- spec "[!mb0i4] returns body as binary string." do
3070
- resp = K8::Mock::TestResponse.new(200, {}, ["foo", "bar"])
3071
- ok {resp.body_binary} == "foobar"
3072
- #ok {resp.body_binary.encoding} == Encoding::UTF_8
3073
- end
3074
-
3075
- end
3076
-
3077
-
3078
- topic '#body_text' do
3079
-
3080
- spec "[!rr18d] error when 'Content-Type' header is missing." do
3081
- resp = K8::Mock::TestResponse.new(200, {}, ["foo", "bar"])
3082
- pr = proc { resp.body_text }
3083
- ok {pr}.raise?(RuntimeError, "body_text(): missing 'Content-Type' header.")
3084
- end
3085
-
3086
- spec "[!dou1n] converts body text according to 'charset' in 'Content-Type' header." do
3087
- ctype = "application/json;charset=us-ascii"
3088
- resp = K8::Mock::TestResponse.new(200, {'Content-Type'=>ctype}, ['{"a":123}'])
3089
- ok {resp.body_text} == '{"a":123}'
3090
- ok {resp.body_text.encoding} == Encoding::ASCII
3091
- end
3092
-
3093
- spec "[!cxje7] assumes charset as 'utf-8' when 'Content-Type' is json." do
3094
- ctype = "application/json"
3095
- resp = K8::Mock::TestResponse.new(200, {'Content-Type'=>ctype}, ['{"a":123}'])
3096
- ok {resp.body_text} == '{"a":123}'
3097
- ok {resp.body_text.encoding} == Encoding::UTF_8
3098
- end
3099
-
3100
- spec "[!n4c71] error when non-json 'Content-Type' header has no 'charset'." do
3101
- ctype = "text/plain"
3102
- resp = K8::Mock::TestResponse.new(200, {'Content-Type'=>ctype}, ["foo", "bar"])
3103
- pr = proc { resp.body_text }
3104
- ok {pr}.raise?(RuntimeError, "body_text(): missing 'charset' in 'Content-Type' header.")
3105
- end
3106
-
3107
- spec "[!vkj9h] returns body as text string, according to 'charset' in 'Content-Type'." do
3108
- ctype = "text/plain;charset=utf-8"
3109
- resp = K8::Mock::TestResponse.new(200, {'Content-Type'=>ctype}, ["foo", "bar"])
3110
- ok {resp.body_text} == "foobar"
3111
- ok {resp.body_text.encoding} == Encoding::UTF_8
3112
- end
3113
-
3114
- end
3115
-
3116
-
3117
- topic '#body_json' do
3118
-
3119
- spec "[!qnic1] returns Hash object representing JSON string." do
3120
- ctype = "application/json"
3121
- resp = K8::Mock::TestResponse.new(200, {'Content-Type'=>ctype}, ['{"a":123}'])
3122
- ok {resp.body_json} == {"a"=>123}
3123
- end
3124
-
3125
- end
3126
-
3127
-
3128
- topic '#content_type' do
3129
-
3130
- spec "[!40hcz] returns 'Content-Type' header value." do
3131
- ctype = "application/json"
3132
- resp = K8::Mock::TestResponse.new(200, {'Content-Type'=>ctype}, ['{"a":123}'])
3133
- ok {resp.content_type} == ctype
3134
- end
3135
-
3136
- end
3137
-
3138
-
3139
- topic '#content_length' do
3140
-
3141
- spec "[!5lb19] returns 'Content-Length' header value as integer." do
3142
- resp = K8::Mock::TestResponse.new(200, {'Content-Length'=>"0"}, [])
3143
- ok {resp.content_length} == 0
3144
- ok {resp.content_length}.is_a?(Fixnum)
3145
- end
3146
-
3147
- spec "[!qjktz] returns nil when 'Content-Length' is not set." do
3148
- resp = K8::Mock::TestResponse.new(200, {}, [])
3149
- ok {resp.content_length} == nil
3150
- end
3151
-
3152
- end
3153
-
3154
-
3155
- topic '#location' do
3156
-
3157
- spec "[!8y8lg] returns 'Location' header value." do
3158
- resp = K8::Mock::TestResponse.new(200, {'Location'=>'/top'}, [])
3159
- ok {resp.location} == "/top"
3160
- end
3161
-
3162
- end
3163
-
3164
-
3165
- end
3166
-
3167
-
3168
3013
  end
3169
3014
 
3170
3015