jsonapi-resources 0.10.0.beta3 → 0.10.0.beta4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36f9ab797756ee5c8d323380fd3c2fe9a82e7c575006f00708161bc5c8eea016
4
- data.tar.gz: 5ba5e81dc2fd9ccdd57e0f03eccf9828b0ac8083d6ce581d4f644eca6e1064d6
3
+ metadata.gz: 9a67d9ec13b0f5c2ae85187c5a3511eebc1e74c0d94cd17109f09115af320aa8
4
+ data.tar.gz: 6526b529feaea2bdd37befe22b35bf994c469b31c884a85e20f1b67f7365c728
5
5
  SHA512:
6
- metadata.gz: fbb3e6edf76acdbb56211e105859198ad8319d551a027bc1a18e9cb824405b1bb0ce8e93666b3765068e29327806a81a9b068fadc141dfee4b188f7cb404f4da
7
- data.tar.gz: cf0a3f74a12c019479ed3e5f0bb79174c12bd0a28681c1a657fbf8c2c50caa610eba7ccce58694ae7e6016aca5af2dc61f28453e3a23abfdaf09a603162a8872
6
+ metadata.gz: 81b16e8293752c7082bd572a3c763ff5b710181860153226c035dca100e52bf4b49ea8c4fa7495418b3657b642455eb889dea88fd4646c95ec1cb88d0fc59756
7
+ data.tar.gz: 44784c1244cfe58a0151246f5076250f0a296ba69eb67d6250e700a227955ad7e006ed670f72700981668f659e462990033bf555ecc0e86b61e65deeca2351be
@@ -1,6 +1,8 @@
1
1
  require 'jsonapi/resources/railtie'
2
2
  require 'jsonapi/naive_cache'
3
3
  require 'jsonapi/compiled_json'
4
+ require 'jsonapi/basic_resource'
5
+ require 'jsonapi/active_relation_resource'
4
6
  require 'jsonapi/resource'
5
7
  require 'jsonapi/cached_response_fragment'
6
8
  require 'jsonapi/response_document'
@@ -25,8 +27,8 @@ require 'jsonapi/operation'
25
27
  require 'jsonapi/operation_result'
26
28
  require 'jsonapi/callbacks'
27
29
  require 'jsonapi/link_builder'
28
- require 'jsonapi/active_relation_resource_finder'
29
- require 'jsonapi/active_relation_resource_finder/join_manager'
30
+ require 'jsonapi/active_relation/adapters/join_left_active_record_adapter'
31
+ require 'jsonapi/active_relation/join_manager'
30
32
  require 'jsonapi/resource_identity'
31
33
  require 'jsonapi/resource_fragment'
32
34
  require 'jsonapi/resource_id_tree'
@@ -1,5 +1,5 @@
1
1
  module JSONAPI
2
- module ActiveRelationResourceFinder
2
+ module ActiveRelation
3
3
  module Adapters
4
4
  module JoinLeftActiveRecordAdapter
5
5
 
@@ -1,5 +1,5 @@
1
1
  module JSONAPI
2
- module ActiveRelationResourceFinder
2
+ module ActiveRelation
3
3
 
4
4
  # Stores relationship paths starting from the resource_klass, consolidating duplicate paths from
5
5
  # relationships, filters and sorts. When joins are made the table aliases are tracked in join_details
@@ -1,13 +1,8 @@
1
- require 'jsonapi/active_relation_resource_finder/adapters/join_left_active_record_adapter'
2
-
3
1
  module JSONAPI
4
- module ActiveRelationResourceFinder
5
- def self.included(base)
6
- base.extend ClassMethods
7
- end
8
-
9
- module ClassMethods
2
+ class ActiveRelationResource < BasicResource
3
+ root_resource
10
4
 
5
+ class << self
11
6
  # Finds Resources using the `filters`. Pagination and sort options are used when provided
12
7
  #
13
8
  # @param filters [Hash] the filters hash
@@ -19,9 +14,9 @@ module JSONAPI
19
14
  def find(filters, options = {})
20
15
  sort_criteria = options.fetch(:sort_criteria) { [] }
21
16
 
22
- join_manager = JoinManager.new(resource_klass: self,
23
- filters: filters,
24
- sort_criteria: sort_criteria)
17
+ join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
18
+ filters: filters,
19
+ sort_criteria: sort_criteria)
25
20
 
26
21
  paginator = options[:paginator]
27
22
 
@@ -41,8 +36,8 @@ module JSONAPI
41
36
  #
42
37
  # @return [Integer] the count
43
38
  def count(filters, options = {})
44
- join_manager = JoinManager.new(resource_klass: self,
45
- filters: filters)
39
+ join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
40
+ filters: filters)
46
41
 
47
42
  records = apply_request_settings_to_records(records: records(options),
48
43
  filters: filters,
@@ -70,6 +65,16 @@ module JSONAPI
70
65
  resources_for(records, options[:context])
71
66
  end
72
67
 
68
+ # Returns an array of Resources identified by the `keys` array. The resources are not filtered as this
69
+ # will have been done in a prior step
70
+ #
71
+ # @param keys [Array<key>] Array of primary keys to find resources for
72
+ # @option options [Hash] :context The context of the request, set in the controller
73
+ def find_to_populate_by_keys(keys, options = {})
74
+ records = records_for_populate(options).where(_primary_key => keys)
75
+ resources_for(records, options[:context])
76
+ end
77
+
73
78
  # Finds Resource fragments using the `filters`. Pagination and sort options are used when provided.
74
79
  # Retrieving the ResourceIdentities and attributes does not instantiate a model instance.
75
80
  # Note: This is incompatible with Polymorphic resources (which are going to come from two separate tables)
@@ -91,11 +96,11 @@ module JSONAPI
91
96
 
92
97
  sort_criteria = options.fetch(:sort_criteria) { [] }
93
98
 
94
- join_manager = JoinManager.new(resource_klass: resource_klass,
95
- source_relationship: nil,
96
- relationships: linkage_relationships,
97
- sort_criteria: sort_criteria,
98
- filters: filters)
99
+ join_manager = ActiveRelation::JoinManager.new(resource_klass: resource_klass,
100
+ source_relationship: nil,
101
+ relationships: linkage_relationships,
102
+ sort_criteria: sort_criteria,
103
+ filters: filters)
99
104
 
100
105
  paginator = options[:paginator]
101
106
 
@@ -149,7 +154,7 @@ module JSONAPI
149
154
  end
150
155
 
151
156
  fragments = {}
152
- rows = records.distinct.pluck(*pluck_fields)
157
+ rows = records.pluck(*pluck_fields)
153
158
  rows.collect do |row|
154
159
  rid = JSONAPI::ResourceIdentity.new(resource_klass, pluck_fields.length == 1 ? row : row[0])
155
160
 
@@ -224,9 +229,9 @@ module JSONAPI
224
229
  filters = options.fetch(:filters, {})
225
230
 
226
231
  # Joins in this case are related to the related_klass
227
- join_manager = JoinManager.new(resource_klass: self,
228
- source_relationship: relationship,
229
- filters: filters)
232
+ join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
233
+ source_relationship: relationship,
234
+ filters: filters)
230
235
 
