jsonapi-resources 0.10.0.beta3 → 0.10.0.beta4

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