caprese 0.2.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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +12 -0
  4. data/.travis.yml +18 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +41 -0
  8. data/Rakefile +8 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/caprese.gemspec +38 -0
  12. data/lib/caprese/adapter/json_api/error.rb +123 -0
  13. data/lib/caprese/adapter/json_api/json_pointer.rb +52 -0
  14. data/lib/caprese/adapter/json_api/jsonapi.rb +49 -0
  15. data/lib/caprese/adapter/json_api/link.rb +88 -0
  16. data/lib/caprese/adapter/json_api/meta.rb +37 -0
  17. data/lib/caprese/adapter/json_api/pagination_links.rb +69 -0
  18. data/lib/caprese/adapter/json_api/relationship.rb +65 -0
  19. data/lib/caprese/adapter/json_api/resource_identifier.rb +49 -0
  20. data/lib/caprese/adapter/json_api.rb +509 -0
  21. data/lib/caprese/concerns/versioning.rb +69 -0
  22. data/lib/caprese/controller/concerns/callbacks.rb +60 -0
  23. data/lib/caprese/controller/concerns/errors.rb +42 -0
  24. data/lib/caprese/controller/concerns/persistence.rb +327 -0
  25. data/lib/caprese/controller/concerns/query.rb +209 -0
  26. data/lib/caprese/controller/concerns/relationships.rb +250 -0
  27. data/lib/caprese/controller/concerns/rendering.rb +43 -0
  28. data/lib/caprese/controller/concerns/typing.rb +39 -0
  29. data/lib/caprese/controller.rb +26 -0
  30. data/lib/caprese/error.rb +121 -0
  31. data/lib/caprese/errors.rb +69 -0
  32. data/lib/caprese/record/errors.rb +82 -0
  33. data/lib/caprese/record.rb +19 -0
  34. data/lib/caprese/routing/caprese_resources.rb +27 -0
  35. data/lib/caprese/serializer/concerns/links.rb +96 -0
  36. data/lib/caprese/serializer/concerns/lookup.rb +37 -0
  37. data/lib/caprese/serializer/error_serializer.rb +13 -0
  38. data/lib/caprese/serializer.rb +13 -0
  39. data/lib/caprese/version.rb +3 -0
  40. data/lib/caprese.rb +35 -0
  41. metadata +273 -0
