jsonapi-resources 0.4.2 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -2
  3. data/README.md +103 -71
  4. data/Rakefile +2 -2
  5. data/jsonapi-resources.gemspec +2 -2
  6. data/lib/jsonapi-resources.rb +0 -1
  7. data/lib/jsonapi/active_record_operations_processor.rb +10 -2
  8. data/lib/jsonapi/acts_as_resource_controller.rb +26 -24
  9. data/lib/jsonapi/association.rb +50 -15
  10. data/lib/jsonapi/callbacks.rb +1 -2
  11. data/lib/jsonapi/configuration.rb +8 -24
  12. data/lib/jsonapi/error.rb +1 -2
  13. data/lib/jsonapi/error_codes.rb +3 -1
  14. data/lib/jsonapi/exceptions.rb +59 -47
  15. data/lib/jsonapi/include_directives.rb +11 -11
  16. data/lib/jsonapi/mime_types.rb +2 -2
  17. data/lib/jsonapi/operation.rb +28 -11
  18. data/lib/jsonapi/operations_processor.rb +16 -5
  19. data/lib/jsonapi/paginator.rb +19 -19
  20. data/lib/jsonapi/request.rb +175 -196
  21. data/lib/jsonapi/resource.rb +158 -105
  22. data/lib/jsonapi/resource_serializer.rb +37 -26
  23. data/lib/jsonapi/resources/version.rb +2 -2
  24. data/lib/jsonapi/response_document.rb +5 -4
  25. data/lib/jsonapi/routing_ext.rb +24 -19
  26. data/test/controllers/controller_test.rb +261 -31
  27. data/test/fixtures/active_record.rb +206 -8
  28. data/test/fixtures/book_comments.yml +2 -1
  29. data/test/fixtures/books.yml +1 -0
  30. data/test/fixtures/documents.yml +3 -0
  31. data/test/fixtures/people.yml +8 -1
  32. data/test/fixtures/pictures.yml +15 -0
  33. data/test/fixtures/products.yml +3 -0
  34. data/test/fixtures/vehicles.yml +8 -0
  35. data/test/helpers/{hash_helpers.rb → assertions.rb} +6 -1
  36. data/test/integration/requests/request_test.rb +14 -3
  37. data/test/integration/routes/routes_test.rb +47 -0
  38. data/test/test_helper.rb +27 -4
  39. data/test/unit/serializer/include_directives_test.rb +5 -0
  40. data/test/unit/serializer/polymorphic_serializer_test.rb +384 -0
  41. data/test/unit/serializer/serializer_test.rb +19 -1
  42. metadata +14 -4
@@ -6,7 +6,7 @@ module JSONAPI
6
6
 
7
7
  @@resource_types = {}
8
8
 
9
- attr :context
9
+ attr_reader :context
10
10
  attr_reader :model
11
11
 
12
12
  define_jsonapi_resources_callbacks :create,
@@ -17,6 +17,7 @@ module JSONAPI
17
17
  :replace_has_many_links,
18
18
  :create_has_one_link,
19
19
  :replace_has_one_link,
20
+ :replace_polymorphic_has_one_link,
20
21
  :remove_has_many_link,
21
22
  :remove_has_one_link,
22
23
  :replace_fields
@@ -48,9 +49,7 @@ module JSONAPI
48
49
  completed = (yield == :completed)
49
50
  end
50
51
 
51
- if @save_needed || is_new?
52
- completed = (save == :completed)
53
- end
52
+ completed = (save == :completed) if @save_needed || is_new?
54
53
  end
55
54
  end
56
55
 
@@ -81,6 +80,12 @@ module JSONAPI
81
80
  end
82
81
  end
83
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)
86
+ end
87
+ end
88
+
84
89
  def remove_has_many_link(association_type, key)
85
90
  change :remove_has_many_link do
86
91
  _remove_has_many_link(association_type, key)
@@ -106,11 +111,12 @@ module JSONAPI
106
111
 
107
112
  # Override this on a resource to customize how the associated records
