jsonapi-resources 0.4.4 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -13,13 +13,13 @@ module JSONAPI
13
13
  :update,
14
14
  :remove,
15
15
  :save,
16
- :create_has_many_link,
17
- :replace_has_many_links,
18
- :create_has_one_link,
19
- :replace_has_one_link,
20
- :replace_polymorphic_has_one_link,
21
- :remove_has_many_link,
22
- :remove_has_one_link,
16
+ :create_to_many_link,
17
+ :replace_to_many_links,
18
+ :create_to_one_link,
19
+ :replace_to_one_link,
20
+ :replace_polymorphic_to_one_link,
21
+ :remove_to_many_link,
22
+ :remove_to_one_link,
23
23
  :replace_fields
24
24
 
25
25
  def initialize(model, context = nil)
@@ -28,7 +28,7 @@ module JSONAPI
28
28
  end
29
29
 
30
30
  def id
31
- model.send(self.class._primary_key)
31
+ model.public_send(self.class._primary_key)
32
32
  end
33
33
 
34
34
  def is_new?
@@ -62,39 +62,39 @@ module JSONAPI
62
62
  end
63
63
  end
64
64
 
65
- def create_has_many_links(association_type, association_key_values)
66
- change :create_has_many_link do
67
- _create_has_many_links(association_type, association_key_values)
65
+ def create_to_many_links(relationship_type, relationship_key_values)
66
+ change :create_to_many_link do
67
+ _create_to_many_links(relationship_type, relationship_key_values)
68
68
  end
69
69
  end
70
70
 
71
- def replace_has_many_links(association_type, association_key_values)
72
- change :replace_has_many_links do
73
- _replace_has_many_links(association_type, association_key_values)
71
+ def replace_to_many_links(relationship_type, relationship_key_values)
72
+ change :replace_to_many_links do
73
+ _replace_to_many_links(relationship_type, relationship_key_values)
74
74
  end
75
75
  end
76
76
 
77
- def replace_has_one_link(association_type, association_key_value)
78
- change :replace_has_one_link do
79
- _replace_has_one_link(association_type, association_key_value)
77
+ def replace_to_one_link(relationship_type, relationship_key_value)
78
+ change :replace_to_one_link do
79
+ _replace_to_one_link(relationship_type, relationship_key_value)
80
80
  end
81
81
  end
82
82
 
83
- def replace_polymorphic_has_one_link(association_type, association_key_value, association_key_type)
84
- change :replace_polymorphic_has_one_link do
85
- _replace_polymorphic_has_one_link(association_type, association_key_value, association_key_type)
83
+ def replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type)
84
+ change :replace_polymorphic_to_one_link do
85
+ _replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type)
86
86
  end
87
87
  end
88
88
 
89
- def remove_has_many_link(association_type, key)
90
- change :remove_has_many_link do
91
- _remove_has_many_link(association_type, key)
89
+ def remove_to_many_link(relationship_type, key)
90
+ change :remove_to_many_link do
91
+ _remove_to_many_link(relationship_type, key)
92
92
  end
93
93
  end
94
94
 
95
- def remove_has_one_link(association_type)
96
- change :remove_has_one_link do
97
- _remove_has_one_link(association_type)
95
+ def remove_to_one_link(relationship_type)
96
+ change :remove_to_one_link do
97
+ _remove_to_one_link(relationship_type)
98
98
  end
99
99
  end
100
100
 
@@ -111,8 +111,8 @@ module JSONAPI
111
111
 
112
112
  # Override this on a resource to customize how the associated records
113
113
  # are fetched for a model. Particularly helpful for authorization.
114
- def records_for(association_name, _options = {})
115
- model.send association_name
114
+ def records_for(relationship_name, _options = {})
115
+ model.public_send relationship_name
116
116
  end
117
117
 
118
118
  private
@@ -138,7 +138,7 @@ module JSONAPI
138
138
  # ```
139
139
  def _save
140
140
  unless @model.valid?
