active_model_serializers 0.10.2 → 0.10.3

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +8 -1
  3. data/CHANGELOG.md +45 -3
  4. data/CODE_OF_CONDUCT.md +74 -0
  5. data/Gemfile +4 -1
  6. data/README.md +5 -2
  7. data/active_model_serializers.gemspec +2 -2
  8. data/docs/ARCHITECTURE.md +6 -7
  9. data/docs/README.md +2 -0
  10. data/docs/general/caching.md +7 -1
  11. data/docs/general/configuration_options.md +64 -0
  12. data/docs/general/rendering.md +35 -1
  13. data/docs/general/serializers.md +35 -5
  14. data/docs/howto/add_pagination_links.md +2 -2
  15. data/docs/howto/add_relationship_links.md +137 -0
  16. data/docs/howto/add_root_key.md +4 -0
  17. data/docs/howto/grape_integration.md +42 -0
  18. data/docs/howto/outside_controller_use.md +9 -2
  19. data/docs/howto/passing_arbitrary_options.md +2 -2
  20. data/docs/howto/test.md +2 -0
  21. data/docs/howto/upgrade_from_0_8_to_0_10.md +265 -0
  22. data/docs/integrations/ember-and-json-api.md +41 -24
  23. data/lib/action_controller/serialization.rb +9 -0
  24. data/lib/active_model/serializer.rb +19 -22
  25. data/lib/active_model/serializer/association.rb +19 -4
  26. data/lib/active_model/serializer/collection_serializer.rb +8 -5
  27. data/lib/active_model/serializer/{associations.rb → concerns/associations.rb} +8 -5
  28. data/lib/active_model/serializer/{attributes.rb → concerns/attributes.rb} +0 -0
  29. data/lib/active_model/serializer/{caching.rb → concerns/caching.rb} +5 -1
  30. data/lib/active_model/serializer/{configuration.rb → concerns/configuration.rb} +24 -1
  31. data/lib/active_model/serializer/{links.rb → concerns/links.rb} +0 -0
  32. data/lib/active_model/serializer/{meta.rb → concerns/meta.rb} +0 -0
  33. data/lib/active_model/serializer/{type.rb → concerns/type.rb} +0 -0
  34. data/lib/active_model/serializer/reflection.rb +37 -21
  35. data/lib/active_model/serializer/version.rb +1 -1
  36. data/lib/active_model_serializers.rb +1 -0
  37. data/lib/active_model_serializers/adapter/attributes.rb +3 -1
  38. data/lib/active_model_serializers/adapter/json_api.rb +15 -22
  39. data/lib/active_model_serializers/adapter/json_api/relationship.rb +30 -19
  40. data/lib/active_model_serializers/adapter/json_api/resource_identifier.rb +23 -9
  41. data/lib/active_model_serializers/key_transform.rb +4 -0
  42. data/lib/active_model_serializers/lookup_chain.rb +80 -0
  43. data/lib/active_model_serializers/model.rb +1 -1
  44. data/lib/active_model_serializers/serializable_resource.rb +6 -5
  45. data/lib/generators/rails/serializer_generator.rb +1 -1
  46. data/test/action_controller/json_api/fields_test.rb +57 -0
  47. data/test/action_controller/json_api/transform_test.rb +3 -3
  48. data/test/action_controller/lookup_proc_test.rb +49 -0
  49. data/test/action_controller/namespace_lookup_test.rb +226 -0
  50. data/test/active_model_serializers/key_transform_test.rb +32 -0
  51. data/test/active_model_serializers/model_test.rb +11 -0
  52. data/test/adapter/attributes_test.rb +43 -0
  53. data/test/adapter/json/transform_test.rb +1 -1
  54. data/test/adapter/json_api/fields_test.rb +4 -3
  55. data/test/adapter/json_api/has_many_test.rb +21 -0
  56. data/test/adapter/json_api/include_data_if_sideloaded_test.rb +166 -0
  57. data/test/adapter/json_api/linked_test.rb +24 -6
  58. data/test/adapter/json_api/links_test.rb +1 -1
  59. data/test/adapter/json_api/pagination_links_test.rb +17 -2
  60. data/test/adapter/json_api/relationship_test.rb +309 -73
  61. data/test/adapter/json_api/resource_identifier_test.rb +20 -0
  62. data/test/adapter/json_api/transform_test.rb +4 -3
  63. data/test/benchmark/benchmarking_support.rb +1 -1
  64. data/test/benchmark/bm_active_record.rb +81 -0
  65. data/test/benchmark/bm_adapter.rb +38 -0
  66. data/test/benchmark/bm_caching.rb +1 -1
  67. data/test/benchmark/bm_lookup_chain.rb +83 -0
  68. data/test/cache_test.rb +42 -4
  69. data/test/collection_serializer_test.rb +1 -1
  70. data/test/fixtures/poro.rb +44 -41
  71. data/test/generators/serializer_generator_test.rb +22 -5
  72. data/test/serializers/association_macros_test.rb +3 -2
  73. data/test/serializers/associations_test.rb +97 -22
  74. data/test/serializers/attribute_test.rb +1 -1
  75. data/test/serializers/serializer_for_test.rb +3 -3
  76. data/test/serializers/serializer_for_with_namespace_test.rb +87 -0
  77. data/test/support/serialization_testing.rb +16 -0
  78. data/test/test_helper.rb +1 -0
  79. metadata +35 -14
  80. data/test/adapter/json_api/relationships_test.rb +0 -204
