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.
@@ -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