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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.coveralls.yml +2 -0
  3. data/.gitignore +5 -13
  4. data/.travis.yml +5 -0
  5. data/.yardopts +3 -0
  6. data/Gemfile +10 -3
  7. data/README.md +470 -6
  8. data/Rakefile +10 -1
  9. data/benchmarks/callable +23 -0
  10. data/benchmarks/named_routes +72 -0
  11. data/benchmarks/resource +44 -0
  12. data/benchmarks/resources +58 -0
  13. data/benchmarks/routes +67 -0
  14. data/benchmarks/run.sh +11 -0
  15. data/benchmarks/utils.rb +56 -0
  16. data/lib/lotus-router.rb +1 -0
  17. data/lib/lotus/router.rb +752 -3
  18. data/lib/lotus/router/version.rb +2 -2
  19. data/lib/lotus/routing/endpoint.rb +114 -0
  20. data/lib/lotus/routing/endpoint_resolver.rb +251 -0
  21. data/lib/lotus/routing/http_router.rb +130 -0
  22. data/lib/lotus/routing/namespace.rb +86 -0
  23. data/lib/lotus/routing/resource.rb +73 -0
  24. data/lib/lotus/routing/resource/action.rb +340 -0
  25. data/lib/lotus/routing/resource/options.rb +48 -0
  26. data/lib/lotus/routing/resources.rb +40 -0
  27. data/lib/lotus/routing/resources/action.rb +123 -0
  28. data/lib/lotus/routing/route.rb +53 -0
  29. data/lotus-router.gemspec +16 -12
  30. data/test/fixtures.rb +193 -0
  31. data/test/integration/client_error_test.rb +16 -0
  32. data/test/integration/pass_on_response_test.rb +13 -0
  33. data/test/named_routes_test.rb +123 -0
  34. data/test/namespace_test.rb +289 -0
  35. data/test/new_test.rb +67 -0
  36. data/test/redirect_test.rb +33 -0
  37. data/test/resource_test.rb +128 -0
  38. data/test/resources_test.rb +136 -0
  39. data/test/routing/endpoint_resolver_test.rb +110 -0
  40. data/test/routing/resource/options_test.rb +36 -0
  41. data/test/routing_test.rb +99 -0
  42. data/test/test_helper.rb +32 -0
  43. data/test/version_test.rb +7 -0
  44. metadata +102 -10