@@ -1,5 +1,5 @@
1
1
  module ActiveModel
2
2
  class Serializer
3
- VERSION = '0.10.2'.freeze
3
+ VERSION = '0.10.3'.freeze
4
4
  end
5
5
  end
@@ -14,6 +14,7 @@ module ActiveModelSerializers
14
14
  autoload :Adapter
15
15
  autoload :JsonPointer
16
16
  autoload :Deprecate
17
+ autoload :LookupChain
17
18
 
18
19
  class << self; attr_accessor :logger; end
19
20
  self.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
@@ -4,7 +4,9 @@ module ActiveModelSerializers
4
4
  def serializable_hash(options = nil)
5
5
  options = serialization_options(options)
6
6
  options[:fields] ||= instance_options[:fields]
7
- serializer.serializable_hash(instance_options, options, self)
7
+ serialized_hash = serializer.serializable_hash(instance_options, options, self)
8
+
9
+ self.class.transform_key_casing!(serialized_hash, instance_options)
8
10
  end
9
11
  end
10
12
  end
@@ -235,17 +235,17 @@ module ActiveModelSerializers
235
235
  @primary = []
236
236
  @included = []
237
237
  @resource_identifiers = Set.new
238
- serializers.each { |serializer| process_resource(serializer, true) }
238
+ serializers.each { |serializer| process_resource(serializer, true, @include_directive) }
239
239
  serializers.each { |serializer| process_relationships(serializer, @include_directive) }
240
240
 
241
241
  [@primary, @included]
242
242
  end
243
243
 
244
- def process_resource(serializer, primary)
244
+ def process_resource(serializer, primary, include_slice = {})
245
245
  resource_identifier = ResourceIdentifier.new(serializer, instance_options).as_json
246
246
  return false unless @resource_identifiers.add?(resource_identifier)
247
247
 
248
- resource_object = resource_object_for(serializer)
248
+ resource_object = resource_object_for(serializer, include_slice)
249
249
  if primary
250
250
  @primary << resource_object
251
251
  else
@@ -255,21 +255,21 @@ module ActiveModelSerializers
255
255
  true
256
256
  end
257
257
 
258
- def process_relationships(serializer, include_directive)
259
- serializer.associations(include_directive).each do |association|
260
- process_relationship(association.serializer, include_directive[association.key])
258
+ def process_relationships(serializer, include_slice)
259
+ serializer.associations(include_slice).each do |association|
260
+ process_relationship(association.serializer, include_slice[association.key])
261
261
  end
262
262
  end
263
263
 
264
- def process_relationship(serializer, include_directive)
264
+ def process_relationship(serializer, include_slice)
265
265
  if serializer.respond_to?(:each)
266
- serializer.each { |s| process_relationship(s, include_directive) }
266
+ serializer.each { |s| process_relationship(s, include_slice) }
267
267
  return
268
268
  end
269
269
  return unless serializer && serializer.object
