apiwork 0.0.0.pre → 0.1.2

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 (202) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +2 -2
  3. data/README.md +117 -1
  4. data/Rakefile +5 -3
  5. data/app/controllers/apiwork/errors_controller.rb +13 -0
  6. data/app/controllers/apiwork/exports_controller.rb +22 -0
  7. data/lib/apiwork/abstractable.rb +26 -0
  8. data/lib/apiwork/adapter/base.rb +369 -0
  9. data/lib/apiwork/adapter/builder/api/base.rb +66 -0
  10. data/lib/apiwork/adapter/builder/contract/base.rb +86 -0
  11. data/lib/apiwork/adapter/capability/api/base.rb +51 -0
  12. data/lib/apiwork/adapter/capability/api/scope.rb +64 -0
  13. data/lib/apiwork/adapter/capability/base.rb +291 -0
  14. data/lib/apiwork/adapter/capability/contract/base.rb +37 -0
  15. data/lib/apiwork/adapter/capability/contract/scope.rb +110 -0
  16. data/lib/apiwork/adapter/capability/operation/base.rb +172 -0
  17. data/lib/apiwork/adapter/capability/operation/metadata_shape.rb +165 -0
  18. data/lib/apiwork/adapter/capability/result.rb +21 -0
  19. data/lib/apiwork/adapter/capability/runner.rb +56 -0
  20. data/lib/apiwork/adapter/capability/transformer/request/base.rb +72 -0
  21. data/lib/apiwork/adapter/capability/transformer/response/base.rb +45 -0
  22. data/lib/apiwork/adapter/registry.rb +16 -0
  23. data/lib/apiwork/adapter/serializer/error/base.rb +72 -0
  24. data/lib/apiwork/adapter/serializer/error/default/api_builder.rb +32 -0
  25. data/lib/apiwork/adapter/serializer/error/default.rb +37 -0
  26. data/lib/apiwork/adapter/serializer/resource/base.rb +84 -0
  27. data/lib/apiwork/adapter/serializer/resource/default/contract_builder.rb +209 -0
  28. data/lib/apiwork/adapter/serializer/resource/default.rb +39 -0
  29. data/lib/apiwork/adapter/standard/capability/filtering/api_builder.rb +75 -0
  30. data/lib/apiwork/adapter/standard/capability/filtering/constants.rb +37 -0
  31. data/lib/apiwork/adapter/standard/capability/filtering/contract_builder.rb +193 -0
  32. data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/builder.rb +47 -0
  33. data/lib/apiwork/adapter/standard/capability/filtering/operation/filter/operator_builder.rb +36 -0
  34. data/lib/apiwork/adapter/standard/capability/filtering/operation/filter.rb +462 -0
  35. data/lib/apiwork/adapter/standard/capability/filtering/operation.rb +22 -0
  36. data/lib/apiwork/adapter/standard/capability/filtering/request_transformer.rb +47 -0
  37. data/lib/apiwork/adapter/standard/capability/filtering.rb +18 -0
  38. data/lib/apiwork/adapter/standard/capability/including/contract_builder.rb +169 -0
  39. data/lib/apiwork/adapter/standard/capability/including/operation.rb +20 -0
  40. data/lib/apiwork/adapter/standard/capability/including.rb +16 -0
  41. data/lib/apiwork/adapter/standard/capability/pagination/api_builder.rb +34 -0
  42. data/lib/apiwork/adapter/standard/capability/pagination/contract_builder.rb +35 -0
  43. data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/cursor.rb +84 -0
  44. data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate/offset.rb +66 -0
  45. data/lib/apiwork/adapter/standard/capability/pagination/operation/paginate.rb +24 -0
  46. data/lib/apiwork/adapter/standard/capability/pagination/operation.rb +24 -0
  47. data/lib/apiwork/adapter/standard/capability/pagination.rb +21 -0
  48. data/lib/apiwork/adapter/standard/capability/sorting/api_builder.rb +19 -0
  49. data/lib/apiwork/adapter/standard/capability/sorting/contract_builder.rb +84 -0
  50. data/lib/apiwork/adapter/standard/capability/sorting/operation/sort.rb +83 -0
  51. data/lib/apiwork/adapter/standard/capability/sorting/operation.rb +22 -0
  52. data/lib/apiwork/adapter/standard/capability/sorting.rb +17 -0
  53. data/lib/apiwork/adapter/standard/capability/writing/constants.rb +15 -0
  54. data/lib/apiwork/adapter/standard/capability/writing/contract_builder.rb +253 -0
  55. data/lib/apiwork/adapter/standard/capability/writing/operation/issue_mapper.rb +210 -0
  56. data/lib/apiwork/adapter/standard/capability/writing/operation.rb +32 -0
  57. data/lib/apiwork/adapter/standard/capability/writing/request_transformer.rb +37 -0
  58. data/lib/apiwork/adapter/standard/capability/writing.rb +17 -0
  59. data/lib/apiwork/adapter/standard/includes_resolver.rb +106 -0
  60. data/lib/apiwork/adapter/standard.rb +22 -0
  61. data/lib/apiwork/adapter/wrapper/base.rb +70 -0
  62. data/lib/apiwork/adapter/wrapper/collection/base.rb +60 -0
  63. data/lib/apiwork/adapter/wrapper/collection/default.rb +47 -0
  64. data/lib/apiwork/adapter/wrapper/error/base.rb +30 -0
  65. data/lib/apiwork/adapter/wrapper/error/default.rb +34 -0
  66. data/lib/apiwork/adapter/wrapper/member/base.rb +58 -0
  67. data/lib/apiwork/adapter/wrapper/member/default.rb +40 -0
  68. data/lib/apiwork/adapter/wrapper/shape.rb +203 -0
  69. data/lib/apiwork/adapter.rb +50 -0
  70. data/lib/apiwork/api/base.rb +802 -0
  71. data/lib/apiwork/api/element.rb +110 -0
  72. data/lib/apiwork/api/enum_registry/definition.rb +51 -0
  73. data/lib/apiwork/api/enum_registry.rb +98 -0
  74. data/lib/apiwork/api/info/contact.rb +67 -0
  75. data/lib/apiwork/api/info/license.rb +50 -0
  76. data/lib/apiwork/api/info/server.rb +50 -0
  77. data/lib/apiwork/api/info.rb +221 -0
  78. data/lib/apiwork/api/object.rb +235 -0
  79. data/lib/apiwork/api/registry.rb +33 -0
  80. data/lib/apiwork/api/representation_registry.rb +76 -0
  81. data/lib/apiwork/api/resource/action.rb +41 -0
  82. data/lib/apiwork/api/resource.rb +648 -0
  83. data/lib/apiwork/api/router.rb +104 -0
  84. data/lib/apiwork/api/type_registry/definition.rb +117 -0
  85. data/lib/apiwork/api/type_registry.rb +99 -0
  86. data/lib/apiwork/api/union.rb +49 -0
  87. data/lib/apiwork/api.rb +85 -0
  88. data/lib/apiwork/configurable.rb +71 -0
  89. data/lib/apiwork/configuration/option.rb +125 -0
  90. data/lib/apiwork/configuration/validatable.rb +25 -0
  91. data/lib/apiwork/configuration.rb +95 -0
  92. data/lib/apiwork/configuration_error.rb +6 -0
  93. data/lib/apiwork/constraint_error.rb +20 -0
  94. data/lib/apiwork/contract/action/request.rb +79 -0
  95. data/lib/apiwork/contract/action/response.rb +87 -0
  96. data/lib/apiwork/contract/action.rb +258 -0
  97. data/lib/apiwork/contract/base.rb +714 -0
  98. data/lib/apiwork/contract/element.rb +130 -0
  99. data/lib/apiwork/contract/object/coercer.rb +194 -0
  100. data/lib/apiwork/contract/object/deserializer.rb +101 -0
  101. data/lib/apiwork/contract/object/transformer.rb +95 -0
  102. data/lib/apiwork/contract/object/validator/result.rb +27 -0
  103. data/lib/apiwork/contract/object/validator.rb +734 -0
  104. data/lib/apiwork/contract/object.rb +566 -0
  105. data/lib/apiwork/contract/request_parser/result.rb +25 -0
  106. data/lib/apiwork/contract/request_parser.rb +72 -0
  107. data/lib/apiwork/contract/response_parser/result.rb +25 -0
  108. data/lib/apiwork/contract/response_parser.rb +35 -0
  109. data/lib/apiwork/contract/union.rb +56 -0
  110. data/lib/apiwork/contract_error.rb +9 -0
  111. data/lib/apiwork/controller.rb +300 -0
  112. data/lib/apiwork/domain_error.rb +13 -0
  113. data/lib/apiwork/element.rb +386 -0
  114. data/lib/apiwork/engine.rb +20 -0
  115. data/lib/apiwork/error.rb +6 -0
  116. data/lib/apiwork/error_code/definition.rb +63 -0
  117. data/lib/apiwork/error_code/registry.rb +18 -0
  118. data/lib/apiwork/error_code.rb +132 -0
  119. data/lib/apiwork/export/base.rb +291 -0
  120. data/lib/apiwork/export/open_api.rb +600 -0
  121. data/lib/apiwork/export/pipeline/writer.rb +66 -0
  122. data/lib/apiwork/export/pipeline.rb +84 -0
  123. data/lib/apiwork/export/registry.rb +16 -0
  124. data/lib/apiwork/export/surface_resolver.rb +189 -0
  125. data/lib/apiwork/export/type_analysis.rb +170 -0
  126. data/lib/apiwork/export/type_script.rb +23 -0
  127. data/lib/apiwork/export/type_script_mapper.rb +349 -0
  128. data/lib/apiwork/export/zod.rb +39 -0
  129. data/lib/apiwork/export/zod_mapper.rb +421 -0
  130. data/lib/apiwork/export.rb +80 -0
  131. data/lib/apiwork/http_error.rb +16 -0
  132. data/lib/apiwork/introspection/action/request.rb +66 -0
  133. data/lib/apiwork/introspection/action/response.rb +57 -0
  134. data/lib/apiwork/introspection/action.rb +124 -0
  135. data/lib/apiwork/introspection/api/info/contact.rb +59 -0
  136. data/lib/apiwork/introspection/api/info/license.rb +49 -0
  137. data/lib/apiwork/introspection/api/info/server.rb +50 -0
  138. data/lib/apiwork/introspection/api/info.rb +107 -0
  139. data/lib/apiwork/introspection/api/resource.rb +83 -0
  140. data/lib/apiwork/introspection/api.rb +92 -0
  141. data/lib/apiwork/introspection/contract.rb +63 -0
  142. data/lib/apiwork/introspection/dump/action.rb +101 -0
  143. data/lib/apiwork/introspection/dump/api.rb +119 -0
  144. data/lib/apiwork/introspection/dump/contract.rb +129 -0
  145. data/lib/apiwork/introspection/dump/param.rb +486 -0
  146. data/lib/apiwork/introspection/dump/resource.rb +112 -0
  147. data/lib/apiwork/introspection/dump/type.rb +339 -0
  148. data/lib/apiwork/introspection/dump.rb +17 -0
  149. data/lib/apiwork/introspection/enum.rb +63 -0
  150. data/lib/apiwork/introspection/error_code.rb +44 -0
  151. data/lib/apiwork/introspection/param/array.rb +88 -0
  152. data/lib/apiwork/introspection/param/base.rb +285 -0
  153. data/lib/apiwork/introspection/param/binary.rb +73 -0
  154. data/lib/apiwork/introspection/param/boolean.rb +73 -0
  155. data/lib/apiwork/introspection/param/date.rb +73 -0
  156. data/lib/apiwork/introspection/param/date_time.rb +73 -0
  157. data/lib/apiwork/introspection/param/decimal.rb +121 -0
  158. data/lib/apiwork/introspection/param/integer.rb +131 -0
  159. data/lib/apiwork/introspection/param/literal.rb +45 -0
  160. data/lib/apiwork/introspection/param/number.rb +121 -0
  161. data/lib/apiwork/introspection/param/object.rb +59 -0
  162. data/lib/apiwork/introspection/param/reference.rb +45 -0
  163. data/lib/apiwork/introspection/param/string.rb +122 -0
  164. data/lib/apiwork/introspection/param/time.rb +73 -0
  165. data/lib/apiwork/introspection/param/union.rb +57 -0
  166. data/lib/apiwork/introspection/param/unknown.rb +26 -0
  167. data/lib/apiwork/introspection/param/uuid.rb +73 -0
  168. data/lib/apiwork/introspection/param.rb +31 -0
  169. data/lib/apiwork/introspection/type.rb +129 -0
  170. data/lib/apiwork/introspection.rb +28 -0
  171. data/lib/apiwork/issue.rb +80 -0
  172. data/lib/apiwork/json_pointer.rb +21 -0
  173. data/lib/apiwork/object.rb +1618 -0
  174. data/lib/apiwork/reference_generator.rb +638 -0
  175. data/lib/apiwork/registry.rb +56 -0
  176. data/lib/apiwork/representation/association.rb +391 -0
  177. data/lib/apiwork/representation/attribute.rb +335 -0
  178. data/lib/apiwork/representation/base.rb +819 -0
  179. data/lib/apiwork/representation/deserializer.rb +95 -0
  180. data/lib/apiwork/representation/element.rb +128 -0
  181. data/lib/apiwork/representation/inheritance.rb +78 -0
  182. data/lib/apiwork/representation/model_detector.rb +75 -0
  183. data/lib/apiwork/representation/root_key.rb +35 -0
  184. data/lib/apiwork/representation/serializer.rb +127 -0
  185. data/lib/apiwork/request.rb +79 -0
  186. data/lib/apiwork/response.rb +56 -0
  187. data/lib/apiwork/union.rb +102 -0
  188. data/lib/apiwork/version.rb +2 -2
  189. data/lib/apiwork.rb +61 -3
  190. data/lib/generators/apiwork/api_generator.rb +38 -0
  191. data/lib/generators/apiwork/contract_generator.rb +25 -0
  192. data/lib/generators/apiwork/install_generator.rb +27 -0
  193. data/lib/generators/apiwork/representation_generator.rb +25 -0
  194. data/lib/generators/apiwork/templates/api/api.rb.tt +4 -0
  195. data/lib/generators/apiwork/templates/contract/contract.rb.tt +6 -0
  196. data/lib/generators/apiwork/templates/install/application_contract.rb.tt +5 -0
  197. data/lib/generators/apiwork/templates/install/application_representation.rb.tt +5 -0
  198. data/lib/generators/apiwork/templates/representation/representation.rb.tt +6 -0
  199. data/lib/tasks/apiwork.rake +102 -0
  200. metadata +319 -19
  201. data/.rubocop.yml +0 -8
  202. data/sig/apiwork.rbs +0 -4
