jsonapi-resources 0.9.0 → 0.10.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE.txt +1 -1
  3. data/README.md +34 -11
  4. data/lib/bug_report_templates/rails_5_latest.rb +125 -0
  5. data/lib/bug_report_templates/rails_5_master.rb +140 -0
  6. data/lib/jsonapi/active_relation/adapters/join_left_active_record_adapter.rb +27 -0
  7. data/lib/jsonapi/active_relation/join_manager.rb +303 -0
  8. data/lib/jsonapi/active_relation_resource.rb +884 -0
  9. data/lib/jsonapi/acts_as_resource_controller.rb +122 -105
  10. data/lib/jsonapi/basic_resource.rb +1162 -0
  11. data/lib/jsonapi/cached_response_fragment.rb +127 -0
  12. data/lib/jsonapi/compiled_json.rb +11 -1
  13. data/lib/jsonapi/configuration.rb +71 -8
  14. data/lib/jsonapi/error.rb +27 -0
  15. data/lib/jsonapi/error_codes.rb +2 -0
  16. data/lib/jsonapi/exceptions.rb +80 -50
  17. data/lib/jsonapi/formatter.rb +3 -3
  18. data/lib/jsonapi/include_directives.rb +18 -65
  19. data/lib/jsonapi/link_builder.rb +74 -80
  20. data/lib/jsonapi/operation.rb +16 -5
  21. data/lib/jsonapi/operation_result.rb +74 -16
  22. data/lib/jsonapi/path.rb +43 -0
  23. data/lib/jsonapi/path_segment.rb +76 -0
  24. data/lib/jsonapi/processor.rb +239 -111
  25. data/lib/jsonapi/relationship.rb +153 -15
  26. data/lib/jsonapi/request_parser.rb +430 -367
  27. data/lib/jsonapi/resource.rb +3 -1253
  28. data/lib/jsonapi/resource_controller_metal.rb +5 -2
  29. data/lib/jsonapi/resource_fragment.rb +47 -0
  30. data/lib/jsonapi/resource_id_tree.rb +112 -0
  31. data/lib/jsonapi/resource_identity.rb +42 -0
  32. data/lib/jsonapi/resource_serializer.rb +143 -285
  33. data/lib/jsonapi/resource_set.rb +176 -0
  34. data/lib/jsonapi/resources/railtie.rb +9 -0
  35. data/lib/jsonapi/resources/version.rb +1 -1
  36. data/lib/jsonapi/response_document.rb +105 -83
  37. data/lib/jsonapi/routing_ext.rb +48 -26
  38. data/lib/jsonapi-resources.rb +20 -4
  39. data/lib/tasks/check_upgrade.rake +52 -0
  40. metadata +50 -20
  41. data/lib/jsonapi/cached_resource_fragment.rb +0 -127
  42. data/lib/jsonapi/operation_dispatcher.rb +0 -88
  43. data/lib/jsonapi/operation_results.rb +0 -35
  44. 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