jsonapi-resources 0.7.1.beta1 → 0.7.1.beta2

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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +248 -74
  3. data/lib/jsonapi-resources.rb +5 -3
  4. data/lib/jsonapi/acts_as_resource_controller.rb +77 -14
  5. data/lib/jsonapi/configuration.rb +77 -16
  6. data/lib/jsonapi/error.rb +12 -0
  7. data/lib/jsonapi/error_codes.rb +2 -0
  8. data/lib/jsonapi/exceptions.rb +29 -9
  9. data/lib/jsonapi/formatter.rb +29 -4
  10. data/lib/jsonapi/link_builder.rb +18 -18
  11. data/lib/jsonapi/mime_types.rb +25 -6
  12. data/lib/jsonapi/naive_cache.rb +30 -0
  13. data/lib/jsonapi/operation.rb +10 -342
  14. data/lib/jsonapi/operation_dispatcher.rb +87 -0
  15. data/lib/jsonapi/operation_result.rb +2 -1
  16. data/lib/jsonapi/paginator.rb +6 -2
  17. data/lib/jsonapi/processor.rb +283 -0
  18. data/lib/jsonapi/relationship.rb +6 -4
  19. data/lib/jsonapi/{request.rb → request_parser.rb} +46 -35
  20. data/lib/jsonapi/resource.rb +88 -13
  21. data/lib/jsonapi/resource_controller.rb +2 -14
  22. data/lib/jsonapi/resource_controller_metal.rb +17 -0
  23. data/lib/jsonapi/resource_serializer.rb +62 -47
  24. data/lib/jsonapi/resources/version.rb +1 -1
  25. data/lib/jsonapi/response_document.rb +13 -2
  26. data/lib/jsonapi/routing_ext.rb +49 -11
  27. metadata +37 -129
  28. data/.gitignore +0 -22
  29. data/.travis.yml +0 -9
  30. data/Gemfile +0 -23
  31. data/Rakefile +0 -20
  32. data/jsonapi-resources.gemspec +0 -29
  33. data/lib/jsonapi/active_record_operations_processor.rb +0 -35
  34. data/lib/jsonapi/operations_processor.rb +0 -120
  35. data/locales/en.yml +0 -80
  36. data/test/config/database.yml +0 -5
  37. data/test/controllers/controller_test.rb +0 -3312
  38. data/test/fixtures/active_record.rb +0 -1486
  39. data/test/fixtures/author_details.yml +0 -9
  40. data/test/fixtures/book_authors.yml +0 -3
  41. data/test/fixtures/book_comments.yml +0 -12
  42. data/test/fixtures/books.yml +0 -7
  43. data/test/fixtures/categories.yml +0 -35
  44. data/test/fixtures/comments.yml +0 -21
  45. data/test/fixtures/comments_tags.yml +0 -20
  46. data/test/fixtures/companies.yml +0 -4
  47. data/test/fixtures/craters.yml +0 -9
  48. data/test/fixtures/customers.yml +0 -11
  49. data/test/fixtures/documents.yml +0 -3
  50. data/test/fixtures/expense_entries.yml +0 -13
  51. data/test/fixtures/facts.yml +0 -11
  52. data/test/fixtures/hair_cuts.yml +0 -3
  53. data/test/fixtures/iso_currencies.yml +0 -17
  54. data/test/fixtures/line_items.yml +0 -37
  55. data/test/fixtures/makes.yml +0 -2
  56. data/test/fixtures/moons.yml +0 -6
  57. data/test/fixtures/numeros_telefone.yml +0 -3
  58. data/test/fixtures/order_flags.yml +0 -7
  59. data/test/fixtures/people.yml +0 -31
  60. data/test/fixtures/pictures.yml +0 -15
  61. data/test/fixtures/planet_types.yml +0 -19
  62. data/test/fixtures/planets.yml +0 -47
  63. data/test/fixtures/posts.yml +0 -102
  64. data/test/fixtures/posts_tags.yml +0 -59
  65. data/test/fixtures/preferences.yml +0 -14
  66. data/test/fixtures/products.yml +0 -3
  67. data/test/fixtures/purchase_orders.yml +0 -23
  68. data/test/fixtures/sections.yml +0 -8
  69. data/test/fixtures/tags.yml +0 -39
  70. data/test/fixtures/vehicles.yml +0 -17
  71. data/test/fixtures/web_pages.yml +0 -3
  72. data/test/helpers/assertions.rb +0 -13
  73. data/test/helpers/functional_helpers.rb +0 -59
  74. data/test/helpers/value_matchers.rb +0 -60
  75. data/test/helpers/value_matchers_test.rb +0 -40
  76. data/test/integration/requests/namespaced_model_test.rb +0 -13
  77. data/test/integration/requests/request_test.rb +0 -932
  78. data/test/integration/routes/routes_test.rb +0 -218
  79. data/test/integration/sti_fields_test.rb +0 -18
  80. data/test/lib/generators/jsonapi/controller_generator_test.rb +0 -25
  81. data/test/lib/generators/jsonapi/resource_generator_test.rb +0 -30
  82. data/test/test_helper.rb +0 -342
  83. data/test/unit/formatters/dasherized_key_formatter_test.rb +0 -8
  84. data/test/unit/jsonapi_request/jsonapi_request_test.rb +0 -199
  85. data/test/unit/operation/operations_processor_test.rb +0 -528
  86. data/test/unit/pagination/offset_paginator_test.rb +0 -245
  87. data/test/unit/pagination/paged_paginator_test.rb +0 -242
  88. data/test/unit/resource/resource_test.rb +0 -560
  89. data/test/unit/serializer/include_directives_test.rb +0 -113
  90. data/test/unit/serializer/link_builder_test.rb +0 -244
  91. data/test/unit/serializer/polymorphic_serializer_test.rb +0 -383
  92. data/test/unit/serializer/response_document_test.rb +0 -61
  93. data/test/unit/serializer/serializer_test.rb +0 -1939