270
- return unless process_resource(serializer, false)
270
+ return unless process_resource(serializer, false, include_slice)
271
271
 
272
- process_relationships(serializer, include_directive)
272
+ process_relationships(serializer, include_slice)
273
273
  end
274
274
 
275
275
  # {http://jsonapi.org/format/#document-resource-object-attributes Document Resource Object Attributes}
@@ -293,7 +293,7 @@ module ActiveModelSerializers
293
293
  end
294
294
 
295
295
  # {http://jsonapi.org/format/#document-resource-objects Document Resource Objects}
296
- def resource_object_for(serializer)
296
+ def resource_object_for(serializer, include_slice = {})
297
297
  resource_object = serializer.fetch(self) do
298
298
  resource_object = ResourceIdentifier.new(serializer, instance_options).as_json
299
299
 
@@ -304,7 +304,7 @@ module ActiveModelSerializers
304
304
  end
305
305
 
306
306
  requested_associations = fieldset.fields_for(resource_object[:type]) || '*'
307
- relationships = relationships_for(serializer, requested_associations)
307
+ relationships = relationships_for(serializer, requested_associations, include_slice)
308
308
  resource_object[:relationships] = relationships if relationships.any?
309
309
 
310
310
  links = links_for(serializer)
@@ -432,20 +432,13 @@ module ActiveModelSerializers
432
432
  # id: 'required-id',
433
433
  # meta: meta
434
434
  # }.reject! {|_,v| v.nil? }
435
- def relationships_for(serializer, requested_associations)
435
+ def relationships_for(serializer, requested_associations, include_slice)
436
436
  include_directive = JSONAPI::IncludeDirective.new(
437
437
  requested_associations,
438
438
  allow_wildcard: true
439
439
  )
440
- serializer.associations(include_directive).each_with_object({}) do |association, hash|
441
- hash[association.key] = Relationship.new(
442
- serializer,
443
- association.serializer,
444
- instance_options,
445
- options: association.options,
446
- links: association.links,
447
- meta: association.meta
448
- ).as_json
440
+ serializer.associations(include_directive, include_slice).each_with_object({}) do |association, hash|
441
+ hash[association.key] = Relationship.new(serializer, instance_options, association).as_json
449
442
  end
450
443
  end
451
444
 
@@ -5,47 +5,58 @@ module ActiveModelSerializers
5
5
  # {http://jsonapi.org/format/#document-resource-object-related-resource-links Document Resource Object Related Resource Links}
6
6
  # {http://jsonapi.org/format/#document-links Document Links}
7
7
  # {http://jsonapi.org/format/#document-resource-object-linkage Document Resource Relationship Linkage}
8
- # {http://jsonapi.org/format/#document-meta Docment Meta}
9
- def initialize(parent_serializer, serializer, serializable_resource_options, args = {})
10
- @object = parent_serializer.object
11
- @scope = parent_serializer.scope
12
- @association_options = args.fetch(:options, {})
8
+ # {http://jsonapi.org/format/#document-meta Document Meta}
9
+ def initialize(parent_serializer, serializable_resource_options, association)
10
+ @parent_serializer = parent_serializer
11
+ @association = association
13
12
  @serializable_resource_options = serializable_resource_options
14
- @data = data_for(serializer)
15
- @links = args.fetch(:links, {}).each_with_object({}) do |(key, value), hash|
16
- hash[key] = ActiveModelSerializers::Adapter::JsonApi::Link.new(parent_serializer, value).as_json
17
- end
18
- meta = args.fetch(:meta, nil)
19
- @meta = meta.respond_to?(:call) ? parent_serializer.instance_eval(&meta) : meta
20
13
  end
21
14
 
22
15
  def as_json
23
16
  hash = {}
24
- hash[:data] = data if association_options[:include_data]
25
- links = self.links
17
+
18
+ if association.options[:include_data]
19
+ hash[:data] = data_for(association)
20
+ end
21
+
22
+ links = links_for(association)
26
23
  hash[:links] = links if links.any?
27
- meta = self.meta
24
+
25
+ meta = meta_for(association)
28
26
  hash[:meta] = meta if meta
27
+ hash[:meta] = {} if hash.empty?
29
28
 