141
- fail JSONAPI::Exceptions::ValidationErrors.new(@model.errors.messages)
141
+ fail JSONAPI::Exceptions::ValidationErrors.new(self)
142
142
  end
143
143
 
144
144
  if defined? @model.save
@@ -159,65 +159,65 @@ module JSONAPI
159
159
  :completed
160
160
  end
161
161
 
162
- def _create_has_many_links(association_type, association_key_values)
163
- association = self.class._associations[association_type]
162
+ def _create_to_many_links(relationship_type, relationship_key_values)
163
+ relationship = self.class._relationships[relationship_type]
164
164
 
165
- association_key_values.each do |association_key_value|
166
- related_resource = association.resource_klass.find_by_key(association_key_value, context: @context)
165
+ relationship_key_values.each do |relationship_key_value|
166
+ related_resource = relationship.resource_klass.find_by_key(relationship_key_value, context: @context)
167
167
 
168
168
  # TODO: Add option to skip relations that already exist instead of returning an error?
169
- relation = @model.send(association.type).where(association.primary_key => association_key_value).first
169
+ relation = @model.public_send(relationship.type).where(relationship.primary_key => relationship_key_value).first
170
170
  if relation.nil?
171
- @model.send(association.type) << related_resource.model
171
+ @model.public_send(relationship.type) << related_resource.model
172
172
  else
173
- fail JSONAPI::Exceptions::HasManyRelationExists.new(association_key_value)
173
+ fail JSONAPI::Exceptions::HasManyRelationExists.new(relationship_key_value)
174
174
  end
175
175
  end
176
176
 
177
177
  :completed
178
178
  end
179
179
 
180
- def _replace_has_many_links(association_type, association_key_values)
181
- association = self.class._associations[association_type]
180
+ def _replace_to_many_links(relationship_type, relationship_key_values)
181
+ relationship = self.class._relationships[relationship_type]
182
182
 
183
- send("#{association.foreign_key}=", association_key_values)
183
+ send("#{relationship.foreign_key}=", relationship_key_values)
184
184
  @save_needed = true
185
185
 
186
186
  :completed
187
187
  end
188
188
 
189
- def _replace_has_one_link(association_type, association_key_value)
190
- association = self.class._associations[association_type]
189
+ def _replace_to_one_link(relationship_type, relationship_key_value)
190
+ relationship = self.class._relationships[relationship_type]
191
191
 
192
- send("#{association.foreign_key}=", association_key_value)
192
+ send("#{relationship.foreign_key}=", relationship_key_value)
193
193
  @save_needed = true
194
194
 
195
195
  :completed
196
196
  end
197
197
 
198
- def _replace_polymorphic_has_one_link(association_type, key_value, key_type)
199
- association = self.class._associations[association_type.to_sym]
198
+ def _replace_polymorphic_to_one_link(relationship_type, key_value, key_type)
199
+ relationship = self.class._relationships[relationship_type.to_sym]
200
200
 
201
- model.send("#{association.foreign_key}=", key_value)
202
- model.send("#{association.polymorphic_type}=", key_type.to_s.classify)
201
+ model.public_send("#{relationship.foreign_key}=", key_value)
202
+ model.public_send("#{relationship.polymorphic_type}=", key_type.to_s.classify)
203
203
 
204
204
  @save_needed = true
205
205
 
206
206
  :completed
207
207
  end
208
208
 
209
- def _remove_has_many_link(association_type, key)
210
- association = self.class._associations[association_type]
209
+ def _remove_to_many_link(relationship_type, key)
210
+ relationship = self.class._relationships[relationship_type]
211
211
 
212
- @model.send(association.type).delete(key)
212
+ @model.public_send(relationship.type).delete(key)
213
213
 
214
214
  :completed
215
215
  end
216
216
 
217
- def _remove_has_one_link(association_type)
218
- association = self.class._associations[association_type]
217
+ def _remove_to_one_link(relationship_type)
218
+ relationship = self.class._relationships[relationship_type]
219
219
 
