jsonapi-resources 0.9.12 → 0.10.7

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 (45) hide show
  1. checksums.yaml +4 -4
  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 -106
  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 +57 -8
  14. data/lib/jsonapi/error.rb +27 -0
  15. data/lib/jsonapi/error_codes.rb +2 -0
  16. data/lib/jsonapi/exceptions.rb +63 -40
  17. data/lib/jsonapi/formatter.rb +3 -3
  18. data/lib/jsonapi/include_directives.rb +18 -75
  19. data/lib/jsonapi/link_builder.rb +18 -25
  20. data/lib/jsonapi/operation.rb +16 -5
  21. data/lib/jsonapi/operation_result.rb +73 -15
  22. data/lib/jsonapi/paginator.rb +17 -0
  23. data/lib/jsonapi/path.rb +43 -0
  24. data/lib/jsonapi/path_segment.rb +76 -0
  25. data/lib/jsonapi/processor.rb +246 -111
  26. data/lib/jsonapi/relationship.rb +117 -24
  27. data/lib/jsonapi/request_parser.rb +383 -396
  28. data/lib/jsonapi/resource.rb +3 -1376
  29. data/lib/jsonapi/resource_controller_metal.rb +3 -0
  30. data/lib/jsonapi/resource_fragment.rb +47 -0
  31. data/lib/jsonapi/resource_id_tree.rb +112 -0
  32. data/lib/jsonapi/resource_identity.rb +42 -0
  33. data/lib/jsonapi/resource_serializer.rb +124 -286
  34. data/lib/jsonapi/resource_set.rb +176 -0
  35. data/lib/jsonapi/resources/railtie.rb +9 -0
  36. data/lib/jsonapi/resources/version.rb +1 -1
  37. data/lib/jsonapi/response_document.rb +104 -87
  38. data/lib/jsonapi/routing_ext.rb +19 -21
  39. data/lib/jsonapi-resources.rb +20 -4
  40. data/lib/tasks/check_upgrade.rake +52 -0
  41. metadata +34 -31
  42. data/lib/jsonapi/cached_resource_fragment.rb +0 -127
  43. data/lib/jsonapi/operation_dispatcher.rb +0 -88
  44. data/lib/jsonapi/operation_results.rb +0 -35
  45. 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