30
29
  hash
31
30
  end
32
31
 
33
32
  protected
34
33
 
35
- attr_reader :object, :scope, :data, :serializable_resource_options,
36
- :association_options, :links, :meta
34
+ attr_reader :parent_serializer, :serializable_resource_options, :association
37
35
 
38
36
  private
39
37
 
40
- def data_for(serializer)
38
+ def data_for(association)
39
+ serializer = association.serializer
41
40
  if serializer.respond_to?(:each)
42
41
  serializer.map { |s| ResourceIdentifier.new(s, serializable_resource_options).as_json }
43
- elsif association_options[:virtual_value]
44
- association_options[:virtual_value]
42
+ elsif (virtual_value = association.options[:virtual_value])
43
+ virtual_value
45
44
  elsif serializer && serializer.object
46
45
  ResourceIdentifier.new(serializer, serializable_resource_options).as_json
47
46
  end
48
47
  end
48
+
49
+ def links_for(association)
50
+ association.links.each_with_object({}) do |(key, value), hash|
51
+ result = Link.new(parent_serializer, value).as_json
52
+ hash[key] = result if result
53
+ end
54
+ end
55
+
56
+ def meta_for(association)
57
+ meta = association.meta
58
+ meta.respond_to?(:call) ? parent_serializer.instance_eval(&meta) : meta
59
+ end
49
60
  end
50
61
  end
51
62
  end
@@ -2,11 +2,30 @@ module ActiveModelSerializers
2
2
  module Adapter
3
3
  class JsonApi
4
4
  class ResourceIdentifier
5
+ def self.type_for(class_name, serializer_type = nil, transform_options = {})
6
+ if serializer_type
7
+ raw_type = serializer_type
8
+ else
9
+ inflection =
10
+ if ActiveModelSerializers.config.jsonapi_resource_type == :singular
11
+ :singularize
12
+ else
13
+ :pluralize
14
+ end
15
+
16
+ raw_type = class_name.underscore
17
+ raw_type = ActiveSupport::Inflector.public_send(inflection, raw_type)
18
+ raw_type
19
+ .gsub!('/'.freeze, ActiveModelSerializers.config.jsonapi_namespace_separator)
20
+ raw_type
21
+ end
22
+ JsonApi.send(:transform_key_casing!, raw_type, transform_options)
23
+ end
24
+
5
25
  # {http://jsonapi.org/format/#document-resource-identifier-objects Resource Identifier Objects}
6
26
  def initialize(serializer, options)
7
27
  @id = id_for(serializer)
8
- @type = JsonApi.send(:transform_key_casing!, type_for(serializer),
9
- options)
28
+ @type = type_for(serializer, options)
10
29
  end
11
30
 
12
31
  def as_json
@@ -19,13 +38,8 @@ module ActiveModelSerializers
19
38
 
20
39
  private
21
40
 
22
- def type_for(serializer)
23
- return serializer._type if serializer._type
24
- if ActiveModelSerializers.config.jsonapi_resource_type == :singular
25
- serializer.object.class.model_name.singular
26
- else
27
- serializer.object.class.model_name.plural
28
- end
41
+ def type_for(serializer, transform_options)
42
+ self.class.type_for(serializer.object.class.name, serializer._type, transform_options)
29
43
  end
30
44
 
31
45
  def id_for(serializer)
@@ -11,6 +11,7 @@ module ActiveModelSerializers
11
11
  # @see {https://github.com/rails/rails/blob/master/activesupport/lib/active_support/inflector/methods.rb#L66-L76 ActiveSupport::Inflector.camelize}
12
12
  def camel(value)
13
13
  case value
14
+ when Array then value.map { |item| camel(item) }
14
15
  when Hash then value.deep_transform_keys! { |key| camel(key) }
15
16
  when Symbol then camel(value.to_s).to_sym
16
17
  when String then value.underscore.camelize
@@ -25,6 +26,7 @@ module ActiveModelSerializers
25
26
  # @see {https://github.com/rails/rails/blob/master/activesupport/lib/active_support/inflector/methods.rb#L66-L76 ActiveSupport::Inflector.camelize}
26
27
  def camel_lower(value)
27
28
  case value
