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