keight 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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