108
113
  # are fetched for a model. Particularly helpful for authorization.
109
- def records_for(association_name, options = {})
114
+ def records_for(association_name, _options = {})
110
115
  model.send association_name
111
116
  end
112
117
 
113
118
  private
119
+
114
120
  def save
115
121
  run_callbacks :save do
116
122
  _save
@@ -132,45 +138,43 @@ module JSONAPI
132
138
  # ```
133
139
  def _save
134
140
  unless @model.valid?
135
- raise JSONAPI::Exceptions::ValidationErrors.new(@model.errors.messages)
141
+ fail JSONAPI::Exceptions::ValidationErrors.new(@model.errors.messages)
136
142
  end
137
143
 
138
144
  if defined? @model.save
139
145
  saved = @model.save
140
- unless saved
141
- raise JSONAPI::Exceptions::SaveFailed.new
142
- end
146
+ fail JSONAPI::Exceptions::SaveFailed.new unless saved
143
147
  else
144
148
  saved = true
145
149
  end
146
150
 
147
151
  @save_needed = !saved
148
152
 
149
- return :completed
153
+ :completed
150
154
  end
151
155
 
152
156
  def _remove
153
157
  @model.destroy
154
158
 
155
- return :completed
159
+ :completed
156
160
  end
157
161
 
158
162
  def _create_has_many_links(association_type, association_key_values)
159
163
  association = self.class._associations[association_type]
160
164
 
161
165
  association_key_values.each do |association_key_value|
162
- related_resource = Resource.resource_for(self.class.module_path + association.type.to_s).find_by_key(association_key_value, context: @context)
166
+ related_resource = association.resource_klass.find_by_key(association_key_value, context: @context)
163
167
 
164
- # ToDo: Add option to skip relations that already exist instead of returning an error?
168
+ # TODO: Add option to skip relations that already exist instead of returning an error?
165
169
  relation = @model.send(association.type).where(association.primary_key => association_key_value).first
166
170
  if relation.nil?
167
171
  @model.send(association.type) << related_resource.model
168
172
  else
169
- raise JSONAPI::Exceptions::HasManyRelationExists.new(association_key_value)
173
+ fail JSONAPI::Exceptions::HasManyRelationExists.new(association_key_value)
170
174
  end
171
175
  end
172
176
 
173
- return :completed
177
+ :completed
174
178
  end
175
179
 
176
180
  def _replace_has_many_links(association_type, association_key_values)
@@ -179,7 +183,7 @@ module JSONAPI
179
183
  send("#{association.foreign_key}=", association_key_values)
180
184
  @save_needed = true
181
185
 
182
- return :completed
186
+ :completed
183
187
  end
184
188
 
185
189
  def _replace_has_one_link(association_type, association_key_value)
@@ -188,7 +192,18 @@ module JSONAPI
188
192
  send("#{association.foreign_key}=", association_key_value)
189
193
  @save_needed = true
190
194
 
191
- return :completed
195
+ :completed
196
+ end
197
+
198
+ def _replace_polymorphic_has_one_link(association_type, key_value, key_type)
199
+ association = self.class._associations[association_type.to_sym]
200
+
201
+ model.send("#{association.foreign_key}=", key_value)
202
+ model.send("#{association.polymorphic_type}=", key_type.to_s.classify)
203
+
204
+ @save_needed = true
205
+
206
+ :completed
192
207
  end
193
208
 
194
209
  def _remove_has_many_link(association_type, key)
@@ -196,7 +211,7 @@ module JSONAPI
196
211
 
197
212
  @model.send(association.type).delete(key)
198
213
 
199
- return :completed
214
+ :completed
200
215
  end
201
216
 
202
217
  def _remove_has_one_link(association_type)
@@ -205,7 +220,7 @@ module JSONAPI
205
220
  send("#{association.foreign_key}=", nil)
206
221
  @save_needed = true
207
222
 
208
- return :completed
223
+ :completed
209
224
  end
210
225
 
211
226
  def _replace_fields(field_data)
@@ -224,7 +239,12 @@ module JSONAPI
224
239
  if value.nil?