220
- send("#{association.foreign_key}=", nil)
220
+ send("#{relationship.foreign_key}=", nil)
221
221
  @save_needed = true
222
222
 
223
223
  :completed
@@ -235,22 +235,22 @@ module JSONAPI
235
235
  end
236
236
  end
237
237
 
238
- field_data[:has_one].each do |association_type, value|
238
+ field_data[:to_one].each do |relationship_type, value|
239
239
  if value.nil?
240
- remove_has_one_link(association_type)
240
+ remove_to_one_link(relationship_type)
241
241
  else
242
242
  case value
243
243
  when Hash
244
- replace_polymorphic_has_one_link(association_type.to_s, value.fetch(:id), value.fetch(:type))
244
+ replace_polymorphic_to_one_link(relationship_type.to_s, value.fetch(:id), value.fetch(:type))
245
245
  else
246
- replace_has_one_link(association_type, value)
246
+ replace_to_one_link(relationship_type, value)
247
247
  end
248
248
  end
249
- end if field_data[:has_one]
249
+ end if field_data[:to_one]
250
250
 
251
- field_data[:has_many].each do |association_type, values|
252
- replace_has_many_links(association_type, values)
253
- end if field_data[:has_many]
251
+ field_data[:to_many].each do |relationship_type, values|
252
+ replace_to_many_links(relationship_type, values)
253
+ end if field_data[:to_many]
254
254
 
255
255
  :completed
256
256
  end
@@ -258,7 +258,7 @@ module JSONAPI
258
258
  class << self
259
259
  def inherited(base)
260
260
  base._attributes = (_attributes || {}).dup
261
- base._associations = (_associations || {}).dup
261
+ base._relationships = (_relationships || {}).dup
262
262
  base._allowed_filters = (_allowed_filters || Set.new).dup
263
263
 
264
264
  type = base.name.demodulize.sub(/Resource$/, '').underscore
@@ -278,7 +278,7 @@ module JSONAPI
278
278
  resource
279
279
  end
280
280
 
281
- attr_accessor :_attributes, :_associations, :_allowed_filters, :_type, :_paginator
281
+ attr_accessor :_attributes, :_relationships, :_allowed_filters, :_type, :_paginator
282
282
 
283
283
  def create(context)
284
284
  new(create_model, context)
@@ -314,11 +314,11 @@ module JSONAPI
314
314
  @_attributes ||= {}
315
315
  @_attributes[attr] = options
316
316
  define_method attr do
317
- @model.send(attr)
317
+ @model.public_send(attr)
318
318
  end unless method_defined?(attr)
319
319
 
320
320
  define_method "#{attr}=" do |value|
321
- @model.send "#{attr}=", value
321
+ @model.public_send "#{attr}=", value
322
322
  end unless method_defined?("#{attr}=")
323
323
  end
324
324
 
@@ -326,12 +326,27 @@ module JSONAPI
326
326
  { format: :default }
327
327
  end
328
328
 
329
+ def relationship(*attrs)
330
+ options = attrs.extract_options!
331
+ klass = case options[:to]
332
+ when :one
333
+ Relationship::ToOne
334
+ when :many
335
+ Relationship::ToMany
336
+ else
337
+ #:nocov:#
338
+ fail ArgumentError.new('to: must be either :one or :many')
339
+ #:nocov:#
340
+ end
341
+ _add_relationship(klass, *attrs, options.except(:to))
342
+ end
343
+
329
344
  def has_one(*attrs)
330
- _associate(Association::HasOne, *attrs)
345
+ _add_relationship(Relationship::ToOne, *attrs)
331
346
  end
332
347
 
333
348
  def has_many(*attrs)
334
- _associate(Association::HasMany, *attrs)
349
+ _add_relationship(Relationship::ToMany, *attrs)
335
350
  end
336
351
 
337
352
  def model_name(model)
@@ -367,12 +382,12 @@ module JSONAPI
367
382
 