@@ -2,7 +2,7 @@ require 'jsonapi/operation'
2
2
  require 'jsonapi/paginator'
3
3
 
4
4
  module JSONAPI
5
- class Request
5
+ class RequestParser
6
6
  attr_accessor :fields, :include, :filters, :sort_criteria, :errors, :operations,
7
7
  :resource_klass, :context, :paginator, :source_klass, :source_id,
8
8
  :include_directives, :params, :warnings, :server_error_callbacks
@@ -289,27 +289,29 @@ module JSONAPI
289
289
  end
290
290
 
291
291
  def add_find_operation
292
- @operations.push JSONAPI::FindOperation.new(
292
+ @operations.push JSONAPI::Operation.new(:find,
293
293
  @resource_klass,
294
294
  context: @context,
295
295
  filters: @filters,
296
296
  include_directives: @include_directives,
297
297
  sort_criteria: @sort_criteria,
298
- paginator: @paginator
298
+ paginator: @paginator,
299
+ fields: @fields
299
300
  )
300
301
  end
301
302
 
302
303
  def add_show_operation
303
- @operations.push JSONAPI::ShowOperation.new(
304
+ @operations.push JSONAPI::Operation.new(:show,
304
305
  @resource_klass,
305
306
  context: @context,
306
307
  id: @id,
307
- include_directives: @include_directives
308
+ include_directives: @include_directives,
309
+ fields: @fields
308
310
  )
309
311
  end
310
312
 
311
313
  def add_show_relationship_operation(relationship_type, parent_key)
312
- @operations.push JSONAPI::ShowRelationshipOperation.new(
314
+ @operations.push JSONAPI::Operation.new(:show_relationship,
313
315
  @resource_klass,
314
316
  context: @context,
315
317
  relationship_type: relationship_type,
@@ -318,17 +320,18 @@ module JSONAPI
318
320
  end
319
321
 
320
322
  def add_show_related_resource_operation(relationship_type)
321
- @operations.push JSONAPI::ShowRelatedResourceOperation.new(
323
+ @operations.push JSONAPI::Operation.new(:show_related_resource,
322
324
  @resource_klass,
323
325
  context: @context,
324
326
  relationship_type: relationship_type,
325
327
  source_klass: @source_klass,
326
- source_id: @source_id
328
+ source_id: @source_id,
329
+ fields: @fields
327
330
  )
328
331
  end
329
332
 
330
333
  def add_show_related_resources_operation(relationship_type)
331
- @operations.push JSONAPI::ShowRelatedResourcesOperation.new(
334
+ @operations.push JSONAPI::Operation.new(:show_related_resources,
332
335
  @resource_klass,
333
336
  context: @context,
334
337
  relationship_type: relationship_type,
@@ -336,7 +339,8 @@ module JSONAPI
336
339
  source_id: @source_id,
337
340
  filters: @source_klass.verify_filters(@filters, @context),
338
341
  sort_criteria: @sort_criteria,
339
- paginator: @paginator
342
+ paginator: @paginator,
343
+ fields: @fields
340
344
  )
341
345
  end
342
346
 
@@ -356,10 +360,11 @@ module JSONAPI
356
360
  verify_type(params[:type])
357
361
 
358
362
  data = parse_params(params, creatable_fields)
359
- @operations.push JSONAPI::CreateResourceOperation.new(
363
+ @operations.push JSONAPI::Operation.new(:create_resource,
360
364
  @resource_klass,
361
365
  context: @context,
362
- data: data
366
+ data: data,
367
+ fields: @fields
363
368
  )
364
369
  end
365
370
  rescue JSONAPI::Exceptions::Error => e
@@ -382,7 +387,8 @@ module JSONAPI
382
387
  }
383
388
  end
384
389
 
385
- if !raw.is_a?(Hash) || raw.length != 2 || !(raw.key?('type') && raw.key?('id'))
390
+ if !(raw.is_a?(Hash) || raw.is_a?(ActionController::Parameters)) ||
391
+ raw.keys.length != 2 || !(raw.key?('type') && raw.key?('id'))
386
392
  fail JSONAPI::Exceptions::InvalidLinksObject.new
387
393
  end
388
394
 
@@ -476,7 +482,7 @@ module JSONAPI
476
482
  def parse_to_many_relationship(link_value, relationship, &add_result)
477
483
  if link_value.is_a?(Array) && link_value.length == 0
478
484
  linkage = []
479
- elsif link_value.is_a?(Hash)
485
+ elsif (link_value.is_a?(Hash) || link_value.is_a?(ActionController::Parameters))
480
486
  linkage = link_value[:data]
481
487
  else
482
488
  fail JSONAPI::Exceptions::InvalidLinksObject.new
@@ -531,7 +537,14 @@ module JSONAPI
531
537
  end
532
538
  end
533
539
  end
534
- when 'type', 'id'
540
+ when 'type'
541
+ when 'id'
542
+ unless formatted_allowed_fields.include?(:id)
543
+ params_not_allowed.push(:id)
544
+ unless JSONAPI.configuration.raise_if_parameters_not_allowed
545
+ params.delete :id
546
+ end
547
+ end
535
548
  else
536
549
  params_not_allowed.push(key)
537
550
  end
@@ -564,7 +577,7 @@ module JSONAPI
564
577
 
565
578
  def parse_add_relationship_operation(verified_params, relationship, parent_key)
566
579
  if relationship.is_a?(JSONAPI::Relationship::ToMany)
567
- @operations.push JSONAPI::CreateToManyRelationshipOperation.new(
580
+ @operations.push JSONAPI::Operation.new(:create_to_many_relationship,
568
581
  resource_klass,
569
582
  context: @context,
570
583
  resource_id: parent_key,
@@ -575,40 +588,37 @@ module JSONAPI
575
588
  end
576
589
 
577
590
  def parse_update_relationship_operation(verified_params, relationship, parent_key)
578
- operation_args = [resource_klass].push(
591
+ options = {
579
592
  context: @context,
580
593
  resource_id: parent_key,
581
594
  relationship_type: relationship.name
582
- )
595
+ }
583
596
 
584
597
  if relationship.is_a?(JSONAPI::Relationship::ToOne)
585
598
  if relationship.polymorphic?
586
- operation_args[1].merge!(
587
- key_value: verified_params[:to_one].values[0][:id],
588
- key_type: verified_params[:to_one].values[0][:type]
589
- )
599
+ options[:key_value] = verified_params[:to_one].values[0][:id]
600
+ options[:key_type] = verified_params[:to_one].values[0][:type]
590
601
 
591
- operation_klass = JSONAPI::ReplacePolymorphicToOneRelationshipOperation
602
+ operation_type = :replace_polymorphic_to_one_relationship
592
603
  else
593
- operation_args[1].merge!(key_value: verified_params[:to_one].values[0])
594
- operation_klass = JSONAPI::ReplaceToOneRelationshipOperation
604
+ options[:key_value] = verified_params[:to_one].values[0]
605
+ operation_type = :replace_to_one_relationship
595
606
  end
596
607
  elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
597
608
  unless relationship.acts_as_set
598
609
  fail JSONAPI::Exceptions::ToManySetReplacementForbidden.new
599
610
  end
600
-
601
- operation_args[1].merge!(data: verified_params[:to_many].values[0])
602
- operation_klass = JSONAPI::ReplaceToManyRelationshipOperation
611
+ options[:data] = verified_params[:to_many].values[0]
612
+ operation_type = :replace_to_many_relationship
603
613
  end
604
614
 
605
- @operations.push(operation_klass.send(:new, *operation_args))
615
+ @operations.push JSONAPI::Operation.new(operation_type, resource_klass, options)
606
616
  end
607
617
 
608
618
  def parse_single_replace_operation(data, keys, id_key_presence_check_required: true)
609
619
  fail JSONAPI::Exceptions::MissingKey.new if data[:id].nil?
610
620
 
611
- key = data[:id]
621
+ key = data[:id].to_s
612
622
  if id_key_presence_check_required && !keys.include?(key)
613
623
  fail JSONAPI::Exceptions::KeyNotIncludedInURL.new(key)
614
624
  end
@@ -617,11 +627,12 @@ module JSONAPI
617
627
 
618
628
  verify_type(data[:type])
619
629
 
620
- @operations.push JSONAPI::ReplaceFieldsOperation.new(
630
+ @operations.push JSONAPI::Operation.new(:replace_fields,
621
631
  @resource_klass,
622
632
  context: @context,
623
633
  resource_id: key,
624
- data: parse_params(data, updatable_fields)
634
+ data: parse_params(data, updatable_fields),
635
+ fields: @fields
625
636
  )
626
637
  end
627
638
 
@@ -645,7 +656,7 @@ module JSONAPI
645
656
  keys = parse_key_array(params.require(:id))
646
657
 
647
658
  keys.each do |key|
648
- @operations.push JSONAPI::RemoveResourceOperation.new(
659
+ @operations.push JSONAPI::Operation.new(:remove_resource,
649
660
  @resource_klass,
650
661
  context: @context,
651
662
  resource_id: key
@@ -667,12 +678,12 @@ module JSONAPI
667
678
  keys.each do |key|
668
679
  operation_args = operation_base_args.dup
669
680
  operation_args[1] = operation_args[1].merge(associated_key: key)
670
- @operations.push JSONAPI::RemoveToManyRelationshipOperation.new(
681
+ @operations.push JSONAPI::Operation.new(:remove_to_many_relationship,
671
682
  *operation_args
672
683
  )
673
684
  end
674
685
  else
675
- @operations.push JSONAPI::RemoveToOneRelationshipOperation.new(
686
+ @operations.push JSONAPI::Operation.new(:remove_to_one_relationship,
676
687
  *operation_base_args
677
688
  )
678
689
  end
@@ -152,6 +152,15 @@ module JSONAPI
152
152
  {}
153
153
  end
154
154
 
155
+ # Override this to return custom links
156
+ # must return a hash, which will be merged with the default { self: 'self-url' } links hash
157
+ # links keys will be not be formatted with the key formatter for the serializer by default.
158
+ # They can however use the serializer's format_key and format_value methods if desired
159
+ # the _options hash will contain the serializer and the serialization_options
160
+ def custom_links(_options)
161
+ {}
162
+ end
163
+
155
164
  private
156
165
 
157
166
  def save
@@ -173,8 +182,8 @@ module JSONAPI
173
182
  # return :accepted
174
183
  # end
175
184
  # ```
176
- def _save
177
- unless @model.valid?
185
+ def _save(validation_context = nil)
186
+ unless @model.valid?(validation_context)
178
187
  fail JSONAPI::Exceptions::ValidationErrors.new(self)
179
188
  end
180
189
 
@@ -201,6 +210,9 @@ module JSONAPI
201
210
  fail JSONAPI::Exceptions::ValidationErrors.new(self)
202
211
  end
203
212
  :completed
213
+
214
+ rescue ActiveRecord::DeleteRestrictionError => e
215
+ fail JSONAPI::Exceptions::RecordLocked.new(e.message)
204
216
  end
205
217
 
206
218
  def _create_to_many_links(relationship_type, relationship_key_values)
@@ -256,6 +268,11 @@ module JSONAPI
256
268
  @model.public_send(relation_name).delete(key)
257
269
 
258
270
  :completed
271
+
272
+ rescue ActiveRecord::DeleteRestrictionError => e
273
+ fail JSONAPI::Exceptions::RecordLocked.new(e.message)
274
+ rescue ActiveRecord::RecordNotFound
275
+ fail JSONAPI::Exceptions::RecordNotFound.new(key)
259
276
  end
260
277
 
261
278
  def _remove_to_one_link(relationship_type)
@@ -390,11 +407,11 @@ module JSONAPI
390
407
  @_attributes ||= {}
391
408
  @_attributes[attr] = options
392
409
  define_method attr do
393
- @model.public_send(attr)
410
+ @model.public_send(options[:delegate] ? options[:delegate].to_sym : attr)
394
411
  end unless method_defined?(attr)
395
412
 
396
413
  define_method "#{attr}=" do |value|
397
- @model.public_send "#{attr}=", value
414
+ @model.public_send("#{options[:delegate] ? options[:delegate].to_sym : attr}=", value)
398
415
  end unless method_defined?("#{attr}=")
399
416
  end
400
417
 
@@ -521,17 +538,57 @@ module JSONAPI
521
538
 
522
539
  def apply_sort(records, order_options, _context = {})
523
540
  if order_options.any?
524
- records.order(order_options)
525
- else
526
- records
541
+ order_options.each_pair do |field, direction|
542
+ if field.to_s.include?(".")
543
+ *model_names, column_name = field.split(".")
544
+
545
+ associations = _lookup_association_chain([records.model.to_s, *model_names])
546
+ joins_query = _build_joins([records.model, *associations])
547
+
548
+ # _sorting is appended to avoid name clashes with manual joins eg. overriden filters
549
+ order_by_query = "#{associations.last.name}_sorting.#{column_name} #{direction}"
550
+ records = records.joins(joins_query).order(order_by_query)
551
+ else
552
+ records = records.order(field => direction)
553
+ end
554
+ end
555
+ end
556
+
557
+ records
558
+ end
559
+
560
+ def _lookup_association_chain(model_names)
561
+ associations = []
562
+ model_names.inject do |prev, current|
563
+ association = prev.classify.constantize.reflect_on_all_associations.detect do |assoc|
564
+ assoc.name.to_s.downcase == current.downcase
565
+ end
566
+ associations << association
567
+ association.class_name
568
+ end
569
+
570
+ associations
571
+ end
572
+
573
+ def _build_joins(associations)
574
+ joins = []
575
+
576
+ associations.inject do |prev, current|
577
+ joins << "LEFT JOIN #{current.table_name} AS #{current.name}_sorting ON #{current.name}_sorting.id = #{prev.table_name}.#{current.foreign_key}"
578
+ current
527
579
  end
580
+ joins.join("\n")
528
581
  end
529
582
 
530
583
  def apply_filter(records, filter, value, options = {})
531
584
  strategy = _allowed_filters.fetch(filter.to_sym, Hash.new)[:apply]
532
585
 
533
586
  if strategy
534
- strategy.call(records, value, options)
587
+ if strategy.is_a?(Symbol) || strategy.is_a?(String)
588
+ send(strategy, records, value, options)
589
+ else
590
+ strategy.call(records, value, options)
591
+ end
535
592
  else
536
593
  records.where(filter => value)
537
594
  end
@@ -571,8 +628,13 @@ module JSONAPI
571
628
  apply_sort(records, order_options, context)
572
629
  end
573
630
 
631
+ # Assumes ActiveRecord's counting. Override if you need a different counting method
632
+ def count_records(records)
633
+ records.count(:all)
634
+ end
635
+
574
636
  def find_count(filters, options = {})
575
- filter_records(filters, options).count(:all)
637
+ count_records(filter_records(filters, options))
576
638
  end
577
639
 
578
640
  # Override this method if you have more complex requirements than this basic find method provides
@@ -587,9 +649,15 @@ module JSONAPI
587
649
 
588
650
  records = apply_pagination(records, options[:paginator], order_options)
589
651
 
652
+ resources_for(records, context)
653
+ end
654
+
655
+ def resources_for(records, context)
590
656
  resources = []
657
+ resource_classes = {}
591
658
  records.each do |model|
592
- resources.push self.resource_for_model(model).new(model, context)
659
+ resource_class = resource_classes[model.class] ||= self.resource_for_model(model)
660
+ resources.push resource_class.new(model, context)
593
661
  end
594
662
 
595
663
  resources
@@ -625,12 +693,19 @@ module JSONAPI
625
693
 
626
694
  def verify_filter(filter, raw, context = nil)
627
695
  filter_values = []
628
- filter_values += CSV.parse_line(raw) unless raw.nil? || raw.empty?
696
+ if raw.present?
697
+ filter_values += raw.is_a?(String) ? CSV.parse_line(raw) : [raw]
698
+ end
629
699
 
630
700
  strategy = _allowed_filters.fetch(filter, Hash.new)[:verify]
631
701
 
632
702
  if strategy
633
- [filter, strategy.call(filter_values, context)]
703
+ if strategy.is_a?(Symbol) || strategy.is_a?(String)
704
+ values = send(strategy, filter_values, context)
705
+ else
706
+ values = strategy.call(filter_values, context)
707
+ end
708
+ [filter, values]
634
709
  else
635
710
  if is_filter_relationship?(filter)
636
711
  verify_relationship_filter(filter, filter_values, context)
@@ -645,7 +720,7 @@ module JSONAPI
645
720
  end
646
721
 
647
722
  def resource_key_type
648
- @_resource_key_type || JSONAPI.configuration.resource_key_type
723
+ @_resource_key_type ||= JSONAPI.configuration.resource_key_type
649
724
  end
650
725
 
651
726
  def verify_key(key, context = nil)
@@ -1,17 +1,5 @@
1
1
  module JSONAPI
2
- class ResourceController < ActionController::Metal
3
- MODULES = [
4
- AbstractController::Rendering,
5
- ActionController::Rendering,
6
- ActionController::Renderers::All,
7
- ActionController::StrongParameters,
8
- ActionController::ForceSSL,
9
- ActionController::Instrumentation,
10
- JSONAPI::ActsAsResourceController
11
- ].freeze
12
-
13
- MODULES.each do |mod|
14
- include mod
15
- end
2
+ class ResourceController < ActionController::Base
3
+ include JSONAPI::ActsAsResourceController
16
4
  end
17
5
  end
@@ -0,0 +1,17 @@
1
+ module JSONAPI
2
+ class ResourceControllerMetal < ActionController::Metal
3
+ MODULES = [
4
+ AbstractController::Rendering,
5
+ ActionController::Rendering,
6
+ ActionController::Renderers::All,
7
+ ActionController::StrongParameters,
8
+ ActionController::ForceSSL,
9
+ ActionController::Instrumentation,
10
+ JSONAPI::ActsAsResourceController
11
+ ].freeze
12
+
13
+ MODULES.each do |mod|
14
+ include mod
15
+ end
16
+ end
17
+ end
@@ -12,7 +12,7 @@ module JSONAPI
12
12
  # Purpose: determines which fields are serialized for a resource type. This encompasses both attributes and
13
13
  # relationship ids in the links section for a resource. Fields are global for a resource type.
14
14
  # Example: { people: [:id, :email, :comments], posts: [:id, :title, :author], comments: [:id, :body, :post]}
15
- # key_formatter: KeyFormatter class to override the default configuration
15
+ # key_formatter: KeyFormatter instance to override the default configuration
16
16
  # serializer_options: additional options that will be passed to resource meta and links lambdas
17
17
 
18
18
  def initialize(primary_resource_klass, options = {})
@@ -21,16 +21,23 @@ module JSONAPI
21
21
  @include = options.fetch(:include, [])
22
22
  @include_directives = options[:include_directives]
23
23
  @key_formatter = options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
24
+ @id_formatter = ValueFormatter.value_formatter_for(:id)
24
25
  @link_builder = generate_link_builder(primary_resource_klass, options)
25
26
  @always_include_to_one_linkage_data = options.fetch(:always_include_to_one_linkage_data,
26
27
  JSONAPI.configuration.always_include_to_one_linkage_data)
27
28
  @always_include_to_many_linkage_data = options.fetch(:always_include_to_many_linkage_data,
28
29
  JSONAPI.configuration.always_include_to_many_linkage_data)
29
30
  @serialization_options = options.fetch(:serialization_options, {})
31
+
32
+ # Warning: This makes ResourceSerializer non-thread-safe. That's not a problem with the
33
+ # request-specific way it's currently used, though.
34
+ @value_formatter_type_cache = NaiveCache.new{|arg| ValueFormatter.value_formatter_for(arg) }
30
35
  end
31
36
 
32
37
  # Converts a single resource, or an array of resources to a hash, conforming to the JSONAPI structure
33
38
  def serialize_to_hash(source)
39
+ @top_level_sources = Set.new([source].flatten.compact.map {|s| top_level_source_key(s) })
40
+
34
41
  is_resource_collection = source.respond_to?(:to_ary)
35
42
 
36
43
  @included_objects = {}
@@ -81,8 +88,7 @@ module JSONAPI
81
88
  end
82
89
 
83
90
  def format_value(value, format)
84
- value_formatter = JSONAPI::ValueFormatter.value_formatter_for(format)
85
- value_formatter.format(value)
91
+ @value_formatter_type_cache.get(format).format(value)
86
92
  end
87
93
 
88
94
  private
@@ -114,19 +120,18 @@ module JSONAPI
114
120
 
115
121
  obj_hash['type'] = format_key(source.class._type.to_s)
116
122
 
117
- links = relationship_links(source)
123
+ links = links_hash(source)
118
124
  obj_hash['links'] = links unless links.empty?
119
125
 
120
- attributes = attribute_hash(source)
126
+ attributes = attributes_hash(source)
121
127
  obj_hash['attributes'] = attributes unless attributes.empty?
122
128
 
123
- relationships = relationship_data(source, include_directives)
129
+ relationships = relationships_hash(source, include_directives)
124
130
  obj_hash['relationships'] = relationships unless relationships.nil? || relationships.empty?
125
131
 
126
- meta = source.meta(custom_generation_options)
127
- if meta.is_a?(Hash) && !meta.empty?
128
- obj_hash['meta'] = meta
129
- end
132
+ meta = meta_hash(source)
133
+ obj_hash['meta'] = meta unless meta.empty?
134
+
130
135
  obj_hash
131
136
  end
132
137
 
@@ -139,7 +144,7 @@ module JSONAPI
139
144
  end
140
145
  end
141
146
 
142
- def attribute_hash(source)
147
+ def attributes_hash(source)
143
148
  requested = requested_fields(source.class)
144
149
  fields = source.fetchable_fields & source.class._attributes.keys.to_a
145
150
  fields = requested & fields unless requested.nil?
@@ -159,7 +164,31 @@ module JSONAPI
159
164
  }
160
165
  end
161
166
 
162
- def relationship_data(source, include_directives)
167
+ def meta_hash(source)
168
+ meta = source.meta(custom_generation_options)
169
+ (meta.is_a?(Hash) && meta) || {}
170
+ end
171
+
172
+ def links_hash(source)
173
+ {
174
+ self: link_builder.self_link(source)
175
+ }.merge(custom_links_hash(source)).compact
176
+ end
177
+
178
+ def custom_links_hash(source)
179
+ custom_links = source.custom_links(custom_generation_options)
180
+ (custom_links.is_a?(Hash) && custom_links) || {}
181
+ end
182
+
183
+ def top_level_source_key(source)
184
+ "#{source.class}_#{source.id}"
185
+ end
186
+
187
+ def self_referential_and_already_in_source(resource)
188
+ resource && @top_level_sources.include?(top_level_source_key(resource))
189
+ end
190
+
191
+ def relationships_hash(source, include_directives)
163
192
  relationships = source.class._relationships
164
193
  requested = requested_fields(source.class)
165
194
  fields = relationships.keys
@@ -177,39 +206,24 @@ module JSONAPI
177
206
 
178
207
  include_linkage = ia && ia[:include]
179
208
  include_linked_children = ia && !ia[:include_related].empty?
209
+ resources = (include_linkage || include_linked_children) && [source.public_send(name)].flatten.compact
180
210
 
181
211
  if field_set.include?(name)
182
212
  hash[format_key(name)] = link_object(source, relationship, include_linkage)
183
213
  end
184
214
 
185
- type = relationship.type
186
-
187
215
  # If the object has been serialized once it will be in the related objects list,
188
216
  # but it's possible all children won't have been captured. So we must still go
189
217
  # through the relationships.
190
218
  if include_linkage || include_linked_children
191
- if relationship.is_a?(JSONAPI::Relationship::ToOne)
192
- resource = source.public_send(name)
193
- if resource
194
- id = resource.id
195
- type = relationship.type_for_source(source)
196
- relationships_only = already_serialized?(type, id)
197
- if include_linkage && !relationships_only
198
- add_included_object(id, object_hash(resource, ia))
199
- elsif include_linked_children || relationships_only
200
- relationship_data(resource, ia)
201
- end
202
- end
203
- elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
204
- resources = source.public_send(name)
205
- resources.each do |resource|
206
- id = resource.id
207
- relationships_only = already_serialized?(type, id)
208
- if include_linkage && !relationships_only
209
- add_included_object(id, object_hash(resource, ia))
210
- elsif include_linked_children || relationships_only
211
- relationship_data(resource, ia)
212
- end
219
+ resources.each do |resource|
220
+ next if self_referential_and_already_in_source(resource)
221
+ id = resource.id
222
+ relationships_only = already_serialized?(relationship.type, id)
223
+ if include_linkage && !relationships_only
224
+ add_included_object(id, object_hash(resource, ia))
225
+ elsif include_linked_children || relationships_only
226
+ relationships_hash(resource, ia)
213
227
  end
214
228
  end
215
229
  end
@@ -217,13 +231,6 @@ module JSONAPI
217
231
  end
218
232
  end
219
233
 
220
- def relationship_links(source)
221
- links = {}
222
- links[:self] = link_builder.self_link(source)
223
-
224
- links
225
- end
226
-
227
234
  def already_serialized?(type, id)
228
235
  type = format_key(type)
229
236
  @included_objects.key?(type) && @included_objects[type].key?(id)
@@ -292,18 +299,26 @@ module JSONAPI
292
299
  def foreign_key_value(source, relationship)
293
300
  foreign_key = relationship.foreign_key
294
301
  value = source.public_send(foreign_key)
295
- IdValueFormatter.format(value)
302
+ @id_formatter.format(value)
296
303
  end
297
304
 
298
305
  def foreign_key_types_and_values(source, relationship)
299
306
  if relationship.is_a?(JSONAPI::Relationship::ToMany)
300
307
  if relationship.polymorphic?
301
- source._model.public_send(relationship.name).pluck(:type, :id).map do |type, id|
302
- [type.pluralize, IdValueFormatter.format(id)]
308
+ assoc = source._model.public_send(relationship.name)
309
+ # Avoid hitting the database again for values already pre-loaded
310
+ if assoc.respond_to?(:loaded?) and assoc.loaded?
311
+ assoc.map do |obj|
312
+ [obj.type.underscore.pluralize, @id_formatter.format(obj.id)]
313
+ end
314
+ else
315
+ assoc.pluck(:type, :id).map do |type, id|
316
+ [type.underscore.pluralize, @id_formatter.format(id)]
317
+ end
303
318
  end
304
319
  else
305
320
  source.public_send(relationship.foreign_key).map do |value|
306
- [relationship.type, IdValueFormatter.format(value)]
321
+ [relationship.type, @id_formatter.format(value)]
307
322
  end
308
323
  end
309
324
  end