29
+ when Array then value.map { |item| camel_lower(item) }
28
30
  when Hash then value.deep_transform_keys! { |key| camel_lower(key) }
29
31
  when Symbol then camel_lower(value.to_s).to_sym
30
32
  when String then value.underscore.camelize(:lower)
@@ -40,6 +42,7 @@ module ActiveModelSerializers
40
42
  # @see {https://github.com/rails/rails/blob/master/activesupport/lib/active_support/inflector/methods.rb#L185-L187 ActiveSupport::Inflector.dasherize}
41
43
  def dash(value)
42
44
  case value
45
+ when Array then value.map { |item| dash(item) }
43
46
  when Hash then value.deep_transform_keys! { |key| dash(key) }
44
47
  when Symbol then dash(value.to_s).to_sym
45
48
  when String then value.underscore.dasherize
@@ -55,6 +58,7 @@ module ActiveModelSerializers
55
58
  # @see {https://github.com/rails/rails/blob/master/activesupport/lib/active_support/inflector/methods.rb#L89-L98 ActiveSupport::Inflector.underscore}
56
59
  def underscore(value)
57
60
  case value
61
+ when Array then value.map { |item| underscore(item) }
58
62
  when Hash then value.deep_transform_keys! { |key| underscore(key) }
59
63
  when Symbol then underscore(value.to_s).to_sym
60
64
  when String then value.underscore
@@ -0,0 +1,80 @@
1
+ module ActiveModelSerializers
2
+ module LookupChain
3
+ # Standard appending of Serializer to the resource name.
4
+ #
5
+ # Example:
6
+ # Author => AuthorSerializer
7
+ BY_RESOURCE = lambda do |resource_class, _serializer_class, _namespace|
8
+ serializer_from(resource_class)
9
+ end
10
+
11
+ # Uses the namespace of the resource to find the serializer
12
+ #
13
+ # Example:
14
+ # British::Author => British::AuthorSerializer
15
+ BY_RESOURCE_NAMESPACE = lambda do |resource_class, _serializer_class, _namespace|
16
+ resource_namespace = namespace_for(resource_class)
17
+ serializer_name = serializer_from(resource_class)
18
+
19
+ "#{resource_namespace}::#{serializer_name}"
20
+ end
21
+
22
+ # Uses the controller namespace of the resource to find the serializer
23
+ #
24
+ # Example:
25
+ # Api::V3::AuthorsController => Api::V3::AuthorSerializer
26
+ BY_NAMESPACE = lambda do |resource_class, _serializer_class, namespace|
27
+ resource_name = resource_class_name(resource_class)
28
+ namespace ? "#{namespace}::#{resource_name}Serializer" : nil
29
+ end
30
+
31
+ # Allows for serializers to be defined in parent serializers
32
+ # - useful if a relationship only needs a different set of attributes
33
+ # than if it were rendered independently.
34
+ #
35
+ # Example:
36
+ # class BlogSerializer < ActiveModel::Serializer
37
+ # class AuthorSerialier < ActiveModel::Serializer
38
+ # ...
39
+ # end
40
+ #
41
+ # belongs_to :author
42
+ # ...
43
+ # end
44
+ #
45
+ # The belongs_to relationship would be rendered with
46
+ # BlogSerializer::AuthorSerialier
47
+ BY_PARENT_SERIALIZER = lambda do |resource_class, serializer_class, _namespace|
48
+ return if serializer_class == ActiveModel::Serializer
49
+
50
+ serializer_name = serializer_from(resource_class)
51
+ "#{serializer_class}::#{serializer_name}"
52
+ end
53
+
54
+ DEFAULT = [
55
+ BY_PARENT_SERIALIZER,
56
+ BY_NAMESPACE,
57
+ BY_RESOURCE_NAMESPACE,
58
+ BY_RESOURCE
59
+ ].freeze
60
+
61
+ module_function
62
+
63
+ def namespace_for(klass)
64
+ klass.name.deconstantize
65
+ end
66
+
67
+ def resource_class_name(klass)
68
+ klass.name.demodulize
69
+ end
70
+
71
+ def serializer_from_resource_name(name)
72
+ "#{name}Serializer"
73
+ end
74
+
75
+ def serializer_from(klass)
76
+ name = resource_class_name(klass)
77
+ serializer_from_resource_name(name)
78
+ end
79
+ end
80
+ end
@@ -9,7 +9,7 @@ module ActiveModelSerializers
9
9
  attr_reader :attributes, :errors