368
383
  # Override in your resource to filter the updatable keys
369
384
  def updatable_fields(_context = nil)
370
- _updatable_associations | _attributes.keys - [:id]
385
+ _updatable_relationships | _attributes.keys - [:id]
371
386
  end
372
387
 
373
388
  # Override in your resource to filter the creatable keys
374
389
  def creatable_fields(_context = nil)
375
- _updatable_associations | _attributes.keys
390
+ _updatable_relationships | _attributes.keys
376
391
  end
377
392
 
378
393
  # Override in your resource to filter the sortable keys
@@ -381,33 +396,33 @@ module JSONAPI
381
396
  end
382
397
 
383
398
  def fields
384
- _associations.keys | _attributes.keys
399
+ _relationships.keys | _attributes.keys
385
400
  end
386
401
 
387
- def resolve_association_names_to_relations(resource_klass, model_includes, options = {})
402
+ def resolve_relationship_names_to_relations(resource_klass, model_includes, options = {})
388
403
  case model_includes
389
404
  when Array
390
405
  return model_includes.map do |value|
391
- resolve_association_names_to_relations(resource_klass, value, options)
406
+ resolve_relationship_names_to_relations(resource_klass, value, options)
392
407
  end
393
408
  when Hash
394
409
  model_includes.keys.each do |key|
395
- association = resource_klass._associations[key]
410
+ relationship = resource_klass._relationships[key]
396
411
  value = model_includes[key]
397
412
  model_includes.delete(key)
398
- model_includes[association.relation_name(options)] = resolve_association_names_to_relations(association.resource_klass, value, options)
413
+ model_includes[relationship.relation_name(options)] = resolve_relationship_names_to_relations(relationship.resource_klass, value, options)
399
414
  end
400
415
  return model_includes
401
416
  when Symbol
402
- association = resource_klass._associations[model_includes]
403
- return association.relation_name(options)
417
+ relationship = resource_klass._relationships[model_includes]
418
+ return relationship.relation_name(options)
404
419
  end
405
420
  end
406
421
 
407
422
  def apply_includes(records, options = {})
408
423
  include_directives = options[:include_directives]
409
424
  if include_directives
410
- model_includes = resolve_association_names_to_relations(self, include_directives.model_includes, options)
425
+ model_includes = resolve_relationship_names_to_relations(self, include_directives.model_includes, options)
411
426
  records = records.includes(model_includes)
412
427
  end
413
428
 
@@ -436,12 +451,12 @@ module JSONAPI
436
451
 
437
452
  if filters
438
453
  filters.each do |filter, value|
439
- if _associations.include?(filter)
440
- if _associations[filter].is_a?(JSONAPI::Association::HasMany)
454
+ if _relationships.include?(filter)
455
+ if _relationships[filter].is_a?(JSONAPI::Relationship::ToMany)
441
456
  required_includes.push(filter.to_s)
442
- records = apply_filter(records, "#{filter}.#{_associations[filter].primary_key}", value, options)
457
+ records = apply_filter(records, "#{filter}.#{_relationships[filter].primary_key}", value, options)
443
458
  else
444
- records = apply_filter(records, "#{_associations[filter].foreign_key}", value, options)
459
+ records = apply_filter(records, "#{_relationships[filter].foreign_key}", value, options)
445
460
  end
446
461
  else
447
462
  records = apply_filter(records, filter, value, options)
@@ -514,16 +529,16 @@ module JSONAPI
514
529
  verified_filters
515
530
  end
516
531
 
517
- def is_filter_association?(filter)
518
- filter == _type || _associations.include?(filter)
532
+ def is_filter_relationship?(filter)
533
+ filter == _type || _relationships.include?(filter)
519
534
  end
520
535
 
521
536
  def verify_filter(filter, raw, context = nil)
522
537
  filter_values = []
523
538
  filter_values += CSV.parse_line(raw) unless raw.nil? || raw.empty?