@@ -0,0 +1,16 @@
1
+ require 'test_helper'
2
+
3
+ describe Lotus::Router do
4
+ before do
5
+ @router = Lotus::Router.new { get '/', to: ->(env) {} }
6
+ @app = Rack::MockRequest.new(@router)
7
+ end
8
+
9
+ it 'returns 404 for unknown path' do
10
+ @app.get('/unknown').status.must_equal 404
11
+ end
12
+
13
+ it 'returns 405 for unacceptable HTTP method' do
14
+ @app.post('/').status.must_equal 405
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ require 'test_helper'
2
+
3
+ describe 'Pass on response' do
4
+ before do
5
+ @routes = Lotus::Router.new { get '/', to: ->(env) { Rack::Response.new } }
6
+ @app = Rack::MockRequest.new(@routes)
7
+ end
8
+
9
+ it 'is successful' do
10
+ response = @app.get('/')
11
+ response.status.must_equal 200
12
+ end
13
+ end
@@ -0,0 +1,123 @@
1
+ describe Lotus::Router do
2
+ before do
3
+ @router = Lotus::Router.new(scheme: 'https', host: 'test.com', port: 443)
4
+
5
+ @router.get('/lotus', to: endpoint, as: :fixed)
6
+ @router.get('/flowers/:id', to: endpoint, as: :variables)
7
+ @router.get('/books/:id', id: /\d+/, to: endpoint, as: :constraints)
8
+ @router.get('/articles(.:format)', to: endpoint, as: :optional)
9
+ @router.get('/files/*', to: endpoint, as: :glob)
10
+ end
11
+
12
+ after do
13
+ @router.reset!
14
+ end
15
+
16
+ let(:endpoint) { ->(env) { [200, {}, ['Hi!']] } }
17
+
18
+ describe '#path' do
19
+ it 'recognizes fixed string' do
20
+ @router.path(:fixed).must_equal '/lotus'
21
+ end
22
+
23
+ it 'recognizes string with variables' do
24
+ @router.path(:variables, id: 'lotus').must_equal '/flowers/lotus'
25
+ end
26
+
27
+ it "raises error when variables aren't satisfied" do
28
+ -> {
29
+ @router.path(:variables)
30
+ }.must_raise(Lotus::Routing::InvalidRouteException)
31
+ end
32
+
33
+ it 'recognizes string with variables and constraints' do
34
+ @router.path(:constraints, id: 23).must_equal '/books/23'
35
+ end
36
+
37
+ it "raises error when constraints aren't satisfied" do
38
+ -> {
39
+ @router.path(:constraints, id: 'x')
40
+ }.must_raise(Lotus::Routing::InvalidRouteException)
41
+ end
42
+
43
+ it 'recognizes optional variables' do
44
+ @router.path(:optional).must_equal '/articles'
45
+ @router.path(:optional, page: '1').must_equal '/articles?page=1'
46
+ @router.path(:optional, format: 'rss').must_equal '/articles.rss'
47
+ @router.path(:optional, format: 'rss', page: '1').must_equal '/articles.rss?page=1'
48
+ end
49
+
50
+ it 'recognizes glob string' do
51
+ @router.path(:glob).must_equal '/files/'
52
+ end
53
+
54
+ it 'escapes additional params in query string' do
55
+ @router.path(:fixed, return_to: '/dashboard').must_equal '/lotus?return_to=%2Fdashboard'
56
+ end
57
+
58
+ it 'raises error when insufficient params are passed' do
59
+ -> {
60
+ @router.path(nil)
61
+ }.must_raise(Lotus::Routing::InvalidRouteException)
62
+ end
63
+
64
+ it 'raises error when too many params are passed' do
65
+ -> {
66
+ @router.path(:fixed, 'x')
67
+ }.must_raise(Lotus::Routing::InvalidRouteException)
68
+ end
69
+ end
70
+
71
+ describe '#url' do
72
+ it 'recognizes fixed string' do
73
+ @router.url(:fixed).must_equal 'https://test.com/lotus'
74
+ end
75
+
76
+ it 'recognizes string with variables' do
77
+ @router.url(:variables, id: 'lotus').must_equal 'https://test.com/flowers/lotus'
78
+ end
79
+
80
+ it "raises error when variables aren't satisfied" do
81
+ -> {
82
+ @router.url(:variables)
83
+ }.must_raise(Lotus::Routing::InvalidRouteException)
84
+ end
85
+
86
+ it 'recognizes string with variables and constraints' do
87
+ @router.url(:constraints, id: 23).must_equal 'https://test.com/books/23'
88
+ end
89
+
90
+ it "raises error when constraints aren't satisfied" do
91
+ -> {
92
+ @router.url(:constraints, id: 'x')
93
+ }.must_raise(Lotus::Routing::InvalidRouteException)
94
+ end
95
+
96
+ it 'recognizes optional variables' do
97
+ @router.url(:optional).must_equal 'https://test.com/articles'
98
+ @router.url(:optional, page: '1').must_equal 'https://test.com/articles?page=1'
99
+ @router.url(:optional, format: 'rss').must_equal 'https://test.com/articles.rss'
100
+ @router.url(:optional, format: 'rss', page: '1').must_equal 'https://test.com/articles.rss?page=1'
101
+ end
102
+
103
+ it 'recognizes glob string' do
104
+ @router.url(:glob).must_equal 'https://test.com/files/'
105
+ end
106
+
107
+ it 'escapes additional params in query string' do
108
+ @router.url(:fixed, return_to: '/dashboard').must_equal 'https://test.com/lotus?return_to=%2Fdashboard'
109
+ end
110
+
111
+ it 'raises error when insufficient params are passed' do
112
+ -> {
113
+ @router.url(nil)
114
+ }.must_raise(Lotus::Routing::InvalidRouteException)
115
+ end
116
+
117
+ it 'raises error when too many params are passed' do
118
+ -> {
119
+ @router.url(:fixed, 'x')
120
+ }.must_raise(Lotus::Routing::InvalidRouteException)
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,289 @@
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 '#namespace' do
18
+ it 'recognizes get path' do
19
+ endpoint = endpoint([200, {}, ['Trees (GET)!']])
20
+ @router.namespace 'trees' do
21
+ get '/plane-tree', to: endpoint
22
+ end
23
+
24
+ @app.request('GET', '/trees/plane-tree').body.must_equal 'Trees (GET)!'
25
+ end
26
+
27
+ it 'recognizes post path' do
28
+ endpoint = endpoint([200, {}, ['Trees (POST)!']])
29
+ @router.namespace 'trees' do
30
+ post '/sequoia', to: endpoint
31
+ end
32
+
33
+ @app.request('POST', '/trees/sequoia').body.must_equal 'Trees (POST)!'
34
+ end
35
+
36
+ it 'recognizes put path' do
37
+ endpoint = endpoint([200, {}, ['Trees (PUT)!']])
38
+ @router.namespace 'trees' do
39
+ put '/cherry-tree', to: endpoint
40
+ end
41
+
42
+ @app.request('PUT', '/trees/cherry-tree').body.must_equal 'Trees (PUT)!'
43
+ end
44
+
45
+ it 'recognizes patch path' do
46
+ endpoint = endpoint([200, {}, ['Trees (PATCH)!']])
47
+ @router.namespace 'trees' do
48
+ patch '/cedar', to: endpoint
49
+ end
50
+
51
+ @app.request('PATCH', '/trees/cedar').body.must_equal 'Trees (PATCH)!'
52
+ end
53
+
54
+ it 'recognizes delete path' do
55
+ endpoint = endpoint([200, {}, ['Trees (DELETE)!']])
56
+ @router.namespace 'trees' do
57
+ delete '/pine', to: endpoint
58
+ end
59
+
60
+ @app.request('DELETE', '/trees/pine').body.must_equal 'Trees (DELETE)!'
61
+ end
62
+
63
+ it 'recognizes trace path' do
64
+ endpoint = endpoint([200, {}, ['Trees (TRACE)!']])
65
+ @router.namespace 'trees' do
66
+ trace '/cypress', to: endpoint
67
+ end
68
+
69
+ @app.request('TRACE', '/trees/cypress').body.must_equal 'Trees (TRACE)!'
70
+ end
71
+
72
+ describe 'nested' do
73
+ before do
74
+ endpoint = endpoint([200, {}, ['Meow!']])
75
+ @router.namespace 'animals' do
76
+ namespace 'mammals' do
77
+ get '/cats', to: endpoint
78
+ end
79
+ end
80
+ end
81
+
82
+ it 'recognizes get path' do
83
+ @app.request('GET', '/animals/mammals/cats').body.must_equal 'Meow!'
84
+ end
85
+ end
86
+
87
+ describe 'redirect' do
88
+ before do
89
+ endpoint = endpoint([200, {}, ['New Home!']])
90
+ @router.namespace 'users' do
91
+ get '/home', to: endpoint
92
+ redirect '/dashboard', to: '/home'
93
+ end
94
+ end
95
+
96
+ it 'recognizes get path' do
97
+ @app.request('GET', '/users/dashboard').headers['Location'].must_equal '/users/home'
98
+ @app.request('GET', '/users/dashboard').status.must_equal 302
99
+ end
100
+ end
101
+
102
+ describe 'restful resources' do
103
+ before do
104
+ @router.namespace 'vegetals' do
105
+ resources 'flowers'
106
+ end
107
+ end
108
+
109
+ it 'recognizes get index' do
110
+ @router.path(:vegetals_flowers).must_equal '/vegetals/flowers'
111
+ @app.request('GET', '/vegetals/flowers').body.must_equal 'Flowers::Index'
112
+ end
113
+
114
+ it 'recognizes get new' do
115
+ @router.path(:vegetals_new_flowers).must_equal '/vegetals/flowers/new'
116
+ @app.request('GET', '/vegetals/flowers/new').body.must_equal 'Flowers::New'
117
+ end
118
+
119
+ it 'recognizes post create' do
120
+ @router.path(:vegetals_flowers).must_equal '/vegetals/flowers'
121
+ @app.request('POST', '/vegetals/flowers').body.must_equal 'Flowers::Create'
122
+ end
123
+
124
+ it 'recognizes get show' do
125
+ @router.path(:vegetals_flowers, id: 23).must_equal '/vegetals/flowers/23'
126
+ @app.request('GET', '/vegetals/flowers/23').body.must_equal 'Flowers::Show 23'
127
+ end
128
+
129
+ it 'recognizes get edit' do
130
+ @router.path(:vegetals_edit_flowers, id: 23).must_equal '/vegetals/flowers/23/edit'
131
+ @app.request('GET', '/vegetals/flowers/23/edit').body.must_equal 'Flowers::Edit 23'
132
+ end
133
+
134
+ it 'recognizes patch update' do
135
+ @router.path(:vegetals_flowers, id: 23).must_equal '/vegetals/flowers/23'
136
+ @app.request('PATCH', '/vegetals/flowers/23').body.must_equal 'Flowers::Update 23'
137
+ end
138
+
139
+ it 'recognizes delete destroy' do
140
+ @router.path(:vegetals_flowers, id: 23).must_equal '/vegetals/flowers/23'
141
+ @app.request('DELETE', '/vegetals/flowers/23').body.must_equal 'Flowers::Destroy 23'
142
+ end
143
+
144
+ describe ':only option' do
145
+ before do
146
+ @router.namespace 'electronics' do
147
+ resources 'keyboards', only: [:index, :edit]
148
+ end
149
+ end
150
+
151
+ it 'recognizes only specified paths' do
152
+ @router.path(:electronics_keyboards).must_equal '/electronics/keyboards'
153
+ @app.request('GET', '/electronics/keyboards').body.must_equal 'Keyboards::Index'
154
+
155
+ @router.path(:electronics_edit_keyboards, id: 23).must_equal '/electronics/keyboards/23/edit'
156
+ @app.request('GET', '/electronics/keyboards/23/edit').body.must_equal 'Keyboards::Edit 23'
157
+ end
158
+
159
+ it 'does not recognize other paths' do
160
+ @app.request('GET', '/electronics/keyboards/new').status.must_equal 404
161
+ @app.request('POST', '/electronics/keyboards').status.must_equal 405
162
+ @app.request('GET', '/electronics/keyboards/23').status.must_equal 404
163
+ @app.request('PATCH', '/electronics/keyboards/23').status.must_equal 405
164
+ @app.request('DELETE', '/electronics/keyboards/23').status.must_equal 405
165
+
166
+ -> { @router.path(:electronics_new_keyboards) }.must_raise HttpRouter::InvalidRouteException
167
+ end
168
+ end
169
+
170
+ describe ':except option' do
171
+ before do
172
+ @router.namespace 'electronics' do
173
+ resources 'keyboards', except: [:new, :show, :update, :destroy]
174
+ end
175
+ end
176
+
177
+ it 'recognizes only the non-rejected paths' do
178
+ @router.path(:electronics_keyboards).must_equal '/electronics/keyboards'
179
+ @app.request('GET', '/electronics/keyboards').body.must_equal 'Keyboards::Index'
180
+
181
+ @router.path(:electronics_edit_keyboards, id: 23).must_equal '/electronics/keyboards/23/edit'
182
+ @app.request('GET', '/electronics/keyboards/23/edit').body.must_equal 'Keyboards::Edit 23'
183
+
184
+ @router.path(:electronics_keyboards).must_equal '/electronics/keyboards'
185
+ @app.request('POST', '/electronics/keyboards').body.must_equal 'Keyboards::Create'
186
+ end
187
+
188
+ it 'does not recognize other paths' do
189
+ @app.request('GET', '/electronics/keyboards/new').status.must_equal 404
190
+ @app.request('PATCH', '/electronics/keyboards/23').status.must_equal 405
191
+ @app.request('DELETE', '/electronics/keyboards/23').status.must_equal 405
192
+
193
+ -> { @router.path(:electronics_new_keyboards) }.must_raise HttpRouter::InvalidRouteException
194
+ end
195
+ end
196
+ end
197
+
198
+ describe 'restful resource' do
199
+ before do
200
+ @router.namespace 'settings' do
201
+ resource 'avatar'
202
+ end
203
+ end
204
+
205
+ it 'recognizes get new' do
206
+ @router.path(:settings_new_avatar).must_equal '/settings/avatar/new'
207
+ @app.request('GET', '/settings/avatar/new').body.must_equal 'Avatar::New'
208
+ end
209
+
210
+ it 'recognizes post create' do
211
+ @router.path(:settings_avatar).must_equal '/settings/avatar'
212
+ @app.request('POST', '/settings/avatar').body.must_equal 'Avatar::Create'
213
+ end
214
+
215
+ it 'recognizes get show' do
216
+ @router.path(:settings_avatar).must_equal '/settings/avatar'
217
+ @app.request('GET', '/settings/avatar').body.must_equal 'Avatar::Show'
218
+ end
219
+
220
+ it 'recognizes get edit' do
221
+ @router.path(:settings_edit_avatar).must_equal '/settings/avatar/edit'
222
+ @app.request('GET', '/settings/avatar/edit').body.must_equal 'Avatar::Edit'
223
+ end
224
+
225
+ it 'recognizes patch update' do
226
+ @router.path(:settings_avatar).must_equal '/settings/avatar'
227
+ @app.request('PATCH', '/settings/avatar').body.must_equal 'Avatar::Update'
228
+ end
229
+
230
+ it 'recognizes delete destroy' do
231
+ @router.path(:settings_avatar).must_equal '/settings/avatar'
232
+ @app.request('DELETE', '/settings/avatar').body.must_equal 'Avatar::Destroy'
233
+ end
234
+
235
+ describe ':only option' do
236
+ before do
237
+ @router.namespace 'settings' do
238
+ resource 'profile', only: [:edit, :update]
239
+ end
240
+ end
241
+
242
+ it 'recognizes only specified paths' do
243
+ @router.path(:settings_edit_profile).must_equal '/settings/profile/edit'
244
+ @app.request('GET', '/settings/profile/edit').body.must_equal 'Profile::Edit'
245
+
246
+ @router.path(:settings_profile).must_equal '/settings/profile'
247
+ @app.request('PATCH', '/settings/profile').body.must_equal 'Profile::Update'
248
+ end
249
+
250
+ it 'does not recognize other paths' do
251
+ @app.request('GET', '/settings/profile').status.must_equal 405
252
+ @app.request('GET', '/settings/profile/new').status.must_equal 405
253
+ @app.request('POST', '/settings/profile').status.must_equal 405
254
+ @app.request('DELETE', '/settings/profile').status.must_equal 405
255
+
256
+ -> { @router.path(:settings_new_profile) }.must_raise HttpRouter::InvalidRouteException
257
+ end
258
+ end
259
+
260
+ describe ':except option' do
261
+ before do
262
+ @router.namespace 'settings' do
263
+ resource 'profile', except: [:edit, :update]
264
+ end
265
+ end
266
+
267
+ it 'recognizes only the non-rejected paths' do
268
+ @router.path(:settings_profile).must_equal '/settings/profile'
269
+ @app.request('GET', '/settings/profile').body.must_equal 'Profile::Show'
270
+
271
+ @router.path(:settings_new_profile).must_equal '/settings/profile/new'
272
+ @app.request('GET', '/settings/profile/new').body.must_equal 'Profile::New'
273
+
274
+ @router.path(:settings_profile).must_equal '/settings/profile'
275
+ @app.request('POST', '/settings/profile').body.must_equal 'Profile::Create'
276
+
277
+ @router.path(:settings_profile).must_equal '/settings/profile'
278
+ @app.request('DELETE', '/settings/profile').body.must_equal 'Profile::Destroy'
279
+ end
280
+
281
+ it 'does not recognize other paths' do
282
+ @app.request('GET', '/settings/profile/edit').status.must_equal 404
283
+
284
+ -> { @router.path(:settings_edit_profile) }.must_raise HttpRouter::InvalidRouteException
285
+ end
286
+ end
287
+ end
288
+ end
289
+ end
data/test/new_test.rb ADDED
@@ -0,0 +1,67 @@
1
+ require 'test_helper'
2
+
3
+ describe Lotus::Router do
4
+ describe '.new' do
5
+ before do
6
+ class MockRoute
7
+ end
8
+
9
+ endpoint = ->(env) { [200, {}, ['']] }
10
+ @router = Lotus::Router.new do
11
+ get '/route', to: endpoint
12
+ get '/named_route', to: endpoint, as: :named_route
13
+ resource 'avatar'
14
+ resources 'flowers'
15
+ namespace 'admin' do
16
+ get '/dashboard', to: endpoint
17
+ end
18
+ end
19
+
20
+ @app = Rack::MockRequest.new(@router)
21
+ end
22
+
23
+ it 'returns instance of Lotus::Router with empty block' do
24
+ router = Lotus::Router.new { }
25
+ router.must_be_instance_of Lotus::Router
26
+ end
27
+
28
+ it 'returns instance of Lotus::Router' do
29
+ @router.must_be_instance_of Lotus::Router
30
+ end
31
+
32
+ it 'sets options' do
33
+ router = Lotus::Router.new(scheme: 'https') do
34
+ get '/', to: ->(env) { }, as: :root
35
+ end
36
+
37
+ router.url(:root).must_match('https')
38
+ end
39
+
40
+ it 'sets custom separator' do
41
+ router = Lotus::Router.new(action_separator: '^')
42
+ route = router.get('/', to: 'test^show', as: :root)
43
+
44
+ route.dest.must_equal(TestController::Show)
45
+ end
46
+
47
+ it 'recognizes path' do
48
+ @app.get('/route').status.must_equal 200
49
+ end
50
+
51
+ it 'recognizes named path' do
52
+ @app.get('/named_route').status.must_equal 200
53
+ end
54
+
55
+ it 'recognizes resource' do
56
+ @app.get('/avatar').status.must_equal 200
57
+ end
58
+
59
+ it 'recognizes resources' do
60
+ @app.get('/avatar').status.must_equal 200
61
+ end
62
+
63
+ it 'recognizes namespaced path' do
64
+ @app.get('/admin/dashboard').status.must_equal 200
65
+ end
66
+ end
67
+ end