@@ -0,0 +1,49 @@
1
+ module Caprese
2
+ module Adapter
3
+ class JsonApi
4
+ class ResourceIdentifier
5
+ def self.type_for(class_name, serializer_type = nil, transform_options = {})
6
+ if serializer_type
7
+ raw_type = serializer_type
8
+ else
9
+ inflection =
10
+ if ActiveModelSerializers.config.jsonapi_resource_type == :singular
11
+ :singularize
12
+ else
13
+ :pluralize
14
+ end
15
+
16
+ raw_type = class_name.underscore
17
+ raw_type = ActiveSupport::Inflector.public_send(inflection, raw_type)
18
+ raw_type
19
+ end
20
+ JsonApi.send(:transform_key_casing!, raw_type, transform_options)
21
+ end
22
+
23
+ # {http://jsonapi.org/format/#document-resource-identifier-objects Resource Identifier Objects}
24
+ def initialize(serializer, options)
25
+ @id = id_for(serializer)
26
+ @type = type_for(serializer, options)
27
+ end
28
+
29
+ def as_json
30
+ { id: id, type: type }
31
+ end
32
+
33
+ protected
34
+
35
+ attr_reader :id, :type
36
+
37
+ private
38
+
39
+ def type_for(serializer, transform_options)
40
+ self.class.type_for(serializer.object.class.name, serializer._type, transform_options)
41
+ end
42
+
43
+ def id_for(serializer)
44
+ serializer.read_attribute_for_serialization(Caprese.config.resource_primary_key).to_s
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,509 @@
1
+ require 'active_model_serializers'
2
+
3
+ module Caprese
4
+ module Adapter
5
+ class JsonApi < ActiveModelSerializers::Adapter::Base
6
+ extend ActiveSupport::Autoload
7
+ autoload :Jsonapi
8
+ autoload :ResourceIdentifier
9
+ autoload :Relationship
10
+ autoload :Link
11
+ autoload :PaginationLinks
12
+ autoload :Meta
13
+ autoload :Error
14
+ autoload :Deserialization
15
+
16
+ def self.default_key_transform
17
+ :dash
18
+ end
19
+
20
+ def self.fragment_cache(cached_hash, non_cached_hash, root = true)
21
+ core_cached = cached_hash.first
22
+ core_non_cached = non_cached_hash.first
23
+ no_root_cache = cached_hash.delete_if { |key, _value| key == core_cached[0] }
24
+ no_root_non_cache = non_cached_hash.delete_if { |key, _value| key == core_non_cached[0] }
25
+ cached_resource = (core_cached[1]) ? core_cached[1].deep_merge(core_non_cached[1]) : core_non_cached[1]
26
+ hash = root ? { root => cached_resource } : cached_resource
27
+
28
+ hash.deep_merge no_root_non_cache.deep_merge no_root_cache
29
+ end
30
+
31
+ def initialize(serializer, options = {})
32
+ super
33
+ @include_directive = JSONAPI::IncludeDirective.new(options[:include], allow_wildcard: true)
34
+ @fieldset = options[:fieldset] || ActiveModel::Serializer::Fieldset.new(options.delete(:fields))
35
+ end
36
+
37
+ # {http://jsonapi.org/format/#crud Requests are transactional, i.e. success or failure}
38
+ # {http://jsonapi.org/format/#document-top-level data and errors MUST NOT coexist in the same document.}
39
+ def serializable_hash(*)
40
+ document = if serializer.success?
41
+ success_document
42
+ else
43
+ failure_document
44
+ end
45
+ self.class.transform_key_casing!(document, instance_options)
46
+ end
47
+
48
+ def fragment_cache(cached_hash, non_cached_hash)
49
+ root = !instance_options.include?(:include)
50
+ self.class.fragment_cache(cached_hash, non_cached_hash, root)
51
+ end
52
+
53
+ # {http://jsonapi.org/format/#document-top-level Primary data}
54
+ # definition:
55
+ # ☐ toplevel_data (required)
56
+ # ☐ toplevel_included
57
+ # ☑ toplevel_meta
58
+ # ☑ toplevel_links
59
+ # ☑ toplevel_jsonapi
60
+ # structure:
61
+ # {
62
+ # data: toplevel_data,
63
+ # included: toplevel_included,
64
+ # meta: toplevel_meta,
65
+ # links: toplevel_links,
66
+ # jsonapi: toplevel_jsonapi
67
+ # }.reject! {|_,v| v.nil? }
68
+ # rubocop:disable Metrics/CyclomaticComplexity
69
+ def success_document
70
+ is_collection = serializer.respond_to?(:each)
71
+ serializers = is_collection ? serializer : [serializer]
72
+ primary_data, included = resource_objects_for(serializers)
73
+
74
+ hash = {}
75
+ # toplevel_data
76
+ # definition:
77
+ # oneOf
78
+ # resource
79
+ # array of unique items of type 'resource'
80
+ # null
81
+ #
82
+ # description:
83
+ # The document's "primary data" is a representation of the resource or collection of resources
84
+ # targeted by a request.
85
+ #
86
+ # Singular: the resource object.
87
+ #
88
+ # Collection: one of an array of resource objects, an array of resource identifier objects, or
89
+ # an empty array ([]), for requests that target resource collections.
90
+ #
91
+ # None: null if the request is one that might correspond to a single resource, but doesn't currently.
92
+ # structure:
93
+ # if serializable_resource.resource?
94
+ # resource
95
+ # elsif serializable_resource.collection?
96
+ # [
97
+ # resource,
98
+ # resource
99
+ # ]
100
+ # else
101
+ # nil
102
+ # end
103
+ hash[:data] = is_collection ? primary_data : primary_data[0]
104
+ # toplevel_included
105
+ # alias included
106
+ # definition:
107
+ # array of unique items of type 'resource'
108
+ #
109
+ # description:
110
+ # To reduce the number of HTTP requests, servers **MAY** allow
111
+ # responses that include related resources along with the requested primary
112
+ # resources. Such responses are called "compound documents".
113
+ # structure:
114
+ # [
115
+ # resource,
116
+ # resource
117
+ # ]
118
+ hash[:included] = included if included.any?
119
+
120
+ Jsonapi.add!(hash)
121
+
122
+ if instance_options[:links]
123
+ hash[:links] ||= {}
124
+ hash[:links].update(instance_options[:links])
125
+ end
126
+
127
+ if is_collection && serializer.paginated?
128
+ hash[:links] ||= {}
129
+ hash[:links].update(pagination_links_for(serializer))
130
+ end
131
+
132
+ hash[:meta] = instance_options[:meta] unless instance_options[:meta].blank?
133
+
134
+ hash
135
+ end
136
+ # rubocop:enable Metrics/CyclomaticComplexity
137
+
138
+ # {http://jsonapi.org/format/#errors JSON API Errors}
139
+ # TODO: look into caching
140
+ # definition:
141
+ # ☑ toplevel_errors array (required)
142
+ # ☐ toplevel_meta
143
+ # ☐ toplevel_jsonapi
144
+ # structure:
145
+ # {
146
+ # errors: toplevel_errors,
147
+ # meta: toplevel_meta,
148
+ # jsonapi: toplevel_jsonapi
149
+ # }.reject! {|_,v| v.nil? }
150
+ # prs:
151
+ # https://github.com/rails-api/active_model_serializers/pull/1004
152
+ def failure_document
153
+ hash = {}
154
+ # PR Please :)
155
+ # Jsonapi.add!(hash)
156
+
157
+ # toplevel_errors
158
+ # definition:
159
+ # array of unique items of type 'error'
160
+ # structure:
161
+ # [
162
+ # error,
163
+ # error
164
+ # ]
165
+ if serializer.respond_to?(:each)
166
+ hash[:errors] = serializer.flat_map do |error_serializer|
167
+ Error.resource_errors(error_serializer, instance_options)
168
+ end
169
+ else
170
+ hash[:errors] =
171
+ if serializer.resource_errors?
172
+ Error.resource_errors(serializer, instance_options)
173
+ else
174
+ Error.param_errors(serializer, instance_options)
175
+ end
176
+ end
177
+ hash
178
+ end
179
+
180
+ protected
181
+
182
+ attr_reader :fieldset
183
+
184
+ private
185
+
186
+ # {http://jsonapi.org/format/#document-resource-objects Primary data}
187
+ # resource
188
+ # definition:
189
+ # JSON Object
190
+ #
191
+ # properties:
192
+ # type (required) : String
193
+ # id (required) : String
194
+ # attributes
195
+ # relationships
196
+ # links
197
+ # meta
198
+ #
199
+ # description:
200
+ # "Resource objects" appear in a JSON API document to represent resources
201
+ # structure:
202
+ # {
203
+ # type: 'admin--some-user',
204
+ # id: '1336',
205
+ # attributes: attributes,
206
+ # relationships: relationships,
207
+ # links: links,
208
+ # meta: meta,
209
+ # }.reject! {|_,v| v.nil? }
210
+ # prs:
211
+ # type
212
+ # https://github.com/rails-api/active_model_serializers/pull/1122
213
+ # [x] https://github.com/rails-api/active_model_serializers/pull/1213
214
+ # https://github.com/rails-api/active_model_serializers/pull/1216
215
+ # https://github.com/rails-api/active_model_serializers/pull/1029
216
+ # links
217
+ # [x] https://github.com/rails-api/active_model_serializers/pull/1246
218
+ # [x] url helpers https://github.com/rails-api/active_model_serializers/issues/1269
219
+ # meta
220
+ # [x] https://github.com/rails-api/active_model_serializers/pull/1340
221
+ def resource_objects_for(serializers)
222
+ @primary = []
223
+ @included = []
224
+ @resource_identifiers = Set.new
225
+ serializers.each { |serializer| process_resource(serializer, true, @include_directive) }
226
+ serializers.each { |serializer| process_relationships(serializer, @include_directive) }
227
+
228
+ [@primary, @included]
229
+ end
230
+
231
+ def process_resource(serializer, primary, included_associations)
232
+ resource_identifier = ResourceIdentifier.new(serializer, instance_options).as_json
233
+ return false unless @resource_identifiers.add?(resource_identifier)
234
+
235
+ resource_object = resource_object_for(serializer, included_associations)
236
+ if primary
237
+ @primary << resource_object
238
+ else
239
+ @included << resource_object
240
+ end
241
+
242
+ true
243
+ end
244
+
245
+ def process_relationships(serializer, include_directive)
246
+ serializer.associations(include_directive).each do |association|
247
+ process_relationship(association.serializer, include_directive[association.key])
248
+ end
249
+ end
250
+
251
+ def process_relationship(serializer, include_directive)
252
+ if serializer.respond_to?(:each)
253
+ serializer.each { |s| process_relationship(s, include_directive) }
254
+ return
255
+ end
256
+ return unless serializer && serializer.object
257
+ return unless process_resource(serializer, false, include_directive)
258
+
259
+ process_relationships(serializer, include_directive)
260
+ end
261
+
262
+ # {http://jsonapi.org/format/#document-resource-object-attributes Document Resource Object Attributes}
263
+ # attributes
264
+ # definition:
265
+ # JSON Object
266
+ #
267
+ # patternProperties:
268
+ # ^(?!relationships$|links$)\\w[-\\w_]*$
269
+ #
270
+ # description:
271
+ # Members of the attributes object ("attributes") represent information about the resource
272
+ # object in which it's defined.
273
+ # Attributes may contain any valid JSON value
274
+ # structure:
275
+ # {
276
+ # foo: 'bar'
277
+ # }
278
+ def attributes_for(serializer, fields)
279
+ serializer.attributes(fields).except(:id)
280
+ end
281
+
282
+ # {http://jsonapi.org/format/#document-resource-objects Document Resource Objects}
283
+ def resource_object_for(serializer, included_associations)
284
+ resource_object = serializer.fetch(self) do
285
+ resource_object = ResourceIdentifier.new(serializer, instance_options).as_json
286
+
287
+ requested_fields = fieldset && fieldset.fields_for(resource_object[:type])
288
+ attributes = attributes_for(serializer, requested_fields)
289
+ resource_object[:attributes] = attributes if attributes.any?
290
+ resource_object
291
+ end
292
+
293
+ requested_associations = fieldset.fields_for(resource_object[:type]) || '*'
294
+ relationships = relationships_for(serializer, requested_associations, included_associations)
295
+ resource_object[:relationships] = relationships if relationships.any?
296
+
297
+ links = links_for(serializer)
298
+ # toplevel_links
299
+ # definition:
300
+ # allOf
301
+ # ☐ links
302
+ # ☐ pagination
303
+ #
304
+ # description:
305
+ # Link members related to the primary data.
306
+ # structure:
307
+ # links.merge!(pagination)
308
+ # prs:
309
+ # https://github.com/rails-api/active_model_serializers/pull/1247
310
+ # https://github.com/rails-api/active_model_serializers/pull/1018
311
+ resource_object[:links] = links if links.any?
312
+
313
+ # toplevel_meta
314
+ # alias meta
315
+ # definition:
316
+ # meta
317
+ # structure
318
+ # {
319
+ # :'git-ref' => 'abc123'
320
+ # }
321
+ meta = meta_for(serializer)
322
+ resource_object[:meta] = meta unless meta.blank?
323
+
324
+ resource_object
325
+ end
326
+
327
+ # {http://jsonapi.org/format/#document-resource-object-relationships Document Resource Object Relationship}
328
+ # relationships
329
+ # definition:
330
+ # JSON Object
331
+ #
332
+ # patternProperties:
333
+ # ^\\w[-\\w_]*$"
334
+ #
335
+ # properties:
336
+ # data : relationshipsData
337
+ # links
338
+ # meta
339
+ #
340
+ # description:
341
+ #
342
+ # Members of the relationships object ("relationships") represent references from the
343
+ # resource object in which it's defined to other resource objects."
344
+ # structure:
345
+ # {
346
+ # links: links,
347
+ # meta: meta,
348
+ # data: relationshipsData
349
+ # }.reject! {|_,v| v.nil? }
350
+ #
351
+ # prs:
352
+ # links
353
+ # [x] https://github.com/rails-api/active_model_serializers/pull/1454
354
+ # meta
355
+ # [x] https://github.com/rails-api/active_model_serializers/pull/1454
356
+ # polymorphic
357
+ # [ ] https://github.com/rails-api/active_model_serializers/pull/1420
358
+ #
359
+ # relationshipsData
360
+ # definition:
361
+ # oneOf
362
+ # relationshipToOne
363
+ # relationshipToMany
364
+ #
365
+ # description:
366
+ # Member, whose value represents "resource linkage"
367
+ # structure:
368
+ # if has_one?
369
+ # relationshipToOne
370
+ # else
371
+ # relationshipToMany
372
+ # end
373
+ #
374
+ # definition:
375
+ # anyOf
376
+ # null
377
+ # linkage
378
+ #
379
+ # relationshipToOne
380
+ # description:
381
+ #
382
+ # References to other resource objects in a to-one ("relationship"). Relationships can be
383
+ # specified by including a member in a resource's links object.
384
+ #
385
+ # None: Describes an empty to-one relationship.
386
+ # structure:
387
+ # if has_related?
388
+ # linkage
389
+ # else
390
+ # nil
391
+ # end
392
+ #
393
+ # relationshipToMany
394
+ # definition:
395
+ # array of unique items of type 'linkage'
396
+ #
397
+ # description:
398
+ # An array of objects each containing "type" and "id" members for to-many relationships
399
+ # structure:
400
+ # [
401
+ # linkage,
402
+ # linkage
403
+ # ]
404
+ # prs:
405
+ # polymorphic
406
+ # [ ] https://github.com/rails-api/active_model_serializers/pull/1282
407
+ #
408
+ # linkage
409
+ # definition:
410
+ # type (required) : String
411
+ # id (required) : String
412
+ # meta
413
+ #
414
+ # description:
415
+ # The "type" and "id" to non-empty members.
416
+ # structure:
417
+ # {
418
+ # type: 'required-type',
419
+ # id: 'required-id',
420
+ # meta: meta
421
+ # }.reject! {|_,v| v.nil? }
422
+ def relationships_for(serializer, requested_associations, included_associations)
423
+ include_directive = JSONAPI::IncludeDirective.new(
424
+ requested_associations,
425
+ allow_wildcard: true
426
+ )
427
+ serializer.associations(include_directive).each_with_object({}) do |association, hash|
428
+ hash[association.key] = Relationship.new(
429
+ serializer,
430
+ instance_options,
431
+ association,
432
+ included_associations
433
+ ).as_json
434
+ end
435
+ end
436
+
437
+ # {http://jsonapi.org/format/#document-links Document Links}
438
+ # links
439
+ # definition:
440
+ # JSON Object
441
+ #
442
+ # properties:
443
+ # self : URI
444
+ # related : link
445
+ #
446
+ # description:
447
+ # A resource object **MAY** contain references to other resource objects ("relationships").
448
+ # Relationships may be to-one or to-many. Relationships can be specified by including a member
449
+ # in a resource's links object.
450
+ #
451
+ # A `self` member’s value is a URL for the relationship itself (a "relationship URL"). This
452
+ # URL allows the client to directly manipulate the relationship. For example, it would allow
453
+ # a client to remove an `author` from an `article` without deleting the people resource
454
+ # itself.
455
+ # structure:
456
+ # {
457
+ # self: 'http://example.com/etc',
458
+ # related: link
459
+ # }.reject! {|_,v| v.nil? }
460
+ def links_for(serializer)
461
+ serializer._links.each_with_object({}) do |(name, value), hash|
462
+ result = Link.new(serializer, value).as_json
463
+ hash[name] = result if result
464
+ end
465
+ end
466
+
467
+ # {http://jsonapi.org/format/#fetching-pagination Pagination Links}
468
+ # pagination
469
+ # definition:
470
+ # first : pageObject
471
+ # last : pageObject
472
+ # prev : pageObject
473
+ # next : pageObject
474
+ # structure:
475
+ # {
476
+ # first: pageObject,
477
+ # last: pageObject,
478
+ # prev: pageObject,
479
+ # next: pageObject
480
+ # }
481
+ #
482
+ # pageObject
483
+ # definition:
484
+ # oneOf
485
+ # URI
486
+ # null
487
+ #
488
+ # description:
489
+ # The <x> page of data
490
+ # structure:
491
+ # if has_page?
492
+ # 'http://example.com/some-page?page[number][x]'
493
+ # else
494
+ # nil
495
+ # end
496
+ # prs:
497
+ # https://github.com/rails-api/active_model_serializers/pull/1041
498
+ def pagination_links_for(serializer)
499
+ PaginationLinks.new(serializer.object, instance_options).as_json
500
+ end
501
+
502
+ # {http://jsonapi.org/format/#document-meta Docment Meta}
503
+ def meta_for(serializer)
504
+ Meta.new(serializer).as_json
505
+ end
506
+ end
507
+ end
508
+ end
509
+ # rubocop:enable Style/AsciiComments
@@ -0,0 +1,69 @@
1
+ require 'active_support/concern'
2
+
3
+ module Caprese
4
+ module Versioning
5
+ extend ActiveSupport::Concern
6
+
7
+ # Get versioned module name of object (class)
8
+ #
9
+ # @example
10
+ # version_module('OrdersController') => 'API::V1::OrdersController'
11
+ #
12
+ # @param [String] suffix the string to append, hypothetically a module in
13
+ # this version's module space
14
+ # @return [String] the versioned modulized name of the suffix passed in
15
+ def version_module(suffix = nil)
16
+ name = (self.is_a?(Class) ? self : self.class).name.deconstantize
17
+
18
+ name + (suffix ? "::#{suffix}" : '')
19
+ end
20
+
21
+ # Get versioned path (url)
22
+ #
23
+ # @example
24
+ # version_path('orders') => 'api/v1/orders'
25
+ #
26
+ # @param [String] suffix the string to append, hypothetically an extension in
27
+ # this version's path space
28
+ # @return [String] the versioned path name of the suffix passed in
29
+ def version_path(suffix = nil)
30
+ version_module.downcase.split('::').push(suffix).reject(&:nil?).join('/')
31
+ end
32
+
33
+ # Get version dot path (translations)
34
+ #
35
+ # @example
36
+ # version_dot_path('orders') => 'api/v1/orders'
37
+ #
38
+ # @param [String] suffix the string to append, hypothetically an extension in
39
+ # this version's dot space
40
+ # @return [String] the versioned dot path of the suffix passed in
41
+ def version_dot_path(suffix = nil)
42
+ version_module.downcase.split('::').push(suffix).reject(&:nil?).join('.')
43
+ end
44
+
45
+ # Get versioned underscore name (routes)
46
+ #
47
+ # @example
48
+ # version_name('orders') => 'api_v1_orders'
49
+ #
50
+ # @param [String] suffix the string to append, hypothetically a name that
51
+ # needs to be versioned
52
+ # @return [String] the versioned name of the suffix passed in
53
+ def version_name(suffix = nil)
54
+ version_module.downcase.split('::').push(suffix).reject(&:nil?).join('_')
55
+ end
56
+
57
+ # Strips string to raw unversioned format regardless of input format
58
+ #
59
+ # @param [String] str the string to unversion
60
+ # @return [String] the stripped, unversioned name of the string passed in
61
+ def unversion(str)
62
+ str
63
+ .remove(version_module(''))
64
+ .remove(version_path(''))
65
+ .remove(version_name(''))
66
+ .remove(version_dot_path(''))
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,60 @@
1
+ require 'active_support/concern'
2
+
3
+ # Defines callbacks like `before_create`, `after_create` to be called in abstracted persistence methods
4
+ # like `create`, so there is no need to override `create`, which won't work properly because of the `render`
5
+ # call in it
6
+ module Caprese
7
+ module Callbacks
8
+ extend ActiveSupport::Concern
9
+
10
+ CALLBACKS = [
11
+ :before_query, :after_query,
12
+ :after_initialize,
13
+ :before_create, :after_create,
14
+ :before_update, :after_update,
15
+ :before_save, :after_save,
16
+ :before_destroy, :after_destroy
17
+ ]
18
+
19
+ included do
20
+ CALLBACKS.each do |method_name|
21
+ instance_variable_name = "@#{method_name}_callbacks"
22
+
23
+ # Defines instance method like `execute_before_create_callbacks` to be called with the resource
24
+ # being created passed in as an argument, to perform actions before saving the resource using
25
+ # callbacks defined at the class level
26
+ # @see below `define_single_method method_name`
27
+ #
28
+ # @note In the instance of `before_create`, the resource to be created will be passed in to callbacks,
29
+ # but in cases like `before_query`, nothing will be passed in, or `after_query`, the result of the
30
+ # query will be passed in
31
+ #
32
+ # @param [ActiveRecord::Base] resource the resource to perform actions on/with
33
+ define_method "execute_#{method_name}_callbacks" do |arg = nil|
34
+ self.class.instance_variable_get(instance_variable_name).try(:each) do |callback|
35
+ if self.class.instance_method(callback).arity > 0
36
+ send(callback, arg)
37
+ else
38
+ send(callback)
39
+ end
40
+ end
41
+ end
42
+
43
+ # Defines class method like `before_create` that takes in symbols that specify
44
+ # callbacks to call at a certain even like before creating a resource
45
+ #
46
+ # @example
47
+ # before_create :do_something, :do_another_thing
48
+ # after_create :do_more_things, :do_other_things
49
+ #
50
+ # @param [Symbol,Array<Symbol>] callbacks the name(s) of callbacks to add to list of callbacks
51
+ define_singleton_method method_name do |*callbacks|
52
+ unless (all_callbacks = self.instance_variable_get(instance_variable_name))
53
+ all_callbacks = self.instance_variable_set(instance_variable_name, [])
54
+ end
55
+ all_callbacks.push *callbacks
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end