524
539
 
525
- if is_filter_association?(filter)
526
- verify_association_filter(filter, filter_values, context)
540
+ if is_filter_relationship?(filter)
541
+ verify_relationship_filter(filter, filter_values, context)
527
542
  else
528
543
  verify_custom_filter(filter, filter_values, context)
529
544
  end
@@ -548,8 +563,8 @@ module JSONAPI
548
563
  [filter, value]
549
564
  end
550
565
 
551
- # override to allow for custom association logic, such as uuids, multiple keys or permission checks on keys
552
- def verify_association_filter(filter, raw, _context = nil)
566
+ # override to allow for custom relationship logic, such as uuids, multiple keys or permission checks on keys
567
+ def verify_relationship_filter(filter, raw, _context = nil)
553
568
  [filter, raw]
554
569
  end
555
570
 
@@ -558,18 +573,18 @@ module JSONAPI
558
573
  default_attribute_options.merge(@_attributes[attr])
559
574
  end
560
575
 
561
- def _updatable_associations
562
- @_associations.map { |key, _association| key }
576
+ def _updatable_relationships
577
+ @_relationships.map { |key, _relationship| key }
563
578
  end
564
579
 
565
- def _has_association?(type)
580
+ def _has_relationship?(type)
566
581
  type = type.to_s
567
- @_associations.key?(type.singularize.to_sym) || @_associations.key?(type.pluralize.to_sym)
582
+ @_relationships.key?(type.singularize.to_sym) || @_relationships.key?(type.pluralize.to_sym)
568
583
  end
569
584
 
570
- def _association(type)
585
+ def _relationship(type)
571
586
  type = type.to_sym
572
- @_associations[type]
587
+ @_relationships[type]
573
588
  end
574
589
 
575
590
  def _model_name
@@ -577,7 +592,7 @@ module JSONAPI
577
592
  end
578
593
 
579
594
  def _primary_key
580
- @_primary_key ||= :id
595
+ @_primary_key ||= _model_class.respond_to?(:primary_key) ? _model_class.primary_key : :id
581
596
  end
582
597
 
583
598
  def _as_parent_key
@@ -638,68 +653,85 @@ module JSONAPI
638
653
  def check_reserved_attribute_name(name)
639
654
  # Allow :id since it can be used to specify the format. Since it is a method on the base Resource
640
655
  # an attribute method won't be created for it.
641
- if [:type, :href, :links].include?(name.to_sym)
656
+ if [:type, :href, :links, :model].include?(name.to_sym)
642
657
  warn "[NAME COLLISION] `#{name}` is a reserved key in #{@@resource_types[_type]}."
643
658
  end
644
659
  end
645
660
 
646
- def check_reserved_association_name(name)
661
+ def check_reserved_relationship_name(name)
647
662
  if [:id, :ids, :type, :types, :href, :hrefs, :link, :links].include?(name.to_sym)
648
- warn "[NAME COLLISION] `#{name}` is a reserved association name in #{@@resource_types[_type]}."
663
+ warn "[NAME COLLISION] `#{name}` is a reserved relationship name in #{@@resource_types[_type]}."
649
664
  end
650
665
  end
651
666
 
652
- def _associate(klass, *attrs)
667
+ def _add_relationship(klass, *attrs)
653
668
  options = attrs.extract_options!
654
669
  options[:module_path] = module_path
655
670
 
656
671
  attrs.each do |attr|
657
- check_reserved_association_name(attr)
658
- @_associations[attr] = association = klass.new(attr, options)
672
+ check_reserved_relationship_name(attr)
673
+ @_relationships[attr] = relationship = klass.new(attr, options)
659
674
 
660
- associated_records_method_name = case association
661
- when JSONAPI::Association::HasOne then "record_for_#{attr}"
662
- when JSONAPI::Association::HasMany then "records_for_#{attr}"
675
+ associated_records_method_name = case relationship
676
+ when JSONAPI::Relationship::ToOne then "record_for_#{attr}"
677
+ when JSONAPI::Relationship::ToMany then "records_for_#{attr}"
663
678
  end