10
10
 
11
11
  def initialize(attributes = {})
12
- @attributes = attributes
12
+ @attributes = attributes && attributes.symbolize_keys
13
13
  @errors = ActiveModel::Errors.new(self)
14
14
  super
15
15
  end
@@ -38,9 +38,10 @@ module ActiveModelSerializers
38
38
 
39
39
  def find_adapter
40
40
  return resource unless serializer?
41
- ActiveModelSerializers::Adapter.create(serializer_instance, adapter_opts)
42
- rescue ActiveModel::Serializer::CollectionSerializer::NoSerializerError
43
- resource
41
+ adapter = catch :no_serializer do
42
+ ActiveModelSerializers::Adapter.create(serializer_instance, adapter_opts)
43
+ end
44
+ adapter || resource
44
45
  end
45
46
 
46
47
  def serializer_instance
@@ -49,12 +50,12 @@ module ActiveModelSerializers
49
50
 
50
51
  # Get serializer either explicitly :serializer or implicitly from resource
51
52
  # Remove :serializer key from serializer_opts
52
- # Replace :serializer key with :each_serializer if present
53
+ # Remove :each_serializer if present and set as :serializer key
53
54
  def serializer
54
55
  @serializer ||=
55
56
  begin
56
57
  @serializer = serializer_opts.delete(:serializer)
57
- @serializer ||= ActiveModel::Serializer.serializer_for(resource)
58
+ @serializer ||= ActiveModel::Serializer.serializer_for(resource, serializer_opts)
58
59
 
59
60
  if serializer_opts.key?(:each_serializer)
60
61
  serializer_opts[:serializer] = serializer_opts.delete(:each_serializer)
@@ -25,7 +25,7 @@ module Rails
25
25
  def parent_class_name
26
26
  if options[:parent]
27
27
  options[:parent]
28
- elsif defined?(::ApplicationSerializer)
28
+ elsif 'ApplicationSerializer'.safe_constantize
29
29
  'ApplicationSerializer'
30
30
  else
31
31
  'ActiveModel::Serializer'
@@ -0,0 +1,57 @@
1
+ require 'test_helper'
2
+
3
+ module ActionController
4
+ module Serialization
5
+ class JsonApi
6
+ class FieldsTest < ActionController::TestCase
7
+ class FieldsTestController < ActionController::Base
8
+ class PostSerializer < ActiveModel::Serializer
9
+ type 'posts'
10
+ attributes :title, :body, :publish_at
11
+ belongs_to :author
12
+ has_many :comments
13
+ end
14
+
15
+ def setup_post
16
+ ActionController::Base.cache_store.clear
17
+ @author = Author.new(id: 1, first_name: 'Bob', last_name: 'Jones')
18
+ @comment1 = Comment.new(id: 7, body: 'cool', author: @author)
19
+ @comment2 = Comment.new(id: 12, body: 'awesome', author: @author)
20
+ @post = Post.new(id: 1337, title: 'Title 1', body: 'Body 1',
21
+ author: @author, comments: [@comment1, @comment2],
22
+ publish_at: '2020-03-16T03:55:25.291Z')
23
+ @comment1.post = @post
24
+ @comment2.post = @post
25
+ end
26
+
27
+ def render_fields_works_on_relationships
28
+ setup_post
29
+ render json: @post, serializer: PostSerializer, adapter: :json_api, fields: { posts: [:author] }
30
+ end
31
+ end
32
+
33
+ tests FieldsTestController
34
+
35
+ test 'fields works on relationships' do
36
+ get :render_fields_works_on_relationships
37
+ response = JSON.parse(@response.body)
38
+ expected = {
39
+ 'data' => {
40
+ 'id' => '1337',
41
+ 'type' => 'posts',
42
+ 'relationships' => {
43
+ 'author' => {
44
+ 'data' => {
45
+ 'id' => '1',
46
+ 'type' => 'authors'
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+ assert_equal expected, response
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end