231
236
  records = apply_request_settings_to_records(records: records(options),
232
237
  resource_klass: related_klass,
@@ -242,10 +247,57 @@ module JSONAPI
242
247
  count_records(records)
243
248
  end
244
249
 
245
- def records(_options = {})
250
+ # This resource class (ActiveRelationResource) uses an `ActiveRecord::Relation` as the starting point for
251
+ # retrieving models. From this relation filters, sorts and joins are applied as needed.
252
+ # Depending on which phase of the request processing different `records` methods will be called, giving the user
253
+ # the opportunity to override them differently for performance and security reasons.
254
+
255
+ # begin `records`methods
256
+
257
+ # Base for the `records` methods that follow and is not directly used for accessing model data by this class.
258
+ # Overriding this method gives a single place to affect the `ActiveRecord::Relation` used for the resource.
259
+ #
260
+ # @option options [Hash] :context The context of the request, set in the controller
261
+ #
262
+ # @return [ActiveRecord::Relation]
263
+ def records_base(_options = {})
246
264
  _model_class.all
247
265
  end
248
266
 
267
+ # The `ActiveRecord::Relation` used for finding user requested models. This may be overridden to enforce
268
+ # permissions checks on the request.
269
+ #
270
+ # @option options [Hash] :context The context of the request, set in the controller
271
+ #
272
+ # @return [ActiveRecord::Relation]
273
+ def records(options = {})
274
+ records_base(options)
275
+ end
276
+
277
+ # The `ActiveRecord::Relation` used for populating the ResourceSet. Only resources that have been previously
278
+ # identified through the `records` method will be accessed. Thus it should not be necessary to reapply permissions
279
+ # checks. However if the model needs to include other models adding `includes` is appropriate
280
+ #
281
+ # @option options [Hash] :context The context of the request, set in the controller
282
+ #
283
+ # @return [ActiveRecord::Relation]
284
+ def records_for_populate(options = {})
285
+ records_base(options)
286
+ end
287
+
288
+ # The `ActiveRecord::Relation` used for the finding related resources. Only resources that have been previously
289
+ # identified through the `records` method will be accessed and used as the basis to find related resources. Thus
290
+ # it should not be necessary to reapply permissions checks.
291
+ #
292
+ # @option options [Hash] :context The context of the request, set in the controller
293
+ #
294
+ # @return [ActiveRecord::Relation]
295
+ def records_for_source_to_related(options = {})
296
+ records_base(options)
297
+ end
298
+
299
+ # end `records` methods
300
+
249
301
  def apply_join(records:, relationship:, resource_type:, join_type:, options:)
250
302
  if relationship.polymorphic? && relationship.belongs_to?
251
303
  case join_type
@@ -267,7 +319,7 @@ module JSONAPI
267
319
  end
268
320
 
269
321
  def relationship_records(relationship:, join_type: :inner, resource_type: nil, options: {})
270
- records = relationship.parent_resource.records(options)
322
+ records = relationship.parent_resource.records_for_source_to_related(options)
271
323
  strategy = relationship.options[:apply_join]
272
324
 
273
325
  if strategy
@@ -328,15 +380,15 @@ module JSONAPI
328
380
  sort_criteria << { field: field, direction: sort[:direction] }
329
381
  end
330
382
 
331
- join_manager = JoinManager.new(resource_klass: self,
332
- source_relationship: relationship,
333
- relationships: linkage_relationships,
334
- sort_criteria: sort_criteria,
335
- filters: filters)
383
+ join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
384
+ source_relationship: relationship,
385
+ relationships: linkage_relationships,
386
+ sort_criteria: sort_criteria,
387
+ filters: filters)
336
388
 
337
389
  paginator = options[:paginator] if source_rids.count == 1
338
390
 
