sanger-jsonapi-resources 0.1.1 → 0.2.0

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