jsonapi-resources 0.9.0 → 0.10.6
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 +5 -5
- data/LICENSE.txt +1 -1
- data/README.md +34 -11
- data/lib/bug_report_templates/rails_5_latest.rb +125 -0
- data/lib/bug_report_templates/rails_5_master.rb +140 -0
- data/lib/jsonapi/active_relation/adapters/join_left_active_record_adapter.rb +27 -0
- data/lib/jsonapi/active_relation/join_manager.rb +303 -0
- data/lib/jsonapi/active_relation_resource.rb +884 -0
- data/lib/jsonapi/acts_as_resource_controller.rb +122 -105
- data/lib/jsonapi/basic_resource.rb +1162 -0
- data/lib/jsonapi/cached_response_fragment.rb +127 -0
- data/lib/jsonapi/compiled_json.rb +11 -1
- data/lib/jsonapi/configuration.rb +71 -8
- data/lib/jsonapi/error.rb +27 -0
- data/lib/jsonapi/error_codes.rb +2 -0
- data/lib/jsonapi/exceptions.rb +80 -50
- data/lib/jsonapi/formatter.rb +3 -3
- data/lib/jsonapi/include_directives.rb +18 -65
- data/lib/jsonapi/link_builder.rb +74 -80
- data/lib/jsonapi/operation.rb +16 -5
- data/lib/jsonapi/operation_result.rb +74 -16
- data/lib/jsonapi/path.rb +43 -0
- data/lib/jsonapi/path_segment.rb +76 -0
- data/lib/jsonapi/processor.rb +239 -111
- data/lib/jsonapi/relationship.rb +153 -15
- data/lib/jsonapi/request_parser.rb +430 -367
- data/lib/jsonapi/resource.rb +3 -1253
- data/lib/jsonapi/resource_controller_metal.rb +5 -2
- data/lib/jsonapi/resource_fragment.rb +47 -0
- data/lib/jsonapi/resource_id_tree.rb +112 -0
- data/lib/jsonapi/resource_identity.rb +42 -0
- data/lib/jsonapi/resource_serializer.rb +143 -285
- data/lib/jsonapi/resource_set.rb +176 -0
- data/lib/jsonapi/resources/railtie.rb +9 -0
- data/lib/jsonapi/resources/version.rb +1 -1
- data/lib/jsonapi/response_document.rb +105 -83
- data/lib/jsonapi/routing_ext.rb +48 -26
- data/lib/jsonapi-resources.rb +20 -4
- data/lib/tasks/check_upgrade.rake +52 -0
- metadata +50 -20
- data/lib/jsonapi/cached_resource_fragment.rb +0 -127
- data/lib/jsonapi/operation_dispatcher.rb +0 -88
- data/lib/jsonapi/operation_results.rb +0 -35
- data/lib/jsonapi/relationship_builder.rb +0 -167
@@ -0,0 +1,1162 @@
|
|
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)).delete(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.cache_field(_cache_field) if @_cache_field
|
426
|
+
subclass.singleton(singleton?, (_singleton_options.dup || {}))
|
427
|
+
subclass.exclude_links(_exclude_links)
|
428
|
+
subclass.paginator(@_paginator)
|
429
|
+
subclass._attributes = (_attributes || {}).dup
|
430
|
+
subclass.polymorphic(false)
|
431
|
+
subclass.key_type(@_resource_key_type)
|
432
|
+
|
433
|
+
subclass._model_hints = (_model_hints || {}).dup
|
434
|
+
|
435
|
+
unless _model_name.empty? || _immutable
|
436
|
+
subclass.model_name(_model_name, add_model_hint: (_model_hints && !_model_hints[_model_name].nil?) == true)
|
437
|
+
end
|
438
|
+
|
439
|
+
subclass.rebuild_relationships(_relationships || {})
|
440
|
+
|
441
|
+
subclass._allowed_filters = (_allowed_filters || Set.new).dup
|
442
|
+
|
443
|
+
subclass._allowed_sort = _allowed_sort.dup
|
444
|
+
|
445
|
+
type = subclass.name.demodulize.sub(/Resource$/, '').underscore
|
446
|
+
subclass._type = type.pluralize.to_sym
|
447
|
+
|
448
|
+
unless subclass._attributes[:id]
|
449
|
+
subclass.attribute :id, format: :id, readonly: true
|
450
|
+
end
|
451
|
+
|
452
|
+
check_reserved_resource_name(subclass._type, subclass.name)
|
453
|
+
|
454
|
+
subclass._routed = false
|
455
|
+
subclass._warned_missing_route = false
|
456
|
+
|
457
|
+
subclass._clear_cached_attribute_options
|
458
|
+
subclass._clear_fields_cache
|
459
|
+
end
|
460
|
+
|
461
|
+
def rebuild_relationships(relationships)
|
462
|
+
original_relationships = relationships.deep_dup
|
463
|
+
|
464
|
+
@_relationships = {}
|
465
|
+
|
466
|
+
if original_relationships.is_a?(Hash)
|
467
|
+
original_relationships.each_value do |relationship|
|
468
|
+
options = relationship.options.dup
|
469
|
+
options[:parent_resource] = self
|
470
|
+
options[:inverse_relationship] = relationship.inverse_relationship
|
471
|
+
_add_relationship(relationship.class, relationship.name, options)
|
472
|
+
end
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
def resource_klass_for(type)
|
477
|
+
type = type.underscore
|
478
|
+
type_with_module = type.start_with?(module_path) ? type : module_path + type
|
479
|
+
|
480
|
+
resource_name = _resource_name_from_type(type_with_module)
|
481
|
+
resource = resource_name.safe_constantize if resource_name
|
482
|
+
if resource.nil?
|
483
|
+
fail NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)"
|
484
|
+
end
|
485
|
+
resource
|
486
|
+
end
|
487
|
+
|
488
|
+
def resource_klass_for_model(model)
|
489
|
+
resource_klass_for(resource_type_for(model))
|
490
|
+
end
|
491
|
+
|
492
|
+
def _resource_name_from_type(type)
|
493
|
+
"#{type.to_s.underscore.singularize}_resource".camelize
|
494
|
+
end
|
495
|
+
|
496
|
+
def resource_type_for(model)
|
497
|
+
model_name = model.class.to_s.underscore
|
498
|
+
if _model_hints[model_name]
|
499
|
+
_model_hints[model_name]
|
500
|
+
else
|
501
|
+
model_name.rpartition('/').last
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
attr_accessor :_attributes, :_relationships, :_type, :_model_hints, :_routed, :_warned_missing_route
|
506
|
+
attr_writer :_allowed_filters, :_paginator, :_allowed_sort
|
507
|
+
|
508
|
+
def create(context)
|
509
|
+
new(create_model, context)
|
510
|
+
end
|
511
|
+
|
512
|
+
def create_model
|
513
|
+
_model_class.new
|
514
|
+
end
|
515
|
+
|
516
|
+
def routing_options(options)
|
517
|
+
@_routing_resource_options = options
|
518
|
+
end
|
519
|
+
|
520
|
+
def routing_resource_options
|
521
|
+
@_routing_resource_options ||= {}
|
522
|
+
end
|
523
|
+
|
524
|
+
# Methods used in defining a resource class
|
525
|
+
def attributes(*attrs)
|
526
|
+
options = attrs.extract_options!.dup
|
527
|
+
attrs.each do |attr|
|
528
|
+
attribute(attr, options)
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
def attribute(attribute_name, options = {})
|
533
|
+
_clear_cached_attribute_options
|
534
|
+
_clear_fields_cache
|
535
|
+
|
536
|
+
attr = attribute_name.to_sym
|
537
|
+
|
538
|
+
check_reserved_attribute_name(attr)
|
539
|
+
|
540
|
+
if (attr == :id) && (options[:format].nil?)
|
541
|
+
ActiveSupport::Deprecation.warn('Id without format is no longer supported. Please remove ids from attributes, or specify a format.')
|
542
|
+
end
|
543
|
+
|
544
|
+
check_duplicate_attribute_name(attr) if options[:format].nil?
|
545
|
+
|
546
|
+
@_attributes ||= {}
|
547
|
+
@_attributes[attr] = options
|
548
|
+
define_method attr do
|
549
|
+
@model.public_send(options[:delegate] ? options[:delegate].to_sym : attr)
|
550
|
+
end unless method_defined?(attr)
|
551
|
+
|
552
|
+
define_method "#{attr}=" do |value|
|
553
|
+
@model.public_send("#{options[:delegate] ? options[:delegate].to_sym : attr}=", value)
|
554
|
+
end unless method_defined?("#{attr}=")
|
555
|
+
|
556
|
+
if options.fetch(:sortable, true) && !_has_sort?(attr)
|
557
|
+
sort attr
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
def attribute_to_model_field(attribute)
|
562
|
+
field_name = if attribute == :_cache_field
|
563
|
+
_cache_field
|
564
|
+
else
|
565
|
+
# Note: this will allow the returning of model attributes without a corresponding
|
566
|
+
# resource attribute, for example a belongs_to id such as `author_id` or bypassing
|
567
|
+
# the delegate.
|
568
|
+
attr = @_attributes[attribute]
|
569
|
+
attr && attr[:delegate] ? attr[:delegate].to_sym : attribute
|
570
|
+
end
|
571
|
+
if Rails::VERSION::MAJOR >= 5
|
572
|
+
attribute_type = _model_class.attribute_types[field_name.to_s]
|
573
|
+
else
|
574
|
+
attribute_type = _model_class.column_types[field_name.to_s]
|
575
|
+
end
|
576
|
+
{ name: field_name, type: attribute_type}
|
577
|
+
end
|
578
|
+
|
579
|
+
def cast_to_attribute_type(value, type)
|
580
|
+
if Rails::VERSION::MAJOR >= 5
|
581
|
+
return type.cast(value)
|
582
|
+
else
|
583
|
+
return type.type_cast_from_database(value)
|
584
|
+
end
|
585
|
+
end
|
586
|
+
|
587
|
+
def default_attribute_options
|
588
|
+
{ format: :default }
|
589
|
+
end
|
590
|
+
|
591
|
+
def relationship(*attrs)
|
592
|
+
options = attrs.extract_options!
|
593
|
+
klass = case options[:to]
|
594
|
+
when :one
|
595
|
+
Relationship::ToOne
|
596
|
+
when :many
|
597
|
+
Relationship::ToMany
|
598
|
+
else
|
599
|
+
#:nocov:#
|
600
|
+
fail ArgumentError.new('to: must be either :one or :many')
|
601
|
+
#:nocov:#
|
602
|
+
end
|
603
|
+
_add_relationship(klass, *attrs, options.except(:to))
|
604
|
+
end
|
605
|
+
|
606
|
+
def has_one(*attrs)
|
607
|
+
_add_relationship(Relationship::ToOne, *attrs)
|
608
|
+
end
|
609
|
+
|
610
|
+
def belongs_to(*attrs)
|
611
|
+
ActiveSupport::Deprecation.warn "In #{name} you exposed a `has_one` relationship "\
|
612
|
+
" using the `belongs_to` class method. We think `has_one`" \
|
613
|
+
" is more appropriate. If you know what you're doing," \
|
614
|
+
" and don't want to see this warning again, override the" \
|
615
|
+
" `belongs_to` class method on your resource."
|
616
|
+
_add_relationship(Relationship::ToOne, *attrs)
|
617
|
+
end
|
618
|
+
|
619
|
+
def has_many(*attrs)
|
620
|
+
_add_relationship(Relationship::ToMany, *attrs)
|
621
|
+
end
|
622
|
+
|
623
|
+
# @model_class is inherited from superclass, and this causes some issues:
|
624
|
+
# ```
|
625
|
+
# CarResource._model_class #=> Vehicle # it should be Car
|
626
|
+
# ```
|
627
|
+
# so in order to invoke the right class from subclasses,
|
628
|
+
# we should call this method to override it.
|
629
|
+
def model_name(model, options = {})
|
630
|
+
@model_class = nil
|
631
|
+
@_model_name = model.to_sym
|
632
|
+
|
633
|
+
model_hint(model: @_model_name, resource: self) unless options[:add_model_hint] == false
|
634
|
+
|
635
|
+
rebuild_relationships(_relationships)
|
636
|
+
end
|
637
|
+
|
638
|
+
def model_hint(model: _model_name, resource: _type)
|
639
|
+
resource_type = ((resource.is_a?(Class)) && (resource < JSONAPI::Resource)) ? resource._type : resource.to_s
|
640
|
+
|
641
|
+
_model_hints[model.to_s.gsub('::', '/').underscore] = resource_type.to_s
|
642
|
+
end
|
643
|
+
|
644
|
+
def singleton(*attrs)
|
645
|
+
@_singleton = (!!attrs[0] == attrs[0]) ? attrs[0] : true
|
646
|
+
@_singleton_options = attrs.extract_options!
|
647
|
+
end
|
648
|
+
|
649
|
+
def _singleton_options
|
650
|
+
@_singleton_options ||= {}
|
651
|
+
end
|
652
|
+
|
653
|
+
def singleton?
|
654
|
+
@_singleton ||= false
|
655
|
+
end
|
656
|
+
|
657
|
+
def filters(*attrs)
|
658
|
+
@_allowed_filters.merge!(attrs.inject({}) { |h, attr| h[attr] = {}; h })
|
659
|
+
end
|
660
|
+
|
661
|
+
def filter(attr, *args)
|
662
|
+
@_allowed_filters[attr.to_sym] = args.extract_options!
|
663
|
+
end
|
664
|
+
|
665
|
+
def sort(sorting, options = {})
|
666
|
+
self._allowed_sort[sorting.to_sym] = options
|
667
|
+
end
|
668
|
+
|
669
|
+
def sorts(*args)
|
670
|
+
options = args.extract_options!
|
671
|
+
_allowed_sort.merge!(args.inject({}) { |h, sorting| h[sorting.to_sym] = options.dup; h })
|
672
|
+
end
|
673
|
+
|
674
|
+
def primary_key(key)
|
675
|
+
@_primary_key = key.to_sym
|
676
|
+
end
|
677
|
+
|
678
|
+
def cache_field(field)
|
679
|
+
@_cache_field = field.to_sym
|
680
|
+
end
|
681
|
+
|
682
|
+
# Override in your resource to filter the updatable keys
|
683
|
+
def updatable_fields(_context = nil)
|
684
|
+
_updatable_relationships | _updatable_attributes - [:id]
|
685
|
+
end
|
686
|
+
|
687
|
+
# Override in your resource to filter the creatable keys
|
688
|
+
def creatable_fields(_context = nil)
|
689
|
+
_updatable_relationships | _updatable_attributes
|
690
|
+
end
|
691
|
+
|
692
|
+
# Override in your resource to filter the sortable keys
|
693
|
+
def sortable_fields(_context = nil)
|
694
|
+
_allowed_sort.keys
|
695
|
+
end
|
696
|
+
|
697
|
+
def sortable_field?(key, context = nil)
|
698
|
+
sortable_fields(context).include? key.to_sym
|
699
|
+
end
|
700
|
+
|
701
|
+
def fields
|
702
|
+
@_fields_cache ||= _relationships.keys | _attributes.keys
|
703
|
+
end
|
704
|
+
|
705
|
+
def resources_for(records, context)
|
706
|
+
records.collect do |record|
|
707
|
+
resource_for(record, context)
|
708
|
+
end
|
709
|
+
end
|
710
|
+
|
711
|
+
def resource_for(model_record, context)
|
712
|
+
resource_klass = self.resource_klass_for_model(model_record)
|
713
|
+
resource_klass.new(model_record, context)
|
714
|
+
end
|
715
|
+
|
716
|
+
def verify_filters(filters, context = nil)
|
717
|
+
verified_filters = {}
|
718
|
+
filters.each do |filter, raw_value|
|
719
|
+
verified_filter = verify_filter(filter, raw_value, context)
|
720
|
+
verified_filters[verified_filter[0]] = verified_filter[1]
|
721
|
+
end
|
722
|
+
verified_filters
|
723
|
+
end
|
724
|
+
|
725
|
+
def is_filter_relationship?(filter)
|
726
|
+
filter == _type || _relationships.include?(filter)
|
727
|
+
end
|
728
|
+
|
729
|
+
def verify_filter(filter, raw, context = nil)
|
730
|
+
filter_values = []
|
731
|
+
if raw.present?
|
732
|
+
begin
|
733
|
+
filter_values += raw.is_a?(String) ? CSV.parse_line(raw) : [raw]
|
734
|
+
rescue CSV::MalformedCSVError
|
735
|
+
filter_values << raw
|
736
|
+
end
|
737
|
+
end
|
738
|
+
|
739
|
+
strategy = _allowed_filters.fetch(filter, Hash.new)[:verify]
|
740
|
+
|
741
|
+
if strategy
|
742
|
+
values = call_method_or_proc(strategy, filter_values, context)
|
743
|
+
[filter, values]
|
744
|
+
else
|
745
|
+
if is_filter_relationship?(filter)
|
746
|
+
verify_relationship_filter(filter, filter_values, context)
|
747
|
+
else
|
748
|
+
verify_custom_filter(filter, filter_values, context)
|
749
|
+
end
|
750
|
+
end
|
751
|
+
end
|
752
|
+
|
753
|
+
def call_method_or_proc(strategy, *args)
|
754
|
+
if strategy.is_a?(Symbol) || strategy.is_a?(String)
|
755
|
+
send(strategy, *args)
|
756
|
+
else
|
757
|
+
strategy.call(*args)
|
758
|
+
end
|
759
|
+
end
|
760
|
+
|
761
|
+
def key_type(key_type)
|
762
|
+
@_resource_key_type = key_type
|
763
|
+
end
|
764
|
+
|
765
|
+
def resource_key_type
|
766
|
+
@_resource_key_type || JSONAPI.configuration.resource_key_type
|
767
|
+
end
|
768
|
+
|
769
|
+
# override to all resolution of masked ids to actual ids. Because singleton routes do not specify the id this
|
770
|
+
# will be needed to allow lookup of singleton resources. Alternately singleton resources can override
|
771
|
+
# `verify_key`
|
772
|
+
def singleton_key(context)
|
773
|
+
if @_singleton_options && @_singleton_options[:singleton_key]
|
774
|
+
strategy = @_singleton_options[:singleton_key]
|
775
|
+
case strategy
|
776
|
+
when Proc
|
777
|
+
key = strategy.call(context)
|
778
|
+
when Symbol, String
|
779
|
+
key = send(strategy, context)
|
780
|
+
else
|
781
|
+
raise "singleton_key must be a proc or function name"
|
782
|
+
end
|
783
|
+
end
|
784
|
+
key
|
785
|
+
end
|
786
|
+
|
787
|
+
def verify_key(key, context = nil)
|
788
|
+
key_type = resource_key_type
|
789
|
+
|
790
|
+
case key_type
|
791
|
+
when :integer
|
792
|
+
return if key.nil?
|
793
|
+
Integer(key)
|
794
|
+
when :string
|
795
|
+
return if key.nil?
|
796
|
+
if key.to_s.include?(',')
|
797
|
+
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
|
798
|
+
else
|
799
|
+
key
|
800
|
+
end
|
801
|
+
when :uuid
|
802
|
+
return if key.nil?
|
803
|
+
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}$/)
|
804
|
+
key
|
805
|
+
else
|
806
|
+
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
|
807
|
+
end
|
808
|
+
else
|
809
|
+
key_type.call(key, context)
|
810
|
+
end
|
811
|
+
rescue
|
812
|
+
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
|
813
|
+
end
|
814
|
+
|
815
|
+
# override to allow for key processing and checking
|
816
|
+
def verify_keys(keys, context = nil)
|
817
|
+
return keys.collect do |key|
|
818
|
+
verify_key(key, context)
|
819
|
+
end
|
820
|
+
end
|
821
|
+
|
822
|
+
# Either add a custom :verify lambda or override verify_custom_filter to allow for custom filters
|
823
|
+
def verify_custom_filter(filter, value, _context = nil)
|
824
|
+
[filter, value]
|
825
|
+
end
|
826
|
+
|
827
|
+
# Either add a custom :verify lambda or override verify_relationship_filter to allow for custom
|
828
|
+
# relationship logic, such as uuids, multiple keys or permission checks on keys
|
829
|
+
def verify_relationship_filter(filter, raw, _context = nil)
|
830
|
+
[filter, raw]
|
831
|
+
end
|
832
|
+
|
833
|
+
# quasi private class methods
|
834
|
+
def _attribute_options(attr)
|
835
|
+
@_cached_attribute_options[attr] ||= default_attribute_options.merge(@_attributes[attr])
|
836
|
+
end
|
837
|
+
|
838
|
+
def _attribute_delegated_name(attr)
|
839
|
+
@_attributes.fetch(attr.to_sym, {}).fetch(:delegate, attr)
|
840
|
+
end
|
841
|
+
|
842
|
+
def _has_attribute?(attr)
|
843
|
+
@_attributes.keys.include?(attr.to_sym)
|
844
|
+
end
|
845
|
+
|
846
|
+
def _updatable_attributes
|
847
|
+
_attributes.map { |key, options| key unless options[:readonly] }.compact
|
848
|
+
end
|
849
|
+
|
850
|
+
def _updatable_relationships
|
851
|
+
@_relationships.map { |key, relationship| key unless relationship.readonly? }.compact
|
852
|
+
end
|
853
|
+
|
854
|
+
def _relationship(type)
|
855
|
+
return nil unless type
|
856
|
+
type = type.to_sym
|
857
|
+
@_relationships[type]
|
858
|
+
end
|
859
|
+
|
860
|
+
def _model_name
|
861
|
+
if _abstract
|
862
|
+
''
|
863
|
+
else
|
864
|
+
return @_model_name.to_s if defined?(@_model_name)
|
865
|
+
class_name = self.name
|
866
|
+
return '' if class_name.nil?
|
867
|
+
@_model_name = class_name.demodulize.sub(/Resource$/, '')
|
868
|
+
@_model_name.to_s
|
869
|
+
end
|
870
|
+
end
|
871
|
+
|
872
|
+
def _polymorphic_name
|
873
|
+
if !_polymorphic
|
874
|
+
''
|
875
|
+
else
|
876
|
+
@_polymorphic_name ||= _model_name.to_s.underscore
|
877
|
+
end
|
878
|
+
end
|
879
|
+
|
880
|
+
def _primary_key
|
881
|
+
@_primary_key ||= _default_primary_key
|
882
|
+
end
|
883
|
+
|
884
|
+
def _default_primary_key
|
885
|
+
@_default_primary_key ||=_model_class.respond_to?(:primary_key) ? _model_class.primary_key : :id
|
886
|
+
end
|
887
|
+
|
888
|
+
def _cache_field
|
889
|
+
@_cache_field || JSONAPI.configuration.default_resource_cache_field
|
890
|
+
end
|
891
|
+
|
892
|
+
def _table_name
|
893
|
+
@_table_name ||= _model_class.respond_to?(:table_name) ? _model_class.table_name : _model_name.tableize
|
894
|
+
end
|
895
|
+
|
896
|
+
def _as_parent_key
|
897
|
+
@_as_parent_key ||= "#{_type.to_s.singularize}_id"
|
898
|
+
end
|
899
|
+
|
900
|
+
def _allowed_filters
|
901
|
+
defined?(@_allowed_filters) ? @_allowed_filters : { id: {} }
|
902
|
+
end
|
903
|
+
|
904
|
+
def _allowed_sort
|
905
|
+
@_allowed_sort ||= {}
|
906
|
+
end
|
907
|
+
|
908
|
+
def _paginator
|
909
|
+
@_paginator || JSONAPI.configuration.default_paginator
|
910
|
+
end
|
911
|
+
|
912
|
+
def paginator(paginator)
|
913
|
+
@_paginator = paginator
|
914
|
+
end
|
915
|
+
|
916
|
+
def _polymorphic
|
917
|
+
@_polymorphic
|
918
|
+
end
|
919
|
+
|
920
|
+
def polymorphic(polymorphic = true)
|
921
|
+
@_polymorphic = polymorphic
|
922
|
+
end
|
923
|
+
|
924
|
+
def _polymorphic_types
|
925
|
+
@poly_hash ||= {}.tap do |hash|
|
926
|
+
ObjectSpace.each_object do |klass|
|
927
|
+
next unless Module === klass
|
928
|
+
if klass < ActiveRecord::Base
|
929
|
+
klass.reflect_on_all_associations(:has_many).select{|r| r.options[:as] }.each do |reflection|
|
930
|
+
(hash[reflection.options[:as]] ||= []) << klass.name.underscore
|
931
|
+
end
|
932
|
+
end
|
933
|
+
end
|
934
|
+
end
|
935
|
+
@poly_hash[_polymorphic_name.to_sym]
|
936
|
+
end
|
937
|
+
|
938
|
+
def _polymorphic_resource_klasses
|
939
|
+
@_polymorphic_resource_klasses ||= _polymorphic_types.collect do |type|
|
940
|
+
resource_klass_for(type)
|
941
|
+
end
|
942
|
+
end
|
943
|
+
|
944
|
+
def root_resource
|
945
|
+
@abstract = true
|
946
|
+
@immutable = true
|
947
|
+
@root = true
|
948
|
+
end
|
949
|
+
|
950
|
+
def root?
|
951
|
+
@root
|
952
|
+
end
|
953
|
+
|
954
|
+
def abstract(val = true)
|
955
|
+
@abstract = val
|
956
|
+
end
|
957
|
+
|
958
|
+
def _abstract
|
959
|
+
@abstract
|
960
|
+
end
|
961
|
+
|
962
|
+
def immutable(val = true)
|
963
|
+
@immutable = val
|
964
|
+
end
|
965
|
+
|
966
|
+
def _immutable
|
967
|
+
@immutable
|
968
|
+
end
|
969
|
+
|
970
|
+
def mutable?
|
971
|
+
!@immutable
|
972
|
+
end
|
973
|
+
|
974
|
+
def parse_exclude_links(exclude)
|
975
|
+
case exclude
|
976
|
+
when :default, "default"
|
977
|
+
[:self]
|
978
|
+
when :none, "none"
|
979
|
+
[]
|
980
|
+
when Array
|
981
|
+
exclude.collect {|link| link.to_sym}
|
982
|
+
else
|
983
|
+
fail "Invalid exclude_links"
|
984
|
+
end
|
985
|
+
end
|
986
|
+
|
987
|
+
def exclude_links(exclude)
|
988
|
+
@_exclude_links = parse_exclude_links(exclude)
|
989
|
+
end
|
990
|
+
|
991
|
+
def _exclude_links
|
992
|
+
@_exclude_links ||= parse_exclude_links(JSONAPI.configuration.default_exclude_links)
|
993
|
+
end
|
994
|
+
|
995
|
+
def exclude_link?(link)
|
996
|
+
_exclude_links.include?(link.to_sym)
|
997
|
+
end
|
998
|
+
|
999
|
+
def caching(val = true)
|
1000
|
+
@caching = val
|
1001
|
+
end
|
1002
|
+
|
1003
|
+
def _caching
|
1004
|
+
@caching
|
1005
|
+
end
|
1006
|
+
|
1007
|
+
def caching?
|
1008
|
+
if @caching.nil?
|
1009
|
+
!JSONAPI.configuration.resource_cache.nil? && JSONAPI.configuration.default_caching
|
1010
|
+
else
|
1011
|
+
@caching && !JSONAPI.configuration.resource_cache.nil?
|
1012
|
+
end
|
1013
|
+
end
|
1014
|
+
|
1015
|
+
def attribute_caching_context(_context)
|
1016
|
+
nil
|
1017
|
+
end
|
1018
|
+
|
1019
|
+
# Generate a hashcode from the value to be used as part of the cache lookup
|
1020
|
+
def hash_cache_field(value)
|
1021
|
+
value.hash
|
1022
|
+
end
|
1023
|
+
|
1024
|
+
def _model_class
|
1025
|
+
return nil if _abstract
|
1026
|
+
|
1027
|
+
return @model_class if @model_class
|
1028
|
+
|
1029
|
+
model_name = _model_name
|
1030
|
+
return nil if model_name.to_s.blank?
|
1031
|
+
|
1032
|
+
@model_class = model_name.to_s.safe_constantize
|
1033
|
+
if @model_class.nil?
|
1034
|
+
warn "[MODEL NOT FOUND] Model could not be found for #{self.name}. If this is a base Resource declare it as abstract."
|
1035
|
+
end
|
1036
|
+
|
1037
|
+
@model_class
|
1038
|
+
end
|
1039
|
+
|
1040
|
+
def _allowed_filter?(filter)
|
1041
|
+
!_allowed_filters[filter].nil?
|
1042
|
+
end
|
1043
|
+
|
1044
|
+
def _has_sort?(sorting)
|
1045
|
+
!_allowed_sort[sorting.to_sym].nil?
|
1046
|
+
end
|
1047
|
+
|
1048
|
+
def module_path
|
1049
|
+
if name == 'JSONAPI::Resource'
|
1050
|
+
''
|
1051
|
+
else
|
1052
|
+
name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').underscore : ''
|
1053
|
+
end
|
1054
|
+
end
|
1055
|
+
|
1056
|
+
def default_sort
|
1057
|
+
[{field: 'id', direction: :asc}]
|
1058
|
+
end
|
1059
|
+
|
1060
|
+
def construct_order_options(sort_params)
|
1061
|
+
sort_params = default_sort if sort_params.blank?
|
1062
|
+
|
1063
|
+
return {} unless sort_params
|
1064
|
+
|
1065
|
+
sort_params.each_with_object({}) do |sort, order_hash|
|
1066
|
+
field = sort[:field].to_s == 'id' ? _primary_key : sort[:field].to_s
|
1067
|
+
order_hash[field] = sort[:direction]
|
1068
|
+
end
|
1069
|
+
end
|
1070
|
+
|
1071
|
+
def _add_relationship(klass, *attrs)
|
1072
|
+
_clear_fields_cache
|
1073
|
+
|
1074
|
+
options = attrs.extract_options!
|
1075
|
+
options[:parent_resource] = self
|
1076
|
+
|
1077
|
+
attrs.each do |name|
|
1078
|
+
relationship_name = name.to_sym
|
1079
|
+
check_reserved_relationship_name(relationship_name)
|
1080
|
+
check_duplicate_relationship_name(relationship_name)
|
1081
|
+
|
1082
|
+
define_relationship_methods(relationship_name.to_sym, klass, options)
|
1083
|
+
end
|
1084
|
+
end
|
1085
|
+
|
1086
|
+
# ResourceBuilder methods
|
1087
|
+
def define_relationship_methods(relationship_name, relationship_klass, options)
|
1088
|
+
relationship = register_relationship(
|
1089
|
+
relationship_name,
|
1090
|
+
relationship_klass.new(relationship_name, options)
|
1091
|
+
)
|
1092
|
+
|
1093
|
+
define_foreign_key_setter(relationship)
|
1094
|
+
end
|
1095
|
+
|
1096
|
+
def define_foreign_key_setter(relationship)
|
1097
|
+
if relationship.polymorphic?
|
1098
|
+
define_on_resource "#{relationship.foreign_key}=" do |v|
|
1099
|
+
_model.method("#{relationship.foreign_key}=").call(v[:id])
|
1100
|
+
_model.public_send("#{relationship.polymorphic_type}=", v[:type])
|
1101
|
+
end
|
1102
|
+
else
|
1103
|
+
define_on_resource "#{relationship.foreign_key}=" do |value|
|
1104
|
+
_model.method("#{relationship.foreign_key}=").call(value)
|
1105
|
+
end
|
1106
|
+
end
|
1107
|
+
end
|
1108
|
+
|
1109
|
+
def define_on_resource(method_name, &block)
|
1110
|
+
return if method_defined?(method_name)
|
1111
|
+
define_method(method_name, block)
|
1112
|
+
end
|
1113
|
+
|
1114
|
+
def register_relationship(name, relationship_object)
|
1115
|
+
@_relationships[name] = relationship_object
|
1116
|
+
end
|
1117
|
+
|
1118
|
+
def _clear_cached_attribute_options
|
1119
|
+
@_cached_attribute_options = {}
|
1120
|
+
end
|
1121
|
+
|
1122
|
+
def _clear_fields_cache
|
1123
|
+
@_fields_cache = nil
|
1124
|
+
end
|
1125
|
+
|
1126
|
+
private
|
1127
|
+
|
1128
|
+
def check_reserved_resource_name(type, name)
|
1129
|
+
if [:ids, :types, :hrefs, :links].include?(type)
|
1130
|
+
warn "[NAME COLLISION] `#{name}` is a reserved resource name."
|
1131
|
+
return
|
1132
|
+
end
|
1133
|
+
end
|
1134
|
+
|
1135
|
+
def check_reserved_attribute_name(name)
|
1136
|
+
# Allow :id since it can be used to specify the format. Since it is a method on the base Resource
|
1137
|
+
# an attribute method won't be created for it.
|
1138
|
+
if [:type, :_cache_field, :cache_field].include?(name.to_sym)
|
1139
|
+
warn "[NAME COLLISION] `#{name}` is a reserved key in #{_resource_name_from_type(_type)}."
|
1140
|
+
end
|
1141
|
+
end
|
1142
|
+
|
1143
|
+
def check_reserved_relationship_name(name)
|
1144
|
+
if [:id, :ids, :type, :types].include?(name.to_sym)
|
1145
|
+
warn "[NAME COLLISION] `#{name}` is a reserved relationship name in #{_resource_name_from_type(_type)}."
|
1146
|
+
end
|
1147
|
+
end
|
1148
|
+
|
1149
|
+
def check_duplicate_relationship_name(name)
|
1150
|
+
if _relationships.include?(name.to_sym)
|
1151
|
+
warn "[DUPLICATE RELATIONSHIP] `#{name}` has already been defined in #{_resource_name_from_type(_type)}."
|
1152
|
+
end
|
1153
|
+
end
|
1154
|
+
|
1155
|
+
def check_duplicate_attribute_name(name)
|
1156
|
+
if _attributes.include?(name.to_sym)
|
1157
|
+
warn "[DUPLICATE ATTRIBUTE] `#{name}` has already been defined in #{_resource_name_from_type(_type)}."
|
1158
|
+
end
|
1159
|
+
end
|
1160
|
+
end
|
1161
|
+
end
|
1162
|
+
end
|