lotus-router 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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