lotus-router 0.0.0 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.coveralls.yml +2 -0
- data/.gitignore +5 -13
- data/.travis.yml +5 -0
- data/.yardopts +3 -0
- data/Gemfile +10 -3
- data/README.md +470 -6
- data/Rakefile +10 -1
- data/benchmarks/callable +23 -0
- data/benchmarks/named_routes +72 -0
- data/benchmarks/resource +44 -0
- data/benchmarks/resources +58 -0
- data/benchmarks/routes +67 -0
- data/benchmarks/run.sh +11 -0
- data/benchmarks/utils.rb +56 -0
- data/lib/lotus-router.rb +1 -0
- data/lib/lotus/router.rb +752 -3
- data/lib/lotus/router/version.rb +2 -2
- data/lib/lotus/routing/endpoint.rb +114 -0
- data/lib/lotus/routing/endpoint_resolver.rb +251 -0
- data/lib/lotus/routing/http_router.rb +130 -0
- data/lib/lotus/routing/namespace.rb +86 -0
- data/lib/lotus/routing/resource.rb +73 -0
- data/lib/lotus/routing/resource/action.rb +340 -0
- data/lib/lotus/routing/resource/options.rb +48 -0
- data/lib/lotus/routing/resources.rb +40 -0
- data/lib/lotus/routing/resources/action.rb +123 -0
- data/lib/lotus/routing/route.rb +53 -0
- data/lotus-router.gemspec +16 -12
- data/test/fixtures.rb +193 -0
- data/test/integration/client_error_test.rb +16 -0
- data/test/integration/pass_on_response_test.rb +13 -0
- data/test/named_routes_test.rb +123 -0
- data/test/namespace_test.rb +289 -0
- data/test/new_test.rb +67 -0
- data/test/redirect_test.rb +33 -0
- data/test/resource_test.rb +128 -0
- data/test/resources_test.rb +136 -0
- data/test/routing/endpoint_resolver_test.rb +110 -0
- data/test/routing/resource/options_test.rb +36 -0
- data/test/routing_test.rb +99 -0
- data/test/test_helper.rb +32 -0
- data/test/version_test.rb +7 -0
- metadata +102 -10
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Lotus::Router do
|
4
|
+
before do
|
5
|
+
@router = Lotus::Router.new
|
6
|
+
end
|
7
|
+
|
8
|
+
describe '#redirect' do
|
9
|
+
it 'recognizes string endpoint' do
|
10
|
+
endpoint = ->(env) { [200, {}, ['Redirect destination!']] }
|
11
|
+
@router.get('/redirect_destination', to: endpoint)
|
12
|
+
@router.redirect('/redirect', to: '/redirect_destination')
|
13
|
+
|
14
|
+
env = Rack::MockRequest.env_for('/redirect')
|
15
|
+
status, headers, _ = @router.call(env)
|
16
|
+
|
17
|
+
status.must_equal 302
|
18
|
+
headers['Location'].must_equal '/redirect_destination'
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'recognizes string endpoint with custom http code' do
|
22
|
+
endpoint = ->(env) { [200, {}, ['Redirect destination!']] }
|
23
|
+
@router.get('/redirect_destination', to: endpoint)
|
24
|
+
@router.redirect('/redirect', to: '/redirect_destination', code: 301)
|
25
|
+
|
26
|
+
env = Rack::MockRequest.env_for('/redirect')
|
27
|
+
status, headers, _ = @router.call(env)
|
28
|
+
|
29
|
+
status.must_equal 301
|
30
|
+
headers['Location'].must_equal '/redirect_destination'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Lotus::Router do
|
4
|
+
before do
|
5
|
+
@router = Lotus::Router.new
|
6
|
+
@app = Rack::MockRequest.new(@router)
|
7
|
+
end
|
8
|
+
|
9
|
+
after do
|
10
|
+
@router.reset!
|
11
|
+
end
|
12
|
+
|
13
|
+
def endpoint(response)
|
14
|
+
->(env) { response }
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#resource' do
|
18
|
+
before do
|
19
|
+
@router.resource 'avatar'
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'recognizes get new' do
|
23
|
+
@router.path(:new_avatar).must_equal '/avatar/new'
|
24
|
+
@app.request('GET', '/avatar/new').body.must_equal 'Avatar::New'
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'recognizes post create' do
|
28
|
+
@router.path(:avatar).must_equal '/avatar'
|
29
|
+
@app.request('POST', '/avatar').body.must_equal 'Avatar::Create'
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'recognizes get show' do
|
33
|
+
@router.path(:avatar).must_equal '/avatar'
|
34
|
+
@app.request('GET', '/avatar').body.must_equal 'Avatar::Show'
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'recognizes get edit' do
|
38
|
+
@router.path(:edit_avatar).must_equal '/avatar/edit'
|
39
|
+
@app.request('GET', '/avatar/edit').body.must_equal 'Avatar::Edit'
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'recognizes patch update' do
|
43
|
+
@router.path(:avatar).must_equal '/avatar'
|
44
|
+
@app.request('PATCH', '/avatar').body.must_equal 'Avatar::Update'
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'recognizes delete destroy' do
|
48
|
+
@router.path(:avatar).must_equal '/avatar'
|
49
|
+
@app.request('DELETE', '/avatar').body.must_equal 'Avatar::Destroy'
|
50
|
+
end
|
51
|
+
|
52
|
+
describe ':only option' do
|
53
|
+
before do
|
54
|
+
@router.resource 'profile', only: [:edit, :update]
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'recognizes only specified paths' do
|
58
|
+
@router.path(:edit_profile).must_equal '/profile/edit'
|
59
|
+
@app.request('GET', '/profile/edit').body.must_equal 'Profile::Edit'
|
60
|
+
|
61
|
+
@router.path(:profile).must_equal '/profile'
|
62
|
+
@app.request('PATCH', '/profile').body.must_equal 'Profile::Update'
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'does not recognize other paths' do
|
66
|
+
@app.request('GET', '/profile/new').status.must_equal 405
|
67
|
+
@app.request('POST', '/profile').status.must_equal 405
|
68
|
+
@app.request('GET', '/profile').status.must_equal 405
|
69
|
+
@app.request('DELETE', '/profile').status.must_equal 405
|
70
|
+
|
71
|
+
-> { @router.path(:new_profile) }.must_raise HttpRouter::InvalidRouteException
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe ':except option' do
|
76
|
+
before do
|
77
|
+
@router.resource 'profile', except: [:new, :show, :create, :destroy]
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'recognizes only the non-rejected paths' do
|
81
|
+
@router.path(:edit_profile).must_equal '/profile/edit'
|
82
|
+
@app.request('GET', '/profile/edit').body.must_equal 'Profile::Edit'
|
83
|
+
|
84
|
+
@router.path(:profile).must_equal '/profile'
|
85
|
+
@app.request('PATCH', '/profile').body.must_equal 'Profile::Update'
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'does not recognize other paths' do
|
89
|
+
@app.request('GET', '/profile/new').status.must_equal 405
|
90
|
+
@app.request('POST', '/profile').status.must_equal 405
|
91
|
+
@app.request('GET', '/profile').status.must_equal 405
|
92
|
+
@app.request('DELETE', '/profile').status.must_equal 405
|
93
|
+
|
94
|
+
-> { @router.path(:new_profile) }.must_raise HttpRouter::InvalidRouteException
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe 'member' do
|
99
|
+
before do
|
100
|
+
@router.resource 'profile', only: [] do
|
101
|
+
member do
|
102
|
+
patch 'activate'
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'recognizes the path' do
|
108
|
+
@router.path(:activate_profile).must_equal '/profile/activate'
|
109
|
+
@app.request('PATCH', '/profile/activate').body.must_equal 'Profile::Activate'
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe 'collection' do
|
114
|
+
before do
|
115
|
+
@router.resource 'profile', only: [] do
|
116
|
+
collection do
|
117
|
+
get 'keys'
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'recognizes the path' do
|
123
|
+
@router.path(:keys_profile).must_equal '/profile/keys'
|
124
|
+
@app.request('GET', '/profile/keys').body.must_equal 'Profile::Keys'
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Lotus::Router do
|
4
|
+
before do
|
5
|
+
@router = Lotus::Router.new
|
6
|
+
@app = Rack::MockRequest.new(@router)
|
7
|
+
end
|
8
|
+
|
9
|
+
after do
|
10
|
+
@router.reset!
|
11
|
+
end
|
12
|
+
|
13
|
+
def endpoint(response)
|
14
|
+
->(env) { response }
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#resources' do
|
18
|
+
before do
|
19
|
+
@router.resources 'flowers'
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'recognizes get index' do
|
23
|
+
@router.path(:flowers).must_equal '/flowers'
|
24
|
+
@app.request('GET', '/flowers').body.must_equal 'Flowers::Index'
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'recognizes get new' do
|
28
|
+
@router.path(:new_flowers).must_equal '/flowers/new'
|
29
|
+
@app.request('GET', '/flowers/new').body.must_equal 'Flowers::New'
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'recognizes post create' do
|
33
|
+
@router.path(:flowers).must_equal '/flowers'
|
34
|
+
@app.request('POST', '/flowers').body.must_equal 'Flowers::Create'
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'recognizes get show' do
|
38
|
+
@router.path(:flowers, id: 23).must_equal '/flowers/23'
|
39
|
+
@app.request('GET', '/flowers/23').body.must_equal 'Flowers::Show 23'
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'recognizes get edit' do
|
43
|
+
@router.path(:edit_flowers, id: 23).must_equal '/flowers/23/edit'
|
44
|
+
@app.request('GET', '/flowers/23/edit').body.must_equal 'Flowers::Edit 23'
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'recognizes patch update' do
|
48
|
+
@router.path(:flowers, id: 23).must_equal '/flowers/23'
|
49
|
+
@app.request('PATCH', '/flowers/23').body.must_equal 'Flowers::Update 23'
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'recognizes delete destroy' do
|
53
|
+
@router.path(:flowers, id: 23).must_equal '/flowers/23'
|
54
|
+
@app.request('DELETE', '/flowers/23').body.must_equal 'Flowers::Destroy 23'
|
55
|
+
end
|
56
|
+
|
57
|
+
describe ':only option' do
|
58
|
+
before do
|
59
|
+
@router.resources 'keyboards', only: [:index, :edit]
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'recognizes only specified paths' do
|
63
|
+
@router.path(:keyboards).must_equal '/keyboards'
|
64
|
+
@app.request('GET', '/keyboards').body.must_equal 'Keyboards::Index'
|
65
|
+
|
66
|
+
@router.path(:edit_keyboards, id: 23).must_equal '/keyboards/23/edit'
|
67
|
+
@app.request('GET', '/keyboards/23/edit').body.must_equal 'Keyboards::Edit 23'
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'does not recognize other paths' do
|
71
|
+
@app.request('GET', '/keyboards/new').status.must_equal 404
|
72
|
+
@app.request('POST', '/keyboards').status.must_equal 405
|
73
|
+
@app.request('GET', '/keyboards/23').status.must_equal 404
|
74
|
+
@app.request('PATCH', '/keyboards/23').status.must_equal 405
|
75
|
+
@app.request('DELETE', '/keyboards/23').status.must_equal 405
|
76
|
+
|
77
|
+
-> { @router.path(:new_keyboards) }.must_raise HttpRouter::InvalidRouteException
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe ':except option' do
|
82
|
+
before do
|
83
|
+
@router.resources 'keyboards', except: [:new, :show, :update, :destroy]
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'recognizes only the non-rejected paths' do
|
87
|
+
@router.path(:keyboards).must_equal '/keyboards'
|
88
|
+
@app.request('GET', '/keyboards').body.must_equal 'Keyboards::Index'
|
89
|
+
|
90
|
+
@router.path(:edit_keyboards, id: 23).must_equal '/keyboards/23/edit'
|
91
|
+
@app.request('GET', '/keyboards/23/edit').body.must_equal 'Keyboards::Edit 23'
|
92
|
+
|
93
|
+
@router.path(:keyboards).must_equal '/keyboards'
|
94
|
+
@app.request('POST', '/keyboards').body.must_equal 'Keyboards::Create'
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'does not recognize other paths' do
|
98
|
+
@app.request('GET', '/keyboards/new').status.must_equal 404
|
99
|
+
@app.request('PATCH', '/keyboards/23').status.must_equal 405
|
100
|
+
@app.request('DELETE', '/keyboards/23').status.must_equal 405
|
101
|
+
|
102
|
+
-> { @router.path(:new_keyboards) }.must_raise HttpRouter::InvalidRouteException
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe 'member' do
|
107
|
+
before do
|
108
|
+
@router.resources 'keyboards', only: [] do
|
109
|
+
member do
|
110
|
+
get 'screenshot'
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'recognizes the path' do
|
116
|
+
@router.path(:screenshot_keyboards, id: 23).must_equal '/keyboards/23/screenshot'
|
117
|
+
@app.request('GET', '/keyboards/23/screenshot').body.must_equal 'Keyboards::Screenshot 23'
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe 'collection' do
|
122
|
+
before do
|
123
|
+
@router.resources 'keyboards', only: [] do
|
124
|
+
collection do
|
125
|
+
get 'search'
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'recognizes the path' do
|
131
|
+
@router.path(:search_keyboards).must_equal '/keyboards/search'
|
132
|
+
@app.request('GET', '/keyboards/search').body.must_equal 'Keyboards::Search'
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Lotus::Routing::EndpointResolver do
|
4
|
+
before do
|
5
|
+
@resolver = Lotus::Routing::EndpointResolver.new
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'recognizes :to when it is a callable object' do
|
9
|
+
endpoint = Object.new
|
10
|
+
endpoint.define_singleton_method(:call) { }
|
11
|
+
|
12
|
+
options = { to: endpoint }
|
13
|
+
|
14
|
+
@resolver.resolve(options).must_equal(endpoint)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'recognizes :to when it is a string that references a class that can be retrieved now' do
|
18
|
+
options = { to: 'test_endpoint' }
|
19
|
+
@resolver.resolve(options).call({}).must_equal 'Hi from TestEndpoint!'
|
20
|
+
end
|
21
|
+
|
22
|
+
describe 'when :to references a missing class' do
|
23
|
+
it 'if the class is available when invoking call, it succeed' do
|
24
|
+
options = { to: 'lazy_controller' }
|
25
|
+
endpoint = @resolver.resolve(options)
|
26
|
+
LazyController = Class.new(Object) { define_method(:call) {|env| env } }
|
27
|
+
|
28
|
+
endpoint.call({}).must_equal({})
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'if the class is not available when invoking call, it raises error' do
|
32
|
+
options = { to: 'missing_endpoint' }
|
33
|
+
endpoint = @resolver.resolve(options)
|
34
|
+
|
35
|
+
-> { endpoint.call({}) }.must_raise Lotus::Routing::EndpointNotFound
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'recognizes :to when it is a string with separator' do
|
40
|
+
options = { to: 'test#show' }
|
41
|
+
@resolver.resolve(options).call({}).must_equal 'Hi from Test::Show!'
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'returns the default endpoint when cannot match anything' do
|
45
|
+
options = { to: 23 }
|
46
|
+
@resolver.resolve(options).call({}).first.must_equal 404
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'namespace' do
|
50
|
+
before do
|
51
|
+
@resolver = Lotus::Routing::EndpointResolver.new(namespace: TestApp)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'recognizes :to when it is a string and an explicit namespace' do
|
55
|
+
options = { to: 'test_endpoint' }
|
56
|
+
@resolver.resolve(options).call({}).must_equal 'Hi from TestApp::TestEndpoint!'
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'recognizes :to when it is a string with separator and it has an explicit namespace' do
|
60
|
+
options = { to: 'test2#show' }
|
61
|
+
@resolver.resolve(options).call({}).must_equal 'Hi from TestApp::Test2Controller::Show!'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe 'custom endpoint' do
|
66
|
+
before :all do
|
67
|
+
class CustomEndpoint
|
68
|
+
def initialize(endpoint)
|
69
|
+
@endpoint = endpoint
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
@resolver = Lotus::Routing::EndpointResolver.new(endpoint: CustomEndpoint)
|
74
|
+
end
|
75
|
+
|
76
|
+
after do
|
77
|
+
Object.send(:remove_const, :CustomEndpoint)
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'returns specified endpoint instance' do
|
81
|
+
@resolver.resolve({}).class.must_equal(CustomEndpoint)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe 'custom separator' do
|
86
|
+
before do
|
87
|
+
@resolver = Lotus::Routing::EndpointResolver.new(action_separator: action_separator)
|
88
|
+
end
|
89
|
+
|
90
|
+
let(:action_separator) { '@' }
|
91
|
+
|
92
|
+
it 'matches controller and action with a custom separator' do
|
93
|
+
options = { to: "test#{ action_separator }show" }
|
94
|
+
@resolver.resolve(options).call({}).must_equal 'Hi from Test::Show!'
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe 'custom suffix' do
|
99
|
+
before do
|
100
|
+
@resolver = Lotus::Routing::EndpointResolver.new(suffix: suffix)
|
101
|
+
end
|
102
|
+
|
103
|
+
let(:suffix) { 'Controller::' }
|
104
|
+
|
105
|
+
it 'matches controller and action with a custom separator' do
|
106
|
+
options = { to: 'test#show' }
|
107
|
+
@resolver.resolve(options).call({}).must_equal 'Hi from Test::Show!'
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Lotus::Routing::Resource::Options do
|
4
|
+
before do
|
5
|
+
@actions = [:index, :new, :create, :show, :edit, :update, :destroy]
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'returns all the actions when no exceptions are requested' do
|
9
|
+
Lotus::Routing::Resources::Options.new(@actions).actions.must_equal @actions
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'returns only the action requested via the :only option' do
|
13
|
+
options = Lotus::Routing::Resources::Options.new(@actions, only: :show)
|
14
|
+
options.actions.must_equal [:show]
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'returns only the actions requested via the :only options' do
|
18
|
+
options = Lotus::Routing::Resources::Options.new(@actions, only: [:create, :edit])
|
19
|
+
options.actions.must_equal [:create, :edit]
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'returns only the action not rejected via the :except option' do
|
23
|
+
options = Lotus::Routing::Resources::Options.new(@actions, except: :destroy)
|
24
|
+
options.actions.must_equal [:index, :new, :create, :show, :edit, :update]
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'returns only the actions requested via the :only options' do
|
28
|
+
options = Lotus::Routing::Resources::Options.new(@actions, except: [:index, :new, :edit])
|
29
|
+
options.actions.must_equal [:create, :show, :update, :destroy]
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'allow access to values' do
|
33
|
+
options = Lotus::Routing::Resources::Options.new(@actions, name: :lotus)
|
34
|
+
options[:name].must_equal :lotus
|
35
|
+
end
|
36
|
+
end
|