@@ -0,0 +1,648 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apiwork
4
+ module API
5
+ # @api public
6
+ # Block context for defining API resources and routes.
7
+ #
8
+ # Resource provides the DSL available inside `resources` and `resource`
9
+ # blocks. Methods include nested resources, custom actions, and concerns.
10
+ #
11
+ # @example Defining resources with actions
12
+ # Apiwork::API.define '/api/v1' do
13
+ # resources :invoices do
14
+ # member do
15
+ # post :send
16
+ # get :preview
17
+ # end
18
+ #
19
+ # collection do
20
+ # get :search
21
+ # end
22
+ #
23
+ # resources :items
24
+ # end
25
+ # end
26
+ class Resource
27
+ attr_reader :api_class,
28
+ :constraints,
29
+ :contract_class_name,
30
+ :controller,
31
+ :defaults,
32
+ :except,
33
+ :name,
34
+ :only,
35
+ :param,
36
+ :path,
37
+ :singular
38
+
39
+ attr_accessor :contract_class
40
+
41
+ def initialize(
42
+ api_class,
43
+ constraints: nil,
44
+ contract_class_name: nil,
45
+ controller: nil,
46
+ defaults: nil,
47
+ except: nil,
48
+ name: nil,
49
+ only: nil,
50
+ param: nil,
51
+ path: nil,
52
+ singular: false
53
+ )
54
+ @api_class = api_class
55
+ @constraints = constraints
56
+ @contract_class_name = contract_class_name
57
+ @controller = controller
58
+ @defaults = defaults
59
+ @except = except
60
+ @name = name
61
+ @only = only
62
+ @param = param
63
+ @path = path
64
+ @singular = singular
65
+
66
+ @crud_actions = name ? determine_crud_actions(singular, except:, only:) : []
67
+ @custom_actions = []
68
+ @resources = {}
69
+ @concerns = {}
70
+ @resource_stack = []
71
+ @current_options = nil
72
+ @in_member_block = false
73
+ @in_collection_block = false
74
+ end
75
+
76
+ def index_actions?
77
+ @resources.values.any? { |resource| resource.actions.key?(:index) || resource.index_actions? }
78
+ end
79
+
80
+ def representation_classes
81
+ @representation_classes ||= collect_all_representation_classes
82
+ end
83
+
84
+ def representation_class
85
+ contract_class&.representation_class
86
+ end
87
+
88
+ def actions
89
+ @actions ||= build_actions
90
+ end
91
+
92
+ def member_actions
93
+ @custom_actions.select(&:member?).index_by(&:name)
94
+ end
95
+
96
+ def collection_actions
97
+ @custom_actions.select(&:collection?).index_by(&:name)
98
+ end
99
+
100
+ def add_action(action_name, method:, type:)
101
+ @custom_actions << Action.new(action_name, method:, type:)
102
+ end
103
+
104
+ def add_resource(resource)
105
+ @resources[resource.name] = resource
106
+ end
107
+
108
+ def find_resource(resource_name = nil, &block)
109
+ return find_resource_by_block(&block) if block
110
+ return @resources[resource_name] if @resources[resource_name]
111
+
112
+ @resources.each_value do |resource|
113
+ found = resource.find_resource(resource_name)
114
+ return found if found
115
+ end
116
+
117
+ nil
118
+ end
119
+
120
+ def find_resource_for_path(resource_path)
121
+ current = nil
122
+ resource_path.split('/').each do |part|
123
+ next if part.empty?
124
+
125
+ resource_name = part.tr('-', '_').to_sym
126
+ target = current ? current.resources : @resources
127
+ found = target[resource_name] || target[resource_name.to_s.singularize.to_sym]
128
+ next unless found
129
+
130
+ current = found
131
+ end
132
+ current
133
+ end
134
+
135
+ def each_resource(&block)
136
+ @resources.each_value do |resource|
137
+ yield resource
138
+ resource.each_resource(&block)
139
+ end
140
+ end
141
+
142
+ def resolve_contract_class
143
+ return @contract_class if @contract_class
144
+ return nil unless @contract_class_name
145
+
146
+ @contract_class = @contract_class_name.constantize
147
+ rescue NameError
148
+ nil
149
+ end
150
+
151
+ # @api public
152
+ # Defines a plural resource with standard CRUD actions.
153
+ #
154
+ # Default actions: :index, :show, :create, :update, :destroy.
155
+ #
156
+ # @param resource_name [Symbol]
157
+ # The resource name (plural).
158
+ # @param concerns [Array<Symbol>, nil] (nil)
159
+ # The concerns to include.
160
+ # @param constraints [Hash, Proc, nil] (nil)
161
+ # The route constraints.
162
+ # @param contract [String, nil] (nil)
163
+ # The custom contract path.
164
+ # @param controller [String, nil] (nil)
165
+ # The custom controller path.
166
+ # @param defaults [Hash, nil] (nil)
167
+ # The default route parameters.
168
+ # @param except [Array<Symbol>, nil] (nil)
169
+ # The actions to exclude.
170
+ # @param only [Array<Symbol>, nil] (nil)
171
+ # The CRUD actions to include.
172
+ # @param param [Symbol, nil] (nil)
173
+ # The custom ID parameter.
174
+ # @param path [String, nil] (nil)
175
+ # The custom URL segment.
176
+ # @yield block for nested resources and custom actions
177
+ # @yieldparam resource [Resource]
178
+ # @return [Hash{Symbol => Resource}]
179
+ #
180
+ # @example instance_eval style
181
+ # resources :invoices do
182
+ # member { get :preview }
183
+ # resources :items
184
+ # end
185
+ #
186
+ # @example yield style
187
+ # resources :invoices do |resource|
188
+ # resource.member { |member| member.get :preview }
189
+ # resource.resources :items
190
+ # end
191
+ def resources(
192
+ resource_name = nil,
193
+ concerns: nil,
194
+ constraints: nil,
195
+ contract: nil,
196
+ controller: nil,
197
+ defaults: nil,
198
+ except: nil,
199
+ only: nil,
200
+ param: nil,
201
+ path: nil,
202
+ &block
203
+ )
204
+ return @resources if resource_name.nil?
205
+
206
+ options = {
207
+ constraints:,
208
+ contract:,
209
+ controller:,
210
+ defaults:,
211
+ except:,
212
+ only:,
213
+ param:,
214
+ path:,
215
+ }.compact
216
+ build_resource(resource_name, options:, singular: false)
217
+
218
+ @resource_stack.push(resource_name)
219
+
220
+ self.concerns(*concerns) if concerns
221
+ if block
222
+ block.arity.positive? ? yield(self) : instance_eval(&block)
223
+ end
224
+
225
+ @resource_stack.pop
226
+ end
227
+
228
+ # @api public
229
+ # Defines a singular resource (no index, no :id in URL).
230
+ #
231
+ # Default actions: :show, :create, :update, :destroy.
232
+ #
233
+ # @param resource_name [Symbol]
234
+ # The resource name (singular).
235
+ # @param concerns [Array<Symbol>, nil] (nil)
236
+ # The concerns to include.
237
+ # @param constraints [Hash, Proc, nil] (nil)
238
+ # The route constraints.
239
+ # @param contract [String, nil] (nil)
240
+ # The custom contract path.
241
+ # @param controller [String, nil] (nil)
242
+ # The custom controller path.
243
+ # @param defaults [Hash, nil] (nil)
244
+ # The default route parameters.
245
+ # @param except [Array<Symbol>, nil] (nil)
246
+ # The actions to exclude.
247
+ # @param only [Array<Symbol>, nil] (nil)
248
+ # The CRUD actions to include.
249
+ # @param param [Symbol, nil] (nil)
250
+ # The custom ID parameter.
251
+ # @param path [String, nil] (nil)
252
+ # The custom URL segment.
253
+ # @yield block for nested resources and custom actions
254
+ # @yieldparam resource [Resource]
255
+ # @return [void]
256
+ #
257
+ # @example instance_eval style
258
+ # resource :profile do
259
+ # resources :settings
260
+ # end
261
+ #
262
+ # @example yield style
263
+ # resource :profile do |resource|
264
+ # resource.resources :settings
265
+ # end
266
+ def resource(
267
+ resource_name,
268
+ concerns: nil,
269
+ constraints: nil,
270
+ contract: nil,
271
+ controller: nil,
272
+ defaults: nil,
273
+ except: nil,
274
+ only: nil,
275
+ param: nil,
276
+ path: nil,
277
+ &block
278
+ )
279
+ options = {
280
+ constraints:,
281
+ contract:,
282
+ controller:,
283
+ defaults:,
284
+ except:,
285
+ only:,
286
+ param:,
287
+ path:,
288
+ }.compact
289
+ build_resource(resource_name, options:, singular: true)
290
+
291
+ @resource_stack.push(resource_name)
292
+
293
+ self.concerns(*concerns) if concerns
294
+ if block
295
+ block.arity.positive? ? yield(self) : instance_eval(&block)
296
+ end
297
+
298
+ @resource_stack.pop
299
+ end
300
+
301
+ # @api public
302
+ # Applies options to all resources defined in the block.
303
+ #
304
+ # @param options [Hash] ({})
305
+ # The options to merge into nested resources.
306
+ # @yield block with resource definitions
307
+ # @yieldparam resource [Resource]
308
+ # @return [void]
309
+ #
310
+ # @example instance_eval style
311
+ # with_options only: [:index, :show] do
312
+ # resources :reports
313
+ # resources :analytics
314
+ # end
315
+ #
316
+ # @example yield style
317
+ # with_options only: [:index, :show] do |resource|
318
+ # resource.resources :reports
319
+ # resource.resources :analytics
320
+ # end
321
+ def with_options(options = {}, &block)
322
+ old_options = @current_options
323
+ @current_options = merged_options(options)
324
+
325
+ block.arity.positive? ? yield(self) : instance_eval(&block)
326
+
327
+ @current_options = old_options
328
+ end
329
+
330
+ # @api public
331
+ # Block for defining member actions (operate on :id).
332
+ #
333
+ # Member routes include :id in the path: `/invoices/:id/action`
334
+ #
335
+ # @yield block with HTTP verb methods
336
+ # @yieldparam resource [Resource]
337
+ # @return [void]
338
+ #
339
+ # @example instance_eval style
340
+ # member do
341
+ # post :send
342
+ # get :preview
343
+ # end
344
+ #
345
+ # @example yield style
346
+ # member do |member|
347
+ # member.post :send
348
+ # member.get :preview
349
+ # end
350
+ def member(&block)
351
+ @in_member_block = true
352
+ block.arity.positive? ? yield(self) : instance_eval(&block)
353
+ @in_member_block = false
354
+ end
355
+
356
+ # @api public
357
+ # Block for defining collection actions.
358
+ #
359
+ # Collection routes don't include :id: `/invoices/action`
360
+ #
361
+ # @yield block with HTTP verb methods
362
+ # @yieldparam resource [Resource]
363
+ # @return [void]
364
+ #
365
+ # @example instance_eval style
366
+ # collection do
367
+ # get :search
368
+ # post :bulk_create
369
+ # end
370
+ #
371
+ # @example yield style
372
+ # collection do |collection|
373
+ # collection.get :search
374
+ # collection.post :bulk_create
375
+ # end
376
+ def collection(&block)
377
+ @in_collection_block = true
378
+ block.arity.positive? ? yield(self) : instance_eval(&block)
379
+ @in_collection_block = false
380
+ end
381
+
382
+ # @api public
383
+ # Defines a GET action.
384
+ #
385
+ # @param action_names [Symbol, Array<Symbol>]
386
+ # The action name(s).
387
+ # @param on [Symbol, nil] (nil) [:collection, :member]
388
+ # The scope.
389
+ # @return [void]
390
+ #
391
+ # @example Inside member block
392
+ # member { get :preview }
393
+ #
394
+ # @example With on parameter
395
+ # get :search, on: :collection
396
+ def get(action_names, on: nil)
397
+ capture_actions(action_names, on:, method: :get)
398
+ end
399
+
400
+ # @api public
401
+ # Defines a POST action.
402
+ #
403
+ # @param action_names [Symbol, Array<Symbol>]
404
+ # The action name(s).
405
+ # @param on [Symbol, nil] (nil) [:collection, :member]
406
+ # The scope.
407
+ # @return [void]
408
+ #
409
+ # @example
410
+ # member { post :send }
411
+ def post(action_names, on: nil)
412
+ capture_actions(action_names, on:, method: :post)
413
+ end
414
+
415
+ # @api public
416
+ # Defines a PATCH action.
417
+ #
418
+ # @param action_names [Symbol, Array<Symbol>]
419
+ # The action name(s).
420
+ # @param on [Symbol, nil] (nil) [:collection, :member]
421
+ # The scope.
422
+ # @return [void]
423
+ #
424
+ # @example
425
+ # member { patch :mark_paid }
426
+ def patch(action_names, on: nil)
427
+ capture_actions(action_names, on:, method: :patch)
428
+ end
429
+
430
+ # @api public
431
+ # Defines a PUT action.
432
+ #
433
+ # @param action_names [Symbol, Array<Symbol>]
434
+ # The action name(s).
435
+ # @param on [Symbol, nil] (nil) [:collection, :member]
436
+ # The scope.
437
+ # @return [void]
438
+ #
439
+ # @example
440
+ # member { put :replace }
441
+ def put(action_names, on: nil)
442
+ capture_actions(action_names, on:, method: :put)
443
+ end
444
+
445
+ # @api public
446
+ # Defines a DELETE action.
447
+ #
448
+ # @param action_names [Symbol, Array<Symbol>]
449
+ # The action name(s).
450
+ # @param on [Symbol, nil] (nil) [:collection, :member]
451
+ # The scope.
452
+ # @return [void]
453
+ #
454
+ # @example
455
+ # member { delete :archive }
456
+ def delete(action_names, on: nil)
457
+ capture_actions(action_names, on:, method: :delete)
458
+ end
459
+
460
+ # @api public
461
+ # Defines a reusable concern.
462
+ #
463
+ # @param concern_name [Symbol]
464
+ # The concern name.
465
+ # @param callable [Proc, nil] (nil)
466
+ # Optional callable instead of block.
467
+ # @yield block with resource definitions
468
+ # @yieldparam resource [Resource]
469
+ # @return [void]
470
+ #
471
+ # @example instance_eval style
472
+ # concern :commentable do
473
+ # resources :comments
474
+ # end
475
+ #
476
+ # resources :posts, concerns: [:commentable]
477
+ #
478
+ # @example yield style
479
+ # concern :commentable do |resource|
480
+ # resource.resources :comments
481
+ # end
482
+ #
483
+ # resources :posts, concerns: [:commentable]
484
+ def concern(concern_name, callable = nil, &block)
485
+ callable ||= lambda do |resource, options|
486
+ if block.arity.positive?
487
+ yield(resource, options)
488
+ else
489
+ resource.instance_exec(options, &block)
490
+ end
491
+ end
492
+ @concerns[concern_name] = callable
493
+ end
494
+
495
+ # @api public
496
+ # Includes previously defined concerns.
497
+ #
498
+ # @param concern_names [Array<Symbol>]
499
+ # The concern names to include.
500
+ # @param options [Hash] ({})
501
+ # The options passed to the concern.
502
+ # @return [void]
503
+ #
504
+ # @example
505
+ # resources :posts do
506
+ # concerns :commentable, :taggable
507
+ # end
508
+ def concerns(*concern_names, **options)
509
+ concern_names.flatten.each do |concern_name|
510
+ callable = @concerns[concern_name]
511
+ raise ConfigurationError, "No concern named :#{concern_name} was found" unless callable
512
+
513
+ callable.call(self, options)
514
+ end
515
+ end
516
+
517
+ private
518
+
519
+ def collect_all_representation_classes
520
+ representation_classes = []
521
+ each_resource do |resource|
522
+ representation_class = resource.representation_class
523
+ representation_classes << representation_class if representation_class
524
+ end
525
+ representation_classes
526
+ end
527
+
528
+ def find_resource_by_block(&block)
529
+ @resources.each_value do |resource|
530
+ return resource if yield(resource)
531
+
532
+ found = resource.find_resource(&block)
533
+ return found if found
534
+ end
535
+
536
+ nil
537
+ end
538
+
539
+ def merged_options(options = {})
540
+ (@current_options || {}).merge(options)
541
+ end
542
+
543
+ def build_resource(resource_name, options:, singular:)
544
+ merged = merged_options(options)
545
+
546
+ parent_name = @resource_stack.last
547
+ parent_resource = parent_name ? find_resource(parent_name) : nil
548
+
549
+ contract = merged.delete(:contract)
550
+
551
+ resource = Resource.new(
552
+ @api_class,
553
+ name: resource_name,
554
+ singular:,
555
+ contract_class_name: contract ? contract_path_to_class_name(contract) : infer_contract_class_name(resource_name),
556
+ **merged,
557
+ )
558
+
559
+ if parent_resource
560
+ parent_resource.add_resource(resource)
561
+ else
562
+ add_resource(resource)
563
+ end
564
+ end
565
+
566
+ def capture_actions(action_names, method:, on:)
567
+ Array(action_names).each do |action_name|
568
+ capture_action(action_name, method:, on:)
569
+ end
570
+ end
571
+
572
+ def capture_action(action_name, method:, on:)
573
+ resource_name = @resource_stack.last
574
+ return unless resource_name
575
+
576
+ resource = find_resource(resource_name)
577
+ return unless resource
578
+
579
+ if on && [:member, :collection].exclude?(on)
580
+ raise ConfigurationError,
581
+ ":on option must be either :member or :collection, got #{on.inspect}"
582
+ end
583
+
584
+ action_type = if @in_member_block || on == :member
585
+ :member
586
+ elsif @in_collection_block || on == :collection
587
+ :collection
588
+ end
589
+
590
+ if action_type
591
+ resource.add_action(action_name, method:, type: action_type)
592
+ else
593
+ raise ConfigurationError,
594
+ "Action '#{action_name}' on resource '#{resource_name}' must be declared " \
595
+ "within a member or collection block, or use the :on parameter.\n" \
596
+ "Examples:\n" \
597
+ " member { #{method} :#{action_name} }\n" \
598
+ " #{method} :#{action_name}, on: :member\n" \
599
+ " collection { #{method} :#{action_name} }\n" \
600
+ " #{method} :#{action_name}, on: :collection"
601
+ end
602
+ end
603
+
604
+ def infer_contract_class_name(resource_name)
605
+ namespaces = @api_class.namespaces
606
+ [*namespaces.map { |namespace| namespace.to_s.camelize }, "#{resource_name.to_s.singularize.camelize}Contract"].join('::')
607
+ end
608
+
609
+ def contract_path_to_class_name(contract_path)
610
+ namespaces = @api_class.namespaces
611
+ parts = if contract_path.start_with?('/')
612
+ contract_path[1..].split('/')
613
+ else
614
+ namespaces + contract_path.split('/')
615
+ end
616
+
617
+ parts = parts.map { |part| part.to_s.camelize }
618
+ parts[-1] = parts[-1].singularize
619
+
620
+ "#{parts.join('::')}Contract"
621
+ end
622
+
623
+ def determine_crud_actions(singular, except:, only:)
624
+ if only
625
+ Array(only).map(&:to_sym)
626
+ else
627
+ default_actions = if singular
628
+ [:show, :create, :update, :destroy]
629
+ else
630
+ [:index, :show, :create, :update, :destroy]
631
+ end
632
+
633
+ if except
634
+ default_actions - Array(except).map(&:to_sym)
635
+ else
636
+ default_actions
637
+ end
638
+ end
639
+ end
640
+
641
+ def build_actions
642
+ actions = @crud_actions.map { |action_name| Action.new(action_name) }
643
+ actions.concat(@custom_actions)
644
+ actions.index_by(&:name)
645
+ end
646
+ end
647
+ end
648
+ end