664
679
 
665
- foreign_key = association.foreign_key
680
+ foreign_key = relationship.foreign_key
666
681
 
667
682
  define_method "#{foreign_key}=" do |value|
668
683
  @model.method("#{foreign_key}=").call(value)
669
684
  end unless method_defined?("#{foreign_key}=")
670
685
 
671
686
  define_method associated_records_method_name do |options = {}|
672
- relation_name = association.relation_name(options.merge({context: @context}))
687
+ options = options.merge({context: @context})
688
+ relation_name = relationship.relation_name(options)
673
689
  records_for(relation_name, options)
674
690
  end unless method_defined?(associated_records_method_name)
675
691
 
676
- if association.is_a?(JSONAPI::Association::HasOne)
677
- define_method foreign_key do
678
- @model.method(foreign_key).call
679
- end unless method_defined?(foreign_key)
692
+ if relationship.is_a?(JSONAPI::Relationship::ToOne)
693
+ if relationship.belongs_to?
694
+ define_method foreign_key do
695
+ @model.method(foreign_key).call
696
+ end unless method_defined?(foreign_key)
680
697
 
681
- define_method attr do |options = {}|
682
- if association.polymorphic?
683
- associated_model = public_send(associated_records_method_name)
684
- resource_klass = Resource.resource_for(self.class.module_path + associated_model.class.to_s.underscore) if associated_model
685
- return resource_klass.new(associated_model, @context) if resource_klass
686
- else
687
- resource_klass = association.resource_klass
698
+ define_method attr do |options = {}|
699
+ if relationship.polymorphic?
700
+ associated_model = public_send(associated_records_method_name)
701
+ resource_klass = Resource.resource_for(self.class.module_path + associated_model.class.to_s.underscore) if associated_model
702
+ return resource_klass.new(associated_model, @context) if resource_klass
703
+ else
704
+ resource_klass = relationship.resource_klass
705
+ if resource_klass
706
+ associated_model = public_send(associated_records_method_name)
707
+ return associated_model ? resource_klass.new(associated_model, @context) : nil
708
+ end
709
+ end
710
+ end unless method_defined?(attr)
711
+ else
712
+ define_method foreign_key do
713
+ record = public_send(associated_records_method_name)
714
+ return nil if record.nil?
715
+ record.public_send(relationship.resource_klass._primary_key)
716
+ end unless method_defined?(foreign_key)
717
+
718
+ define_method attr do |options = {}|
719
+ resource_klass = relationship.resource_klass
688
720
  if resource_klass
689
721
  associated_model = public_send(associated_records_method_name)
690
722
  return associated_model ? resource_klass.new(associated_model, @context) : nil
691
723
  end
692
- end
693
- end unless method_defined?(attr)
694
- elsif association.is_a?(JSONAPI::Association::HasMany)
724
+ end unless method_defined?(attr)
725
+ end
726
+ elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
695
727
  define_method foreign_key do
696
728
  records = public_send(associated_records_method_name)
697
729
  return records.collect do |record|
698
- record.send(association.resource_klass._primary_key)
730
+ record.public_send(relationship.resource_klass._primary_key)
699
731
  end
700
732
  end unless method_defined?(foreign_key)
701
733
  define_method attr do |options = {}|
702
- resource_klass = association.resource_klass
734
+ resource_klass = relationship.resource_klass
703
735
  records = public_send(associated_records_method_name)
704
736
 
705
737
  filters = options.fetch(:filters, {})
@@ -719,7 +751,7 @@ module JSONAPI
719
751
  end
720
752
 
721
753
  return records.collect do |record|
722
- if association.polymorphic?
754
+ if relationship.polymorphic?
723
755
  resource_klass = Resource.resource_for(self.class.module_path + record.class.to_s.underscore)
724
756
  end
725
757
  resource_klass.new(record, @context)