225
240
  remove_has_one_link(association_type)
226
241
  else
227
- replace_has_one_link(association_type, value)
242
+ case value
243
+ when Hash
244
+ replace_polymorphic_has_one_link(association_type.to_s, value.fetch(:id), value.fetch(:type))
245
+ else
246
+ replace_has_one_link(association_type, value)
247
+ end
228
248
  end
229
249
  end if field_data[:has_one]
230
250
 
@@ -232,7 +252,7 @@ module JSONAPI
232
252
  replace_has_many_links(association_type, values)
233
253
  end if field_data[:has_many]
234
254
 
235
- return :completed
255
+ :completed
236
256
  end
237
257
 
238
258
  class << self
@@ -253,15 +273,15 @@ module JSONAPI
253
273
  resource_name = JSONAPI::Resource._resource_name_from_type(type)
254
274
  resource = resource_name.safe_constantize if resource_name
255
275
  if resource.nil?
256
- raise NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)"
276
+ fail NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)"
257
277
  end
258
278
  resource
259
279
  end
260
280
 
261
- attr_accessor :_attributes, :_associations, :_allowed_filters , :_type, :_paginator
281
+ attr_accessor :_attributes, :_associations, :_allowed_filters, :_type, :_paginator
262
282
 
263
283
  def create(context)
264
- self.new(self.create_model, context)
284
+ new(create_model, context)
265
285
  end
266
286
 
267
287
  def create_model
@@ -303,7 +323,7 @@ module JSONAPI
303
323
  end
304
324
 
305
325
  def default_attribute_options
306
- {format: :default}
326
+ { format: :default }
307
327
  end
308
328
 
309
329
  def has_one(*attrs)
@@ -319,7 +339,7 @@ module JSONAPI
319
339
  end
320
340
 
321
341
  def filters(*attrs)
322
- @_allowed_filters.merge!(attrs.inject( Hash.new ) { |h, attr| h[attr] = {}; h })
342
+ @_allowed_filters.merge!(attrs.inject({}) { |h, attr| h[attr] = {}; h })
323
343
  end
324
344
 
325
345
  def filter(attr, *args)
@@ -334,11 +354,11 @@ module JSONAPI
334
354
  # :nocov:
335
355
  def method_missing(method, *args)
336
356
  if method.to_s.match /createable_fields/
337
- ActiveSupport::Deprecation.warn("`createable_fields` is deprecated, please use `creatable_fields` instead")
338
- self.creatable_fields(*args)
357
+ ActiveSupport::Deprecation.warn('`createable_fields` is deprecated, please use `creatable_fields` instead')
358
+ creatable_fields(*args)
339
359
  elsif method.to_s.match /updateable_fields/
340
- ActiveSupport::Deprecation.warn("`updateable_fields` is deprecated, please use `updatable_fields` instead")
341
- self.updatable_fields(*args)
360
+ ActiveSupport::Deprecation.warn('`updateable_fields` is deprecated, please use `updatable_fields` instead')
361
+ updatable_fields(*args)
342
362
  else
343
363
  super
344
364
  end
@@ -346,17 +366,17 @@ module JSONAPI
346
366
  # :nocov:
347
367
 
348
368
  # Override in your resource to filter the updatable keys
349
- def updatable_fields(context = nil)
369
+ def updatable_fields(_context = nil)
350
370
  _updatable_associations | _attributes.keys - [:id]
351
371
  end
352
372
 
353
373
  # Override in your resource to filter the creatable keys
354
- def creatable_fields(context = nil)
374
+ def creatable_fields(_context = nil)
355
375
  _updatable_associations | _attributes.keys
356
376
  end
357
377
 
358
378
  # Override in your resource to filter the sortable keys
359
- def sortable_fields(context = nil)
379
+ def sortable_fields(_context = nil)
360
380
  _attributes.keys
361
381
  end
362
382
 
@@ -364,15 +384,38 @@ module JSONAPI
364
384
  _associations.keys | _attributes.keys
365
385
  end
