jsonapi-resources 0.7.1.beta1 → 0.7.1.beta2

Sign up to get free protection for your applications and to get access to all the features.
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