339
- records = apply_request_settings_to_records(records: records(options),
391
+ records = apply_request_settings_to_records(records: records_for_source_to_related(options),
340
392
  resource_klass: resource_klass,
341
393
  sort_criteria: sort_criteria,
342
394
  primary_keys: source_ids,
@@ -454,16 +506,16 @@ module JSONAPI
454
506
  end
455
507
  end
456
508
 
457
- join_manager = JoinManager.new(resource_klass: self,
458
- source_relationship: relationship,
459
- relationships: linkage_relationships,
460
- filters: filters)
509
+ join_manager = ActiveRelation::JoinManager.new(resource_klass: self,
510
+ source_relationship: relationship,
511
+ relationships: linkage_relationships,
512
+ filters: filters)
461
513
 
462
514
  paginator = options[:paginator] if source_rids.count == 1
463
515
 
464
516
  # Note: We will sort by the source table. Without using unions we can't sort on a polymorphic relationship
465
517
  # in any manner that makes sense
466
- records = apply_request_settings_to_records(records: records(options),
518
+ records = apply_request_settings_to_records(records: records_for_source_to_related(options),
467
519
  resource_klass: resource_klass,
468
520
  sort_primary: true,
469
521
  primary_keys: source_ids,
@@ -615,14 +667,14 @@ module JSONAPI
615
667
  end
616
668
 
617
669
  def apply_request_settings_to_records(records:,
618
- join_manager: JoinManager.new(resource_klass: self),
619
- resource_klass: self,
620
- filters: {},
621
- primary_keys: nil,
622
- sort_criteria: nil,
623
- sort_primary: nil,
624
- paginator: nil,
625
- options: {})
670
+ join_manager: ActiveRelation::JoinManager.new(resource_klass: self),
671
+ resource_klass: self,
672
+ filters: {},
673
+ primary_keys: nil,
674
+ sort_criteria: nil,
675
+ sort_primary: nil,
676
+ paginator: nil,
677
+ options: {})
626
678
 
627
679
  opts = options.dup
628
680
  records = resource_klass.apply_joins(records, join_manager, opts)
@@ -777,4 +829,4 @@ module JSONAPI
777
829
  end
778
830
  end
779
831
  end
780
- end
832
+ end
@@ -70,7 +70,7 @@ module JSONAPI
70
70
  def get_related_resources
71
71
  # :nocov:
72
72
  ActiveSupport::Deprecation.warn "In #{self.class.name} you exposed a `get_related_resources`"\
73
- " action. Please use `index_related_resource` instead."
73
+ " action. Please use `index_related_resources` instead."
74
74
  index_related_resources
75
75
  # :nocov:
76
76
  end
@@ -0,0 +1,1137 @@
1
+ require 'jsonapi/callbacks'
2
+ require 'jsonapi/configuration'
3
+
4
+ module JSONAPI
5
+ class BasicResource
6
+ include Callbacks
7
+
8
+ @abstract = true
9
+ @immutable = true
10
+ @root = true
11
+
12
+ attr_reader :context
13
+
14
+ define_jsonapi_resources_callbacks :create,
15
+ :update,
16
+ :remove,
17
+ :save,
18
+ :create_to_many_link,
19
+ :replace_to_many_links,
20
+ :create_to_one_link,
21
+ :replace_to_one_link,
22
+ :replace_polymorphic_to_one_link,
23
+ :remove_to_many_link,
24
+ :remove_to_one_link,
25
+ :replace_fields
26
+
27
+ def initialize(model, context)
28
+ @model = model
29
+ @context = context
30
+ @reload_needed = false
31
+ @changing = false
32
+ @save_needed = false
33
+ end
34
+
35
+ def _model
36
+ @model
37
+ end
38
+
39
+ def id
40
+ _model.public_send(self.class._primary_key)
41
+ end
42
+
43
+ def identity
44
+ JSONAPI::ResourceIdentity.new(self.class, id)
45
+ end
46
+
47
+ def cache_id
48
+ [id, self.class.hash_cache_field(_model.public_send(self.class._cache_field))]
49
+ end
50
+
51
+ def is_new?
52
+ id.nil?
53
+ end
54
+
55
+ def change(callback)
56
+ completed = false
57
+
58
+ if @changing
59
+ run_callbacks callback do
60
+ completed = (yield == :completed)
61
+ end
62
+ else
63
+ run_callbacks is_new? ? :create : :update do
64
+ @changing = true
65
+ run_callbacks callback do
66
+ completed = (yield == :completed)
67
+ end
68
+
69
+ completed = (save == :completed) if @save_needed || is_new?
70
+ end
71
+ end
72
+
73
+ return completed ? :completed : :accepted
74
+ end
75
+
76
+ def remove
77
+ run_callbacks :remove do
78
+ _remove
79
+ end
80
+ end
81
+
82
+ def create_to_many_links(relationship_type, relationship_key_values, options = {})
83
+ change :create_to_many_link do
84
+ _create_to_many_links(relationship_type, relationship_key_values, options)
85
+ end
86
+ end
87
+
88
+ def replace_to_many_links(relationship_type, relationship_key_values, options = {})
89
+ change :replace_to_many_links do
90
+ _replace_to_many_links(relationship_type, relationship_key_values, options)
91
+ end
92
+ end
93
+
94
+ def replace_to_one_link(relationship_type, relationship_key_value, options = {})
95
+ change :replace_to_one_link do
96
+ _replace_to_one_link(relationship_type, relationship_key_value, options)
97
+ end
98
+ end
99
+
100
+ def replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type, options = {})
101
+ change :replace_polymorphic_to_one_link do
102
+ _replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type, options)
103
+ end
104
+ end
105
+
106
+ def remove_to_many_link(relationship_type, key, options = {})
107
+ change :remove_to_many_link do
108
+ _remove_to_many_link(relationship_type, key, options)
109
+ end
110
+ end
111
+
112
+ def remove_to_one_link(relationship_type, options = {})
113
+ change :remove_to_one_link do
114
+ _remove_to_one_link(relationship_type, options)
115
+ end
116
+ end
117
+
118
+ def replace_fields(field_data)
119
+ change :replace_fields do
120
+ _replace_fields(field_data)
121
+ end
122
+ end
123
+
124
+ # Override this on a resource instance to override the fetchable keys
125
+ def fetchable_fields
126
+ self.class.fields
127
+ end
128
+
129
+ def model_error_messages
130
+ _model.errors.messages
131
+ end
132
+
133
+ # Add metadata to validation error objects.
134
+ #
135
+ # Suppose `model_error_messages` returned the following error messages
136
+ # hash:
137
+ #
138
+ # {password: ["too_short", "format"]}
139
+ #
140
+ # Then to add data to the validation error `validation_error_metadata`
141
+ # could return:
142
+ #
143
+ # {
144
+ # password: {
145
+ # "too_short": {"minimum_length" => 6},
146
+ # "format": {"requirement" => "must contain letters and numbers"}
147
+ # }
148
+ # }
149
+ #
150
+ # The specified metadata is then be merged into the validation error
151
+ # object.
152
+ def validation_error_metadata
153
+ {}
154
+ end
155
+
156
+ # Override this to return resource level meta data
157
+ # must return a hash, and if the hash is empty the meta section will not be serialized with the resource
158
+ # meta keys will be not be formatted with the key formatter for the serializer by default. They can however use the
159
+ # serializer's format_key and format_value methods if desired
160
+ # the _options hash will contain the serializer and the serialization_options
161
+ def meta(_options)
162
+ {}
163
+ end
164
+
165
+ # Override this to return custom links
166
+ # must return a hash, which will be merged with the default { self: 'self-url' } links hash
167
+ # links keys will be not be formatted with the key formatter for the serializer by default.
168
+ # They can however use the serializer's format_key and format_value methods if desired
169
+ # the _options hash will contain the serializer and the serialization_options
170
+ def custom_links(_options)
171
+ {}
172
+ end
173
+
174
+ private
175
+
176
+ def save
177
+ run_callbacks :save do
178
+ _save
179
+ end
180
+ end
181
+
182
+ # Override this on a resource to return a different result code. Any
183
+ # value other than :completed will result in operations returning
184
+ # `:accepted`
185
+ #
186
+ # For example to return `:accepted` if your model does not immediately
187
+ # save resources to the database you could override `_save` as follows:
188
+ #
189
+ # ```
190
+ # def _save
191
+ # super
192
+ # return :accepted
193
+ # end
194
+ # ```
195
+ def _save(validation_context = nil)
196
+ unless @model.valid?(validation_context)
197
+ fail JSONAPI::Exceptions::ValidationErrors.new(self)
198
+ end
199
+
200
+ if defined? @model.save
201
+ saved = @model.save(validate: false)
202
+
203
+ unless saved
204
+ if @model.errors.present?
205
+ fail JSONAPI::Exceptions::ValidationErrors.new(self)
206
+ else
207
+ fail JSONAPI::Exceptions::SaveFailed.new
208
+ end
209
+ end
210
+ else
211
+ saved = true
212
+ end
213
+ @model.reload if @reload_needed
214
+ @reload_needed = false
215
+
216
+ @save_needed = !saved
217
+
218
+ :completed
219
+ end
220
+
221
+ def _remove
222
+ unless @model.destroy
223
+ fail JSONAPI::Exceptions::ValidationErrors.new(self)
224
+ end
225
+ :completed
226
+
227
+ rescue ActiveRecord::DeleteRestrictionError => e
228
+ fail JSONAPI::Exceptions::RecordLocked.new(e.message)
229
+ end
230
+
231
+ def reflect_relationship?(relationship, options)
232
+ return false if !relationship.reflect ||
233
+ (!JSONAPI.configuration.use_relationship_reflection || options[:reflected_source])
234
+
235
+ inverse_relationship = relationship.resource_klass._relationships[relationship.inverse_relationship]
236
+ if inverse_relationship.nil?
237
+ warn "Inverse relationship could not be found for #{self.class.name}.#{relationship.name}. Relationship reflection disabled."
238
+ return false
239
+ end
240
+ true
241
+ end
242
+
243
+ def _create_to_many_links(relationship_type, relationship_key_values, options)
244
+ relationship = self.class._relationships[relationship_type]
245
+ relation_name = relationship.relation_name(context: @context)
246
+
247
+ if options[:reflected_source]
248
+ @model.public_send(relation_name) << options[:reflected_source]._model
249
+ return :completed
250
+ end
251
+
252
+ # load requested related resources
253
+ # make sure they all exist (also based on context) and add them to relationship
254
+
255
+ related_resources = relationship.resource_klass.find_by_keys(relationship_key_values, context: @context)
256
+
257
+ if related_resources.count != relationship_key_values.count
258
+ # todo: obscure id so not to leak info
259
+ fail JSONAPI::Exceptions::RecordNotFound.new('unspecified')
260
+ end
261
+
262
+ reflect = reflect_relationship?(relationship, options)
263
+
264
+ related_resources.each do |related_resource|
265
+ if reflect
266
+ if related_resource.class._relationships[relationship.inverse_relationship].is_a?(JSONAPI::Relationship::ToMany)
267
+ related_resource.create_to_many_links(relationship.inverse_relationship, [id], reflected_source: self)
268
+ else
269
+ related_resource.replace_to_one_link(relationship.inverse_relationship, id, reflected_source: self)
270
+ end
271
+ @reload_needed = true
272
+ else
273
+ unless @model.public_send(relation_name).include?(related_resource._model)
274
+ @model.public_send(relation_name) << related_resource._model
275
+ end
276
+ end
277
+ end
278
+
279
+ :completed
280
+ end
281
+
282
+ def _replace_to_many_links(relationship_type, relationship_key_values, options)
283
+ relationship = self.class._relationship(relationship_type)
284
+
285
+ reflect = reflect_relationship?(relationship, options)
286
+
287
+ if reflect
288
+ existing_rids = self.class.find_related_fragments([identity], relationship_type, options)
289
+
290
+ existing = existing_rids.keys.collect { |rid| rid.id }
291
+
292
+ to_delete = existing - (relationship_key_values & existing)
293
+ to_delete.each do |key|
294
+ _remove_to_many_link(relationship_type, key, reflected_source: self)
295
+ end
296
+
297
+ to_add = relationship_key_values - (relationship_key_values & existing)
298
+ _create_to_many_links(relationship_type, to_add, {})
299
+
300
+ @reload_needed = true
301
+ elsif relationship.polymorphic?
302
+ relationship_key_values.each do |relationship_key_value|
303
+ relationship_resource_klass = self.class.resource_klass_for(relationship_key_value[:type])
304
+ ids = relationship_key_value[:ids]
305
+
306
+ related_records = relationship_resource_klass
307
+ .records(options)
308
+ .where({relationship_resource_klass._primary_key => ids})
309
+
310
+ missed_ids = ids - related_records.pluck(relationship_resource_klass._primary_key)
311
+
312
+ if missed_ids.present?
313
+ fail JSONAPI::Exceptions::RecordNotFound.new(missed_ids)
314
+ end
315
+
316
+ relation_name = relationship.relation_name(context: @context)
317
+ @model.send("#{relation_name}") << related_records
318
+ end
319
+
320
+ @reload_needed = true
321
+ else
322
+ send("#{relationship.foreign_key}=", relationship_key_values)
323
+ @save_needed = true
324
+ end
325
+
326
+ :completed
327
+ end
328
+
329
+ def _replace_to_one_link(relationship_type, relationship_key_value, _options)
330
+ relationship = self.class._relationships[relationship_type]
331
+
332
+ send("#{relationship.foreign_key}=", relationship_key_value)
333
+ @save_needed = true
334
+
335
+ :completed
336
+ end
337
+
338
+ def _replace_polymorphic_to_one_link(relationship_type, key_value, key_type, _options)
339
+ relationship = self.class._relationships[relationship_type.to_sym]
340
+
341
+ send("#{relationship.foreign_key}=", {type: key_type, id: key_value})
342
+ @save_needed = true
343
+
344
+ :completed
345
+ end
346
+
347
+ def _remove_to_many_link(relationship_type, key, options)
348
+ relationship = self.class._relationships[relationship_type]
349
+
350
+ reflect = reflect_relationship?(relationship, options)
351
+
352
+ if reflect
353
+
354
+ related_resource = relationship.resource_klass.find_by_key(key, context: @context)
355
+
356
+ if related_resource.nil?
357
+ fail JSONAPI::Exceptions::RecordNotFound.new(key)
358
+ else
359
+ if related_resource.class._relationships[relationship.inverse_relationship].is_a?(JSONAPI::Relationship::ToMany)
360
+ related_resource.remove_to_many_link(relationship.inverse_relationship, id, reflected_source: self)
361
+ else
362
+ related_resource.remove_to_one_link(relationship.inverse_relationship, reflected_source: self)
363
+ end
364
+ end
365
+
366
+ @reload_needed = true
367
+ else
368
+ @model.public_send(relationship.relation_name(context: @context)).destroy(key)
369
+ end
370
+
371
+ :completed
372
+
373
+ rescue ActiveRecord::DeleteRestrictionError => e
374
+ fail JSONAPI::Exceptions::RecordLocked.new(e.message)
375
+ rescue ActiveRecord::RecordNotFound
376
+ fail JSONAPI::Exceptions::RecordNotFound.new(key)
377
+ end
378
+
379
+ def _remove_to_one_link(relationship_type, _options)
380
+ relationship = self.class._relationships[relationship_type]
381
+
382
+ send("#{relationship.foreign_key}=", nil)
383
+ @save_needed = true
384
+
385
+ :completed
386
+ end
387
+
388
+ def _replace_fields(field_data)
389
+ field_data[:attributes].each do |attribute, value|
390
+ begin
391
+ send "#{attribute}=", value
392
+ @save_needed = true
393
+ rescue ArgumentError
394
+ # :nocov: Will be thrown if an enum value isn't allowed for an enum. Currently not tested as enums are a rails 4.1 and higher feature
395
+ raise JSONAPI::Exceptions::InvalidFieldValue.new(attribute, value)
396
+ # :nocov:
397
+ end
398
+ end
399
+
400
+ field_data[:to_one].each do |relationship_type, value|
401
+ if value.nil?
402
+ remove_to_one_link(relationship_type)
403
+ else
404
+ case value
405
+ when Hash
406
+ replace_polymorphic_to_one_link(relationship_type.to_s, value.fetch(:id), value.fetch(:type))
407
+ else
408
+ replace_to_one_link(relationship_type, value)
409
+ end
410
+ end
411
+ end if field_data[:to_one]
412
+
413
+ field_data[:to_many].each do |relationship_type, values|
414
+ replace_to_many_links(relationship_type, values)
415
+ end if field_data[:to_many]
416
+
417
+ :completed
418
+ end
419
+
420
+ class << self
421
+ def inherited(subclass)
422
+ subclass.abstract(false)
423
+ subclass.immutable(false)
424
+ subclass.caching(_caching)
425
+ subclass.singleton(singleton?, (_singleton_options.dup || {}))
426
+ subclass.exclude_links(_exclude_links)
427
+ subclass.paginator(_paginator)
428
+ subclass._attributes = (_attributes || {}).dup
429
+ subclass.polymorphic(false)
430
+
431
+ subclass._model_hints = (_model_hints || {}).dup
432
+
433
+ unless _model_name.empty? || _immutable
434
+ subclass.model_name(_model_name, add_model_hint: (_model_hints && !_model_hints[_model_name].nil?) == true)
435
+ end
436
+
437
+ subclass.rebuild_relationships(_relationships || {})
438
+
439
+ subclass._allowed_filters = (_allowed_filters || Set.new).dup
440
+
441
+ subclass._allowed_sort = _allowed_sort.dup
442
+
443
+ type = subclass.name.demodulize.sub(/Resource$/, '').underscore
444
+ subclass._type = type.pluralize.to_sym
445
+
446
+ unless subclass._attributes[:id]
447
+ subclass.attribute :id, format: :id, readonly: true
448
+ end
449
+
450
+ check_reserved_resource_name(subclass._type, subclass.name)
451
+ end
452
+
453
+ def rebuild_relationships(relationships)
454
+ original_relationships = relationships.deep_dup
455
+
456
+ @_relationships = {}
457
+
458
+ if original_relationships.is_a?(Hash)
459
+ original_relationships.each_value do |relationship|
460
+ options = relationship.options.dup
461
+ options[:parent_resource] = self
462
+ options[:inverse_relationship] = relationship.inverse_relationship
463
+ _add_relationship(relationship.class, relationship.name, options)
464
+ end
465
+ end
466
+ end
467
+
468
+ def resource_klass_for(type)
469
+ type = type.underscore
470
+ type_with_module = type.start_with?(module_path) ? type : module_path + type
471
+
472
+ resource_name = _resource_name_from_type(type_with_module)
473
+ resource = resource_name.safe_constantize if resource_name
474
+ if resource.nil?
475
+ fail NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)"
476
+ end
477
+ resource
478
+ end
479
+
480
+ def resource_klass_for_model(model)
481
+ resource_klass_for(resource_type_for(model))
482
+ end
483
+
484
+ def _resource_name_from_type(type)
485
+ "#{type.to_s.underscore.singularize}_resource".camelize
486
+ end
487
+
488
+ def resource_type_for(model)
489
+ model_name = model.class.to_s.underscore
490
+ if _model_hints[model_name]
491
+ _model_hints[model_name]
492
+ else
493
+ model_name.rpartition('/').last
494
+ end
495
+ end
496
+
497
+ attr_accessor :_attributes, :_relationships, :_type, :_model_hints
498
+ attr_writer :_allowed_filters, :_paginator, :_allowed_sort
499
+
500
+ def create(context)
501
+ new(create_model, context)
502
+ end
503
+
504
+ def create_model
505
+ _model_class.new
506
+ end
507
+
508
+ def routing_options(options)
509
+ @_routing_resource_options = options
510
+ end
511
+
512
+ def routing_resource_options
513
+ @_routing_resource_options ||= {}
514
+ end
515
+
516
+ # Methods used in defining a resource class
517
+ def attributes(*attrs)
518
+ options = attrs.extract_options!.dup
519
+ attrs.each do |attr|
520
+ attribute(attr, options)
521
+ end
522
+ end
523
+
524
+ def attribute(attribute_name, options = {})
525
+ attr = attribute_name.to_sym
526
+
527
+ check_reserved_attribute_name(attr)
528
+
529
+ if (attr == :id) && (options[:format].nil?)
530
+ ActiveSupport::Deprecation.warn('Id without format is no longer supported. Please remove ids from attributes, or specify a format.')
531
+ end
532
+
533
+ check_duplicate_attribute_name(attr) if options[:format].nil?
534
+
535
+ @_attributes ||= {}
536
+ @_attributes[attr] = options
537
+ define_method attr do
538
+ @model.public_send(options[:delegate] ? options[:delegate].to_sym : attr)
539
+ end unless method_defined?(attr)
540
+
541
+ define_method "#{attr}=" do |value|
542
+ @model.public_send("#{options[:delegate] ? options[:delegate].to_sym : attr}=", value)
543
+ end unless method_defined?("#{attr}=")
544
+
545
+ if options.fetch(:sortable, true) && !_has_sort?(attr)
546
+ sort attr
547
+ end
548
+ end
549
+
550
+ def attribute_to_model_field(attribute)
551
+ field_name = if attribute == :_cache_field
552
+ _cache_field
553
+ else
554
+ # Note: this will allow the returning of model attributes without a corresponding
555
+ # resource attribute, for example a belongs_to id such as `author_id` or bypassing
556
+ # the delegate.
557
+ attr = @_attributes[attribute]
558
+ attr && attr[:delegate] ? attr[:delegate].to_sym : attribute
559
+ end
560
+ if Rails::VERSION::MAJOR >= 5
561
+ attribute_type = _model_class.attribute_types[field_name.to_s]
562
+ else
563
+ attribute_type = _model_class.column_types[field_name.to_s]
564
+ end
565
+ { name: field_name, type: attribute_type}
566
+ end
567
+
568
+ def cast_to_attribute_type(value, type)
569
+ if Rails::VERSION::MAJOR >= 5
570
+ return type.cast(value)
571
+ else
572
+ return type.type_cast_from_database(value)
573
+ end
574
+ end
575
+
576
+ def default_attribute_options
577
+ { format: :default }
578
+ end
579
+
580
+ def relationship(*attrs)
581
+ options = attrs.extract_options!
582
+ klass = case options[:to]
583
+ when :one
584
+ Relationship::ToOne
585
+ when :many
586
+ Relationship::ToMany
587
+ else
588
+ #:nocov:#
589
+ fail ArgumentError.new('to: must be either :one or :many')
590
+ #:nocov:#
591
+ end
592
+ _add_relationship(klass, *attrs, options.except(:to))
593
+ end
594
+
595
+ def has_one(*attrs)
596
+ _add_relationship(Relationship::ToOne, *attrs)
597
+ end
598
+
599
+ def belongs_to(*attrs)
600
+ ActiveSupport::Deprecation.warn "In #{name} you exposed a `has_one` relationship "\
601
+ " using the `belongs_to` class method. We think `has_one`" \
602
+ " is more appropriate. If you know what you're doing," \
603
+ " and don't want to see this warning again, override the" \
604
+ " `belongs_to` class method on your resource."
605
+ _add_relationship(Relationship::ToOne, *attrs)
606
+ end
607
+
608
+ def has_many(*attrs)
609
+ _add_relationship(Relationship::ToMany, *attrs)
610
+ end
611
+
612
+ # @model_class is inherited from superclass, and this causes some issues:
613
+ # ```
614
+ # CarResource._model_class #=> Vehicle # it should be Car
615
+ # ```
616
+ # so in order to invoke the right class from subclasses,
617
+ # we should call this method to override it.
618
+ def model_name(model, options = {})
619
+ @model_class = nil
620
+ @_model_name = model.to_sym
621
+
622
+ model_hint(model: @_model_name, resource: self) unless options[:add_model_hint] == false
623
+
624
+ rebuild_relationships(_relationships)
625
+ end
626
+
627
+ def model_hint(model: _model_name, resource: _type)
628
+ resource_type = ((resource.is_a?(Class)) && (resource < JSONAPI::Resource)) ? resource._type : resource.to_s
629
+
630
+ _model_hints[model.to_s.gsub('::', '/').underscore] = resource_type.to_s
631
+ end
632
+
633
+ def singleton(*attrs)
634
+ @_singleton = (!!attrs[0] == attrs[0]) ? attrs[0] : true
635
+ @_singleton_options = attrs.extract_options!
636
+ end
637
+
638
+ def _singleton_options
639
+ @_singleton_options ||= {}
640
+ end
641
+
642
+ def singleton?
643
+ @_singleton ||= false
644
+ end
645
+
646
+ def filters(*attrs)
647
+ @_allowed_filters.merge!(attrs.inject({}) { |h, attr| h[attr] = {}; h })
648
+ end
649
+
650
+ def filter(attr, *args)
651
+ @_allowed_filters[attr.to_sym] = args.extract_options!
652
+ end
653
+
654
+ def sort(sorting, options = {})
655
+ self._allowed_sort[sorting.to_sym] = options
656
+ end
657
+
658
+ def sorts(*args)
659
+ options = args.extract_options!
660
+ _allowed_sort.merge!(args.inject({}) { |h, sorting| h[sorting.to_sym] = options.dup; h })
661
+ end
662
+
663
+ def primary_key(key)
664
+ @_primary_key = key.to_sym
665
+ end
666
+
667
+ def cache_field(field)
668
+ @_cache_field = field.to_sym
669
+ end
670
+
671
+ # Override in your resource to filter the updatable keys
672
+ def updatable_fields(_context = nil)
673
+ _updatable_relationships | _updatable_attributes - [:id]
674
+ end
675
+
676
+ # Override in your resource to filter the creatable keys
677
+ def creatable_fields(_context = nil)
678
+ _updatable_relationships | _updatable_attributes
679
+ end
680
+
681
+ # Override in your resource to filter the sortable keys
682
+ def sortable_fields(_context = nil)
683
+ _allowed_sort.keys
684
+ end
685
+
686
+ def sortable_field?(key, context = nil)
687
+ sortable_fields(context).include? key.to_sym
688
+ end
689
+
690
+ def fields
691
+ _relationships.keys | _attributes.keys
692
+ end
693
+
694
+ def resources_for(records, context)
695
+ records.collect do |record|
696
+ resource_for(record, context)
697
+ end
698
+ end
699
+
700
+ def resource_for(model_record, context)
701
+ resource_klass = self.resource_klass_for_model(model_record)
702
+ resource_klass.new(model_record, context)
703
+ end
704
+
705
+ def verify_filters(filters, context = nil)
706
+ verified_filters = {}
707
+ filters.each do |filter, raw_value|
708
+ verified_filter = verify_filter(filter, raw_value, context)
709
+ verified_filters[verified_filter[0]] = verified_filter[1]
710
+ end
711
+ verified_filters
712
+ end
713
+
714
+ def is_filter_relationship?(filter)
715
+ filter == _type || _relationships.include?(filter)
716
+ end
717
+
718
+ def verify_filter(filter, raw, context = nil)
719
+ filter_values = []
720
+ if raw.present?
721
+ begin
722
+ filter_values += raw.is_a?(String) ? CSV.parse_line(raw) : [raw]
723
+ rescue CSV::MalformedCSVError
724
+ filter_values << raw
725
+ end
726
+ end
727
+
728
+ strategy = _allowed_filters.fetch(filter, Hash.new)[:verify]
729
+
730
+ if strategy
731
+ values = call_method_or_proc(strategy, filter_values, context)
732
+ [filter, values]
733
+ else
734
+ if is_filter_relationship?(filter)
735
+ verify_relationship_filter(filter, filter_values, context)
736
+ else
737
+ verify_custom_filter(filter, filter_values, context)
738
+ end
739
+ end
740
+ end
741
+
742
+ def call_method_or_proc(strategy, *args)
743
+ if strategy.is_a?(Symbol) || strategy.is_a?(String)
744
+ send(strategy, *args)
745
+ else
746
+ strategy.call(*args)
747
+ end
748
+ end
749
+
750
+ def key_type(key_type)
751
+ @_resource_key_type = key_type
752
+ end
753
+
754
+ def resource_key_type
755
+ @_resource_key_type ||= JSONAPI.configuration.resource_key_type
756
+ end
757
+
758
+ # override to all resolution of masked ids to actual ids. Because singleton routes do not specify the id this
759
+ # will be needed to allow lookup of singleton resources. Alternately singleton resources can override
760
+ # `verify_key`
761
+ def singleton_key(context)
762
+ if @_singleton_options && @_singleton_options[:singleton_key]
763
+ strategy = @_singleton_options[:singleton_key]
764
+ case strategy
765
+ when Proc
766
+ key = strategy.call(context)
767
+ when Symbol, String
768
+ key = send(strategy, context)
769
+ else
770
+ raise "singleton_key must be a proc or function name"
771
+ end
772
+ end
773
+ key
774
+ end
775
+
776
+ def verify_key(key, context = nil)
777
+ key_type = resource_key_type
778
+
779
+ case key_type
780
+ when :integer
781
+ return if key.nil?
782
+ Integer(key)
783
+ when :string
784
+ return if key.nil?
785
+ if key.to_s.include?(',')
786
+ raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
787
+ else
788
+ key
789
+ end
790
+ when :uuid
791
+ return if key.nil?
792
+ if key.to_s.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)
793
+ key
794
+ else
795
+ raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
796
+ end
797
+ else
798
+ key_type.call(key, context)
799
+ end
800
+ rescue
801
+ raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
802
+ end
803
+
804
+ # override to allow for key processing and checking
805
+ def verify_keys(keys, context = nil)
806
+ return keys.collect do |key|
807
+ verify_key(key, context)
808
+ end
809
+ end
810
+
811
+ # Either add a custom :verify lambda or override verify_custom_filter to allow for custom filters
812
+ def verify_custom_filter(filter, value, _context = nil)
813
+ [filter, value]
814
+ end
815
+
816
+ # Either add a custom :verify lambda or override verify_relationship_filter to allow for custom
817
+ # relationship logic, such as uuids, multiple keys or permission checks on keys
818
+ def verify_relationship_filter(filter, raw, _context = nil)
819
+ [filter, raw]
820
+ end
821
+
822
+ # quasi private class methods
823
+ def _attribute_options(attr)
824
+ default_attribute_options.merge(@_attributes[attr])
825
+ end
826
+
827
+ def _attribute_delegated_name(attr)
828
+ @_attributes.fetch(attr.to_sym, {}).fetch(:delegate, attr)
829
+ end
830
+
831
+ def _has_attribute?(attr)
832
+ @_attributes.keys.include?(attr.to_sym)
833
+ end
834
+
835
+ def _updatable_attributes
836
+ _attributes.map { |key, options| key unless options[:readonly] }.compact
837
+ end
838
+
839
+ def _updatable_relationships
840
+ @_relationships.map { |key, relationship| key unless relationship.readonly? }.compact
841
+ end
842
+
843
+ def _relationship(type)
844
+ return nil unless type
845
+ type = type.to_sym
846
+ @_relationships[type]
847
+ end
848
+
849
+ def _model_name
850
+ if _abstract
851
+ ''
852
+ else
853
+ return @_model_name.to_s if defined?(@_model_name)
854
+ class_name = self.name
855
+ return '' if class_name.nil?
856
+ @_model_name = class_name.demodulize.sub(/Resource$/, '')
857
+ @_model_name.to_s
858
+ end
859
+ end
860
+
861
+ def _polymorphic_name
862
+ if !_polymorphic
863
+ ''
864
+ else
865
+ @_polymorphic_name ||= _model_name.to_s.downcase
866
+ end
867
+ end
868
+
869
+ def _primary_key
870
+ @_primary_key ||= _default_primary_key
871
+ end
872
+
873
+ def _default_primary_key
874
+ @_default_primary_key ||=_model_class.respond_to?(:primary_key) ? _model_class.primary_key : :id
875
+ end
876
+
877
+ def _cache_field
878
+ @_cache_field ||= JSONAPI.configuration.default_resource_cache_field
879
+ end
880
+
881
+ def _table_name
882
+ @_table_name ||= _model_class.respond_to?(:table_name) ? _model_class.table_name : _model_name.tableize
883
+ end
884
+
885
+ def _as_parent_key
886
+ @_as_parent_key ||= "#{_type.to_s.singularize}_id"
887
+ end
888
+
889
+ def _allowed_filters
890
+ defined?(@_allowed_filters) ? @_allowed_filters : { id: {} }
891
+ end
892
+
893
+ def _allowed_sort
894
+ @_allowed_sort ||= {}
895
+ end
896
+
897
+ def _paginator
898
+ @_paginator ||= JSONAPI.configuration.default_paginator
899
+ end
900
+
901
+ def paginator(paginator)
902
+ @_paginator = paginator
903
+ end
904
+
905
+ def _polymorphic
906
+ @_polymorphic
907
+ end
908
+
909
+ def polymorphic(polymorphic = true)
910
+ @_polymorphic = polymorphic
911
+ end
912
+
913
+ def _polymorphic_types
914
+ @poly_hash ||= {}.tap do |hash|
915
+ ObjectSpace.each_object do |klass|
916
+ next unless Module === klass
917
+ if klass < ActiveRecord::Base
918
+ klass.reflect_on_all_associations(:has_many).select{|r| r.options[:as] }.each do |reflection|
919
+ (hash[reflection.options[:as]] ||= []) << klass.name.downcase
920
+ end
921
+ end
922
+ end
923
+ end
924
+ @poly_hash[_polymorphic_name.to_sym]
925
+ end
926
+
927
+ def _polymorphic_resource_klasses
928
+ @_polymorphic_resource_klasses ||= _polymorphic_types.collect do |type|
929
+ resource_klass_for(type)
930
+ end
931
+ end
932
+
933
+ def root_resource
934
+ @abstract = true
935
+ @immutable = true
936
+ @root = true
937
+ end
938
+
939
+ def root?
940
+ @root
941
+ end
942
+
943
+ def abstract(val = true)
944
+ @abstract = val
945
+ end
946
+
947
+ def _abstract
948
+ @abstract
949
+ end
950
+
951
+ def immutable(val = true)
952
+ @immutable = val
953
+ end
954
+
955
+ def _immutable
956
+ @immutable
957
+ end
958
+
959
+ def mutable?
960
+ !@immutable
961
+ end
962
+
963
+ def exclude_links(exclude)
964
+ case exclude
965
+ when :default, "default"
966
+ @_exclude_links = [:self]
967
+ when :none, "none"
968
+ @_exclude_links = []
969
+ when Array
970
+ @_exclude_links = exclude.collect {|link| link.to_sym}
971
+ else
972
+ fail "Invalid exclude_links"
973
+ end
974
+ end
975
+
976
+ def _exclude_links
977
+ @_exclude_links ||= []
978
+ end
979
+
980
+ def exclude_link?(link)
981
+ _exclude_links.include?(link.to_sym)
982
+ end
983
+
984
+ def caching(val = true)
985
+ @caching = val
986
+ end
987
+
988
+ def _caching
989
+ @caching
990
+ end
991
+
992
+ def caching?
993
+ if @caching.nil?
994
+ !JSONAPI.configuration.resource_cache.nil? && JSONAPI.configuration.default_caching
995
+ else
996
+ @caching && !JSONAPI.configuration.resource_cache.nil?
997
+ end
998
+ end
999
+
1000
+ def attribute_caching_context(_context)
1001
+ nil
1002
+ end
1003
+
1004
+ # Generate a hashcode from the value to be used as part of the cache lookup
1005
+ def hash_cache_field(value)
1006
+ value.hash
1007
+ end
1008
+
1009
+ def _model_class
1010
+ return nil if _abstract
1011
+
1012
+ return @model_class if @model_class
1013
+
1014
+ model_name = _model_name
1015
+ return nil if model_name.to_s.blank?
1016
+
1017
+ @model_class = model_name.to_s.safe_constantize
1018
+ if @model_class.nil?
1019
+ warn "[MODEL NOT FOUND] Model could not be found for #{self.name}. If this is a base Resource declare it as abstract."
1020
+ end
1021
+
1022
+ @model_class
1023
+ end
1024
+
1025
+ def _allowed_filter?(filter)
1026
+ !_allowed_filters[filter].nil?
1027
+ end
1028
+
1029
+ def _has_sort?(sorting)
1030
+ !_allowed_sort[sorting.to_sym].nil?
1031
+ end
1032
+
1033
+ def module_path
1034
+ if name == 'JSONAPI::Resource'
1035
+ ''
1036
+ else
1037
+ name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').underscore : ''
1038
+ end
1039
+ end
1040
+
1041
+ def default_sort
1042
+ [{field: 'id', direction: :asc}]
1043
+ end
1044
+
1045
+ def construct_order_options(sort_params)
1046
+ sort_params ||= default_sort
1047
+
1048
+ return {} unless sort_params
1049
+
1050
+ sort_params.each_with_object({}) do |sort, order_hash|
1051
+ field = sort[:field].to_s == 'id' ? _primary_key : sort[:field].to_s
1052
+ order_hash[field] = sort[:direction]
1053
+ end
1054
+ end
1055
+
1056
+ def _add_relationship(klass, *attrs)
1057
+ options = attrs.extract_options!
1058
+ options[:parent_resource] = self
1059
+
1060
+ attrs.each do |name|
1061
+ relationship_name = name.to_sym
1062
+ check_reserved_relationship_name(relationship_name)
1063
+ check_duplicate_relationship_name(relationship_name)
1064
+
1065
+ define_relationship_methods(relationship_name.to_sym, klass, options)
1066
+ end
1067
+ end
1068
+
1069
+ # ResourceBuilder methods
1070
+ def define_relationship_methods(relationship_name, relationship_klass, options)
1071
+ relationship = register_relationship(
1072
+ relationship_name,
1073
+ relationship_klass.new(relationship_name, options)
1074
+ )
1075
+
1076
+ define_foreign_key_setter(relationship)
1077
+ end
1078
+
1079
+ def define_foreign_key_setter(relationship)
1080
+ if relationship.polymorphic?
1081
+ define_on_resource "#{relationship.foreign_key}=" do |v|
1082
+ _model.method("#{relationship.foreign_key}=").call(v[:id])
1083
+ _model.public_send("#{relationship.polymorphic_type}=", v[:type])
1084
+ end
1085
+ else
1086
+ define_on_resource "#{relationship.foreign_key}=" do |value|
1087
+ _model.method("#{relationship.foreign_key}=").call(value)
1088
+ end
1089
+ end
1090
+ end
1091
+
1092
+ def define_on_resource(method_name, &block)
1093
+ return if method_defined?(method_name)
1094
+ define_method(method_name, block)
1095
+ end
1096
+
1097
+ def register_relationship(name, relationship_object)
1098
+ @_relationships[name] = relationship_object
1099
+ end
1100
+
1101
+ private
1102
+
1103
+ def check_reserved_resource_name(type, name)
1104
+ if [:ids, :types, :hrefs, :links].include?(type)
1105
+ warn "[NAME COLLISION] `#{name}` is a reserved resource name."
1106
+ return
1107
+ end
1108
+ end
1109
+
1110
+ def check_reserved_attribute_name(name)
1111
+ # Allow :id since it can be used to specify the format. Since it is a method on the base Resource
1112
+ # an attribute method won't be created for it.
1113
+ if [:type, :_cache_field, :cache_field].include?(name.to_sym)
1114
+ warn "[NAME COLLISION] `#{name}` is a reserved key in #{_resource_name_from_type(_type)}."
1115
+ end
1116
+ end
1117
+
1118
+ def check_reserved_relationship_name(name)
1119
+ if [:id, :ids, :type, :types].include?(name.to_sym)
1120
+ warn "[NAME COLLISION] `#{name}` is a reserved relationship name in #{_resource_name_from_type(_type)}."
1121
+ end
1122
+ end
1123
+
1124
+ def check_duplicate_relationship_name(name)
1125
+ if _relationships.include?(name.to_sym)
1126
+ warn "[DUPLICATE RELATIONSHIP] `#{name}` has already been defined in #{_resource_name_from_type(_type)}."
1127
+ end
1128
+ end
1129
+
1130
+ def check_duplicate_attribute_name(name)
1131
+ if _attributes.include?(name.to_sym)
1132
+ warn "[DUPLICATE ATTRIBUTE] `#{name}` has already been defined in #{_resource_name_from_type(_type)}."
1133
+ end
1134
+ end
1135
+ end
1136
+ end
1137
+ end