366
386
 
367
- def apply_includes(records, directives)
368
- records = records.includes(*directives.model_includes) if directives
387
+ def resolve_association_names_to_relations(resource_klass, model_includes, options = {})
388
+ case model_includes
389
+ when Array
390
+ return model_includes.map do |value|
391
+ resolve_association_names_to_relations(resource_klass, value, options)
392
+ end
393
+ when Hash
394
+ model_includes.keys.each do |key|
395
+ association = resource_klass._associations[key]
396
+ value = model_includes[key]
397
+ model_includes.delete(key)
398
+ model_includes[association.relation_name(options)] = resolve_association_names_to_relations(association.resource_klass, value, options)
399
+ end
400
+ return model_includes
401
+ when Symbol
402
+ association = resource_klass._associations[model_includes]
403
+ return association.relation_name(options)
404
+ end
405
+ end
406
+
407
+ def apply_includes(records, options = {})
408
+ include_directives = options[:include_directives]
409
+ if include_directives
410
+ model_includes = resolve_association_names_to_relations(self, include_directives.model_includes, options)
411
+ records = records.includes(model_includes)
412
+ end
413
+
369
414
  records
370
415
  end
371
416
 
372
417
  def apply_pagination(records, paginator, order_options)
373
- if paginator
374
- records = paginator.apply(records, order_options)
375
- end
418
+ records = paginator.apply(records, order_options) if paginator
376
419
  records
377
420
  end
378
421
 
@@ -384,7 +427,7 @@ module JSONAPI
384
427
  end
385
428
  end
386
429
 
387
- def apply_filter(records, filter, value, options = {})
430
+ def apply_filter(records, filter, value, _options = {})
388
431
  records.where(filter => value)
389
432
  end
390
433
 
@@ -395,7 +438,7 @@ module JSONAPI
395
438
  filters.each do |filter, value|
396
439
  if _associations.include?(filter)
397
440
  if _associations[filter].is_a?(JSONAPI::Association::HasMany)
398
- required_includes.push(filter)
441
+ required_includes.push(filter.to_s)
399
442
  records = apply_filter(records, "#{filter}.#{_associations[filter].primary_key}", value, options)
400
443
  else
401
444
  records = apply_filter(records, "#{_associations[filter].foreign_key}", value, options)
@@ -407,20 +450,16 @@ module JSONAPI
407
450
  end
408
451
 
409
452
  if required_includes.any?
410
- records.includes(required_includes)
411
- elsif records.respond_to? :to_ary
412
- records
413
- else
414
- records.all
453
+ records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(required_includes)))
415
454
  end
455
+
456
+ records
416
457
  end
417
458
 
418
459
  def filter_records(filters, options)
419
- include_directives = options[:include_directives]
420
-
421
460
  records = records(options)
422
- records = apply_includes(records, include_directives)
423
- apply_filters(records, filters, options)
461
+ records = apply_filters(records, filters, options)
462
+ apply_includes(records, options)
424
463
  end
425
464
 
426
465
  def sort_records(records, order_options)
@@ -445,27 +484,24 @@ module JSONAPI
445
484
 
446
485
  resources = []
447
486
  records.each do |model|
448
- resources.push self.new(model, context)
487
+ resources.push new(model, context)
449
488
  end
450
489
 
451
- return resources
490
+ resources
452
491
  end
453
492
 
454
493
  def find_by_key(key, options = {})
455
494
  context = options[:context]
456
- include_directives = options[:include_directives]
457
495
  records = records(options)
458
- records = apply_includes(records, include_directives)
496
+ records = apply_includes(records, options)
459
497
  model = records.where({_primary_key => key}).first
460
- if model.nil?
461
- raise JSONAPI::Exceptions::RecordNotFound.new(key)
462
- end
463
- self.new(model, context)
498
+ fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil?
499
+ new(model, context)
464
500
  end
465
501
 
466
502
  # Override this method if you want to customize the relation for
467
503
  # finder methods (find, find_by_key)
