caprese 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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