hanami-router 0.0.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,116 @@
1
+ require 'hanami/utils/class_attribute'
2
+ require 'hanami/routing/resource/options'
3
+ require 'hanami/routing/resource/action'
4
+
5
+ module Hanami
6
+ module Routing
7
+ # Set of RESTful resource routes
8
+ # Implementation of Hanami::Router#resource
9
+ #
10
+ # @since 0.1.0
11
+ #
12
+ # @api private
13
+ #
14
+ # @see Hanami::Router#resource
15
+ class Resource
16
+ include Utils::ClassAttribute
17
+
18
+ # @api private
19
+ # @since 0.4.0
20
+ NESTED_ROUTES_SEPARATOR = '/'.freeze
21
+
22
+ # Set of default routes
23
+ #
24
+ # @api private
25
+ # @since 0.1.0
26
+ class_attribute :actions
27
+ self.actions = [:new, :create, :show, :edit, :update, :destroy]
28
+
29
+ # Action class
30
+ #
31
+ # @api private
32
+ # @since 0.1.0
33
+ class_attribute :action
34
+ self.action = Resource::Action
35
+
36
+ # Member action class
37
+ #
38
+ # @api private
39
+ # @since 0.1.0
40
+ class_attribute :member
41
+ self.member = Resource::MemberAction
42
+
43
+ # Collection action class
44
+ #
45
+ # @api private
46
+ # @since 0.1.0
47
+ class_attribute :collection
48
+ self.collection = Resource::CollectionAction
49
+
50
+ # @api private
51
+ # @since 0.4.0
52
+ attr_reader :parent
53
+
54
+ # @api private
55
+ # @since 0.1.0
56
+ def initialize(router, name, options = {}, parent = nil, &blk)
57
+ @router = router
58
+ @name = name
59
+ @parent = parent
60
+ @options = Options.new(self.class.actions, options.merge(name: @name))
61
+ generate(&blk)
62
+ end
63
+
64
+ # Allow nested resources inside resource or resources
65
+ #
66
+ # @since 0.4.0
67
+ #
68
+ # @see Hanami::Router#resources
69
+ def resources(name, options = {}, &blk)
70
+ _resource(Resources, name, options, &blk)
71
+ end
72
+
73
+ # Allow nested resource inside resource or resources
74
+ #
75
+ # @since 0.4.0
76
+ #
77
+ # @see Hanami::Router#resource
78
+ def resource(name, options = {}, &blk)
79
+ _resource(Resource, name, options, &blk)
80
+ end
81
+
82
+ # Return separator
83
+ #
84
+ # @api private
85
+ # @since 0.4.0
86
+ def wildcard_param(route_param = nil)
87
+ NESTED_ROUTES_SEPARATOR
88
+ end
89
+
90
+ private
91
+
92
+ # @api private
93
+ # @since 0.4.0
94
+ def _resource(klass, name, options, &blk)
95
+ options = options.merge(separator: @options[:separator], namespace: @options[:namespace])
96
+ klass.new(@router, [@name, name].join(NESTED_ROUTES_SEPARATOR), options, self, &blk)
97
+ end
98
+
99
+ def generate(&blk)
100
+ instance_eval(&blk) if block_given?
101
+
102
+ @options.actions.each do |action|
103
+ self.class.action.generate(@router, action, @options, self)
104
+ end
105
+ end
106
+
107
+ def member(&blk)
108
+ self.class.member.new(@router, @options, self, &blk)
109
+ end
110
+
111
+ def collection(&blk)
112
+ self.class.collection.new(@router, @options, self, &blk)
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,387 @@
1
+ require 'hanami/utils/string'
2
+ require 'hanami/utils/path_prefix'
3
+ require 'hanami/utils/class_attribute'
4
+ require 'hanami/routing/resource/nested'
5
+
6
+ module Hanami
7
+ module Routing
8
+ class Resource
9
+ # Action for RESTful resource
10
+ #
11
+ # @since 0.1.0
12
+ #
13
+ # @api private
14
+ #
15
+ # @see Hanami::Router#resource
16
+ class Action
17
+ include Utils::ClassAttribute
18
+
19
+ # Nested routes separator
20
+ #
21
+ # @api private
22
+ # @since 0.4.0
23
+ NESTED_ROUTES_SEPARATOR = '/'.freeze
24
+
25
+ # Ruby namespace where lookup for default subclasses.
26
+ #
27
+ # @api private
28
+ # @since 0.1.0
29
+ class_attribute :namespace
30
+ self.namespace = Resource
31
+
32
+ # Accepted HTTP verb
33
+ #
34
+ # @api private
35
+ # @since 0.1.0
36
+ class_attribute :verb
37
+ self.verb = :get
38
+
39
+ # Separator for named routes
40
+ #
41
+ # @api private
42
+ # @since 0.2.0
43
+ class_attribute :named_route_separator
44
+ self.named_route_separator = '_'.freeze
45
+
46
+ # Generate an action for the given router
47
+ #
48
+ # @param router [Hanami::Router]
49
+ # @param action [Hanami::Routing::Resource::Action]
50
+ # @param options [Hash]
51
+ # @param resource [Hanami::Routing::Resource, Hanami::Routing::Resources]
52
+ #
53
+ # @api private
54
+ #
55
+ # @since 0.1.0
56
+ def self.generate(router, action, options = {}, resource = nil)
57
+ class_for(action).new(router, options, resource)
58
+ end
59
+
60
+ # Initialize an action
61
+ #
62
+ # @param router [Hanami::Router]
63
+ # @param options [Hash]
64
+ # @param resource [Hanami::Routing::Resource, Hanami::Routing::Resources]
65
+ # @param blk [Proc]
66
+ #
67
+ # @api private
68
+ #
69
+ # @since 0.1.0
70
+ def initialize(router, options = {}, resource = nil, &blk)
71
+ @router = router
72
+ @options = options
73
+ @resource = resource
74
+ generate(&blk)
75
+ end
76
+
77
+ # Generate an action for the given router
78
+ #
79
+ # @param blk [Proc]
80
+ #
81
+ # @api private
82
+ #
83
+ # @since 0.1.0
84
+ def generate(&blk)
85
+ @router.send verb, path, to: endpoint, as: as
86
+ instance_eval(&blk) if block_given?
87
+ end
88
+
89
+ # Resource name
90
+ #
91
+ # @return [String]
92
+ #
93
+ # @api private
94
+ # @since 0.1.0
95
+ #
96
+ # @example
97
+ # require 'hanami/router'
98
+ #
99
+ # Hanami::Router.new do
100
+ # resource 'identity'
101
+ # end
102
+ #
103
+ # # 'identity' is the name passed in the @options
104
+ def resource_name
105
+ @resource_name ||= @options[:name].to_s
106
+ end
107
+
108
+ # Namespace
109
+ #
110
+ # @api private
111
+ # @since 0.2.0
112
+ def namespace
113
+ @namespace ||= Utils::PathPrefix.new @options[:namespace]
114
+ end
115
+
116
+ private
117
+ # Load a subclass, according to the given action name
118
+ #
119
+ # @param action [String] the action name
120
+ #
121
+ # @example
122
+ # Hanami::Routing::Resource::Action.send(:class_for, 'New') # =>
123
+ # Hanami::Routing::Resource::New
124
+ #
125
+ # @api private
126
+ # @since 0.1.0
127
+ def self.class_for(action)
128
+ Utils::Class.load!(Utils::String.new(action).classify, namespace)
129
+ end
130
+
131
+ # Accepted HTTP verb
132
+ #
133
+ # @see Hanami::Routing::Resource::Action.verb
134
+ #
135
+ # @api private
136
+ # @since 0.1.0
137
+ def verb
138
+ self.class.verb
139
+ end
140
+
141
+ # The namespaced URL relative path
142
+ #
143
+ # @example
144
+ # require 'hanami/router'
145
+ #
146
+ # Hanami::Router.new do
147
+ # resources 'flowers'
148
+ #
149
+ # namespace 'animals' do
150
+ # resources 'mammals'
151
+ # end
152
+ # end
153
+ #
154
+ # # It will generate paths like '/flowers', '/flowers/:id' ..
155
+ # # It will generate paths like '/animals/mammals', '/animals/mammals/:id' ..
156
+ #
157
+ # @api private
158
+ # @since 0.1.0
159
+ def path
160
+ rest_path
161
+ end
162
+
163
+ # The URL relative path
164
+ #
165
+ # @example
166
+ # '/flowers'
167
+ # '/flowers/new'
168
+ # '/flowers/:id'
169
+ #
170
+ # @api private
171
+ # @since 0.1.0
172
+ def rest_path
173
+ namespace.join(_nested_rest_path || resource_name.to_s)
174
+ end
175
+
176
+ # The namespaced name of the action within the whole context of the router.
177
+ #
178
+ # @example
179
+ # require 'hanami/router'
180
+ #
181
+ # Hanami::Router.new do
182
+ # resources 'flowers'
183
+ #
184
+ # namespace 'animals' do
185
+ # resources 'mammals'
186
+ # end
187
+ # end
188
+ #
189
+ # # It will generate named routes like :flowers, :new_flowers ..
190
+ # # It will generate named routes like :animals_mammals, :animals_new_mammals ..
191
+ #
192
+ # @api private
193
+ # @since 0.1.0
194
+ def as
195
+ namespace.relative_join(_singularized_as, self.class.named_route_separator).to_sym
196
+ end
197
+
198
+ # The name of the RESTful action.
199
+ #
200
+ # @example
201
+ # 'index'
202
+ # 'new'
203
+ # 'create'
204
+ #
205
+ # @api private
206
+ # @since 0.1.0
207
+ def action_name
208
+ Utils::String.new(self.class.name).demodulize.downcase
209
+ end
210
+
211
+ # A string that represents the endpoint to be loaded.
212
+ # It is composed by controller and action name.
213
+ #
214
+ # @see Hanami::Routing::Resource::Action#separator
215
+ #
216
+ # @example
217
+ # 'flowers#index'
218
+ #
219
+ # @api private
220
+ # @since 0.1.0
221
+ def endpoint
222
+ [ controller_name, action_name ].join separator
223
+ end
224
+
225
+ # Separator between controller and action name
226
+ #
227
+ # @see Hanami::Routing::EndpointResolver#separator
228
+ #
229
+ # @example
230
+ # '#' # default
231
+ #
232
+ # @api private
233
+ # @since 0.1.0
234
+ def separator
235
+ @options[:separator]
236
+ end
237
+
238
+ # Resource controller name
239
+ #
240
+ # @example
241
+ # Hanami::Router.new do
242
+ # resources 'flowers', controller: 'rocks'
243
+ # end
244
+ #
245
+ # # It will mount path 'flowers/new' to Rocks::New instead of Flowers::New
246
+ # # Same for other action names
247
+ #
248
+ # @api private
249
+ # @since 0.4.0
250
+ def controller_name
251
+ @options[:controller] || resource_name
252
+ end
253
+
254
+ private
255
+
256
+ # Singularize as (helper route)
257
+ #
258
+ # @api private
259
+ # @since 0.4.0
260
+ def _singularized_as
261
+ resource_name.split(NESTED_ROUTES_SEPARATOR).map do |name|
262
+ Hanami::Utils::String.new(name).singularize
263
+ end
264
+ end
265
+
266
+ # Create nested rest path
267
+ #
268
+ # @api private
269
+ # @since 0.4.0
270
+ def _nested_rest_path
271
+ Nested.new(resource_name, @resource).to_path
272
+ end
273
+ end
274
+
275
+ # Collection action
276
+ # It implements #collection within a #resource block.
277
+ #
278
+ # @api private
279
+ # @since 0.1.0
280
+ # @see Hanami::Router#resource
281
+ class CollectionAction < Action
282
+ def generate(&blk)
283
+ instance_eval(&blk) if block_given?
284
+ end
285
+
286
+ protected
287
+ def method_missing(m, *args)
288
+ verb = m
289
+ action_name = Utils::PathPrefix.new(args.first).relative_join(nil)
290
+
291
+ @router.__send__ verb, path(action_name),
292
+ to: endpoint(action_name), as: as(action_name)
293
+ end
294
+
295
+ private
296
+ def path(action_name)
297
+ rest_path.join(action_name)
298
+ end
299
+
300
+ def endpoint(action_name)
301
+ [ controller_name, action_name ].join separator
302
+ end
303
+
304
+ def as(action_name)
305
+ [ action_name, super() ].join(self.class.named_route_separator).to_sym
306
+ end
307
+ end
308
+
309
+ # Collection action
310
+ # It implements #member within a #resource block.
311
+ #
312
+ # @api private
313
+ # @since 0.1.0
314
+ # @see Hanami::Router#resource
315
+ class MemberAction < CollectionAction
316
+ end
317
+
318
+ # Implementation of common methods for concrete member actions
319
+ #
320
+ # @api private
321
+ # @since 0.1.0
322
+ module DefaultMemberAction
323
+ private
324
+ def path
325
+ rest_path.join(action_name)
326
+ end
327
+
328
+ def as
329
+ [ action_name, super ].join(self.class.named_route_separator).to_sym
330
+ end
331
+ end
332
+
333
+ # New action
334
+ #
335
+ # @api private
336
+ # @since 0.1.0
337
+ # @see Hanami::Router#resource
338
+ class New < Action
339
+ include DefaultMemberAction
340
+ end
341
+
342
+ # Create action
343
+ #
344
+ # @api private
345
+ # @since 0.1.0
346
+ # @see Hanami::Router#resource
347
+ class Create < Action
348
+ self.verb = :post
349
+ end
350
+
351
+ # Show action
352
+ #
353
+ # @api private
354
+ # @since 0.1.0
355
+ # @see Hanami::Router#resource
356
+ class Show < Action
357
+ end
358
+
359
+ # Edit action
360
+ #
361
+ # @api private
362
+ # @since 0.1.0
363
+ # @see Hanami::Router#resource
364
+ class Edit < Action
365
+ include DefaultMemberAction
366
+ end
367
+
368
+ # Update action
369
+ #
370
+ # @api private
371
+ # @since 0.1.0
372
+ # @see Hanami::Router#resource
373
+ class Update < Action
374
+ self.verb = :patch
375
+ end
376
+
377
+ # Destroy action
378
+ #
379
+ # @api private
380
+ # @since 0.1.0
381
+ # @see Hanami::Router#resource
382
+ class Destroy < Action
383
+ self.verb = :delete
384
+ end
385
+ end
386
+ end
387
+ end