hanami-router 0.0.0 → 0.6.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.
@@ -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