468
- def records(options = {})
504
+ def records(_options = {})
469
505
  _model_class
470
506
  end
471
507
 
@@ -494,7 +530,7 @@ module JSONAPI
494
530
  end
495
531
 
496
532
  # override to allow for key processing and checking
497
- def verify_key(key, context = nil)
533
+ def verify_key(key, _context = nil)
498
534
  key && Integer(key)
499
535
  rescue
500
536
  raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
@@ -508,13 +544,13 @@ module JSONAPI
508
544
  end
509
545
 
510
546
  # override to allow for custom filters
511
- def verify_custom_filter(filter, value, context = nil)
512
- return filter, value
547
+ def verify_custom_filter(filter, value, _context = nil)
548
+ [filter, value]
513
549
  end
514
550
 
515
551
  # override to allow for custom association logic, such as uuids, multiple keys or permission checks on keys
516
- def verify_association_filter(filter, raw, context = nil)
517
- return filter, raw
552
+ def verify_association_filter(filter, raw, _context = nil)
553
+ [filter, raw]
518
554
  end
519
555
 
520
556
  # quasi private class methods
@@ -523,12 +559,12 @@ module JSONAPI
523
559
  end
524
560
 
525
561
  def _updatable_associations
526
- @_associations.map { |key, association| key }
562
+ @_associations.map { |key, _association| key }
527
563
  end
528
564
 
529
565
  def _has_association?(type)
530
566
  type = type.to_s
531
- @_associations.has_key?(type.singularize.to_sym) || @_associations.has_key?(type.pluralize.to_sym)
567
+ @_associations.key?(type.singularize.to_sym) || @_associations.key?(type.pluralize.to_sym)
532
568
  end
533
569
 
534
570
  def _association(type)
@@ -537,7 +573,7 @@ module JSONAPI
537
573
  end
538
574
 
539
575
  def _model_name
540
- @_model_name ||= self.name.demodulize.sub(/Resource$/, '')
576
+ @_model_name ||= name.demodulize.sub(/Resource$/, '')
541
577
  end
542
578
 
543
579
  def _primary_key
@@ -549,13 +585,13 @@ module JSONAPI
549
585
  end
550
586
 
551
587
  def _allowed_filters
552
- !@_allowed_filters.nil? ? @_allowed_filters : { :id => {} }
588
+ !@_allowed_filters.nil? ? @_allowed_filters : { id: {} }
553
589
  end
554
590
 
555
591
  def _resource_name_from_type(type)
556
592
  class_name = @@resource_types[type]
557
593
  if class_name.nil?
558
- class_name = "#{type.to_s.singularize}_resource".camelize
594
+ class_name = "#{type.to_s.underscore.singularize}_resource".camelize
559
595
  @@resource_types[type] = class_name
560
596
  end
561
597
  return class_name
@@ -578,19 +614,20 @@ module JSONAPI
578
614
  end
579
615
 
580
616
  def module_path
581
- @module_path ||= self.name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').downcase : ''
617
+ @module_path ||= name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').downcase : ''
582
618
  end
583
619
 
584
620
  def construct_order_options(sort_params)
585
621
  return {} unless sort_params
586
622
 
587
- sort_params.each_with_object({}) { |sort, order_hash|
623
+ sort_params.each_with_object({}) do |sort, order_hash|
588
624
  field = sort[:field] == 'id' ? _primary_key : sort[:field]
589
625
  order_hash[field] = sort[:direction]
590
- }
626
+ end
591
627
  end
592
628
 
593
629
  private
630
+
594
631
  def check_reserved_resource_name(type, name)
595
632
  if [:ids, :types, :hrefs, :links].include?(type)
596
633
  warn "[NAME COLLISION] `#{name}` is a reserved resource name."
@@ -618,63 +655,79 @@ module JSONAPI
618
655
 
619
656
  attrs.each do |attr|
620
657
  check_reserved_association_name(attr)
658
+ @_associations[attr] = association = klass.new(attr, options)
621
659
 
622
- @_associations[attr] = klass.new(attr, options)
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}"
663
+ end
623
664
 
624
- foreign_key = @_associations[attr].foreign_key
625
-
626
- define_method foreign_key do
627
- @model.method(foreign_key).call
628
- end unless method_defined?(foreign_key)
665
+ foreign_key = association.foreign_key
629
666
 
630
667
  define_method "#{foreign_key}=" do |value|
631
668
  @model.method("#{foreign_key}=").call(value)
632
669
  end unless method_defined?("#{foreign_key}=")
633
670
 
634
- associated_records_method_name = case @_associations[attr]
635
- when JSONAPI::Association::HasOne then "record_for_#{attr}"
636
- when JSONAPI::Association::HasMany then "records_for_#{attr}"
637
- end
638
-
639
- define_method associated_records_method_name do |options={}|
640
- records_for(attr, options)
671
+ define_method associated_records_method_name do |options = {}|
672
+ relation_name = association.relation_name(options.merge({context: @context}))
673
+ records_for(relation_name, options)
641
674
  end unless method_defined?(associated_records_method_name)
642
675
 
643
- if @_associations[attr].is_a?(JSONAPI::Association::HasOne)
644
- define_method attr do
645
- type_name = self.class._associations[attr].type.to_s
646
- resource_class = Resource.resource_for(self.class.module_path + type_name)
647
- if resource_class
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)
680
+
681
+ define_method attr do |options = {}|
682
+ if association.polymorphic?
648
683
  associated_model = public_send(associated_records_method_name)
649
- return associated_model ? resource_class.new(associated_model, @context) : nil
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
688
+ if resource_klass
689
+ associated_model = public_send(associated_records_method_name)
690
+ return associated_model ? resource_klass.new(associated_model, @context) : nil
691
+ end
650
692
  end
651
693
  end unless method_defined?(attr)
652
- elsif @_associations[attr].is_a?(JSONAPI::Association::HasMany)
694
+ elsif association.is_a?(JSONAPI::Association::HasMany)
695
+ define_method foreign_key do
696
+ records = public_send(associated_records_method_name)
697
+ return records.collect do |record|
698
+ record.send(association.resource_klass._primary_key)
699
+ end
700
+ end unless method_defined?(foreign_key)
653
701
  define_method attr do |options = {}|
654
- type_name = self.class._associations[attr].type.to_s
655
- resource_class = Resource.resource_for(self.class.module_path + type_name)
702
+ resource_klass = association.resource_klass
703
+ records = public_send(associated_records_method_name)
704
+
656
705
  filters = options.fetch(:filters, {})
706
+ unless filters.nil? || filters.empty?
707
+ records = resource_klass.apply_filters(records, filters, options)
708
+ end
709
+
657
710
  sort_criteria = options.fetch(:sort_criteria, {})
658
- paginator = options[:paginator]
711
+ unless sort_criteria.nil? || sort_criteria.empty?
712
+ order_options = self.class.construct_order_options(sort_criteria)
713
+ records = resource_klass.apply_sort(records, order_options)
714
+ end
659
715
 
660
- resources = []
716
+ paginator = options[:paginator]
717
+ if paginator
718
+ records = resource_klass.apply_pagination(records, paginator, order_options)
719
+ end
661
720
 
662
- if resource_class
663
- records = public_send(associated_records_method_name)
664
- records = resource_class.apply_filters(records, filters, options)
665
- order_options = self.class.construct_order_options(sort_criteria)
666
- records = resource_class.apply_sort(records, order_options)
667
- records = resource_class.apply_pagination(records, paginator, order_options)
668
- records.each do |record|
669
- resources.push resource_class.new(record, @context)
721
+ return records.collect do |record|
722
+ if association.polymorphic?
723
+ resource_klass = Resource.resource_for(self.class.module_path + record.class.to_s.underscore)
670
724
  end
725
+ resource_klass.new(record, @context)
671
726
  end
672
- return resources
673
727
  end unless method_defined?(attr)
674
728
  end
675
729
  end
676
730
  end
677
731
  end
678
-
679
732
  end
680
733
  end