Refactor Association into Field like everything else (#1897)

* Make assocations asserts easier to understand

* Refactor Association into Field like everything else

* Make assocation serializer/links/meta lazier

* Push association deeper into relationship

* Simplify association usage in relationships

* Better naming of reflection parent serializer

* Easier to read association method
This commit is contained in:
Benjamin Fleischer 2016-08-31 08:35:41 -05:00 committed by L. Preston Sego III
parent 7d2997b3ff
commit 20e394d512
6 changed files with 94 additions and 47 deletions

View File

@ -3,17 +3,32 @@ module ActiveModel
# This class hold all information about serializer's association. # This class hold all information about serializer's association.
# #
# @attr [Symbol] name # @attr [Symbol] name
# @attr [ActiveModel::Serializer] serializer
# @attr [Hash{Symbol => Object}] options # @attr [Hash{Symbol => Object}] options
# @attr [block]
# #
# @example # @example
# Association.new(:comments, CommentSummarySerializer) # Association.new(:comments, { serializer: CommentSummarySerializer })
# #
Association = Struct.new(:name, :serializer, :options, :links, :meta) do class Association < Field
# @return [Symbol] # @return [Symbol]
def key def key
options.fetch(:key, name) options.fetch(:key, name)
end end
# @return [ActiveModel::Serializer, nil]
def serializer
options[:serializer]
end
# @return [Hash]
def links
options.fetch(:links) || {}
end
# @return [Hash, nil]
def meta
options[:meta]
end
end end
end end
end end

View File

@ -74,8 +74,8 @@ module ActiveModel
# @api private # @api private
# #
def associate(reflection) def associate(reflection)
key = reflection.options[:key] key = reflection.options[:key] || reflection.name
key ? self._reflections[key] = reflection : self._reflections[reflection.name] = reflection self._reflections[key] = reflection
end end
end end

View File

@ -88,7 +88,7 @@ module ActiveModel
# Build association. This method is used internally to # Build association. This method is used internally to
# build serializer's association by its reflection. # build serializer's association by its reflection.
# #
# @param [Serializer] subject is a parent serializer for given association # @param [Serializer] parent_serializer for given association
# @param [Hash{Symbol => Object}] parent_serializer_options # @param [Hash{Symbol => Object}] parent_serializer_options
# #
# @example # @example
@ -106,17 +106,19 @@ module ActiveModel
# #
# @api private # @api private
# #
def build_association(subject, parent_serializer_options) def build_association(parent_serializer, parent_serializer_options)
association_value = value(subject) association_value = value(parent_serializer)
reflection_options = options.dup reflection_options = options.dup
serializer_class = subject.class.serializer_for(association_value, reflection_options) serializer_class = parent_serializer.class.serializer_for(association_value, reflection_options)
reflection_options[:include_data] = @_include_data reflection_options[:include_data] = @_include_data
reflection_options[:links] = @_links
reflection_options[:meta] = @_meta
if serializer_class if serializer_class
begin begin
serializer = serializer_class.new( reflection_options[:serializer] = serializer_class.new(
association_value, association_value,
serializer_options(subject, parent_serializer_options, reflection_options) serializer_options(parent_serializer, parent_serializer_options, reflection_options)
) )
rescue ActiveModel::Serializer::CollectionSerializer::NoSerializerError rescue ActiveModel::Serializer::CollectionSerializer::NoSerializerError
reflection_options[:virtual_value] = association_value.try(:as_json) || association_value reflection_options[:virtual_value] = association_value.try(:as_json) || association_value
@ -125,7 +127,8 @@ module ActiveModel
reflection_options[:virtual_value] = association_value reflection_options[:virtual_value] = association_value
end end
Association.new(name, serializer, reflection_options, @_links, @_meta) block = nil
Association.new(name, reflection_options, block)
end end
protected protected
@ -134,12 +137,12 @@ module ActiveModel
private private
def serializer_options(subject, parent_serializer_options, reflection_options) def serializer_options(parent_serializer, parent_serializer_options, reflection_options)
serializer = reflection_options.fetch(:serializer, nil) serializer = reflection_options.fetch(:serializer, nil)
serializer_options = parent_serializer_options.except(:serializer) serializer_options = parent_serializer_options.except(:serializer)
serializer_options[:serializer] = serializer if serializer serializer_options[:serializer] = serializer if serializer
serializer_options[:serializer_context_class] = subject.class serializer_options[:serializer_context_class] = parent_serializer.class
serializer_options serializer_options
end end
end end

View File

@ -7,28 +7,22 @@ module ActiveModelSerializers
# {http://jsonapi.org/format/#document-resource-object-linkage Document Resource Relationship Linkage} # {http://jsonapi.org/format/#document-resource-object-linkage Document Resource Relationship Linkage}
# {http://jsonapi.org/format/#document-meta Document Meta} # {http://jsonapi.org/format/#document-meta Document Meta}
def initialize(parent_serializer, serializable_resource_options, association) def initialize(parent_serializer, serializable_resource_options, association)
serializer = association.serializer @parent_serializer = parent_serializer
options = association.options @association = association
links = association.links
meta = association.meta
@object = parent_serializer.object
@scope = parent_serializer.scope
@association_options = options || {}
@serializable_resource_options = serializable_resource_options @serializable_resource_options = serializable_resource_options
@data = data_for(serializer)
@links = (links || {}).each_with_object({}) do |(key, value), hash|
result = Link.new(parent_serializer, value).as_json
hash[key] = result if result
end
@meta = meta.respond_to?(:call) ? parent_serializer.instance_eval(&meta) : meta
end end
def as_json def as_json
hash = {} hash = {}
hash[:data] = data if association_options[:include_data]
links = self.links if association.options[:include_data]
hash[:data] = data_for(association)
end
links = links_for(association)
hash[:links] = links if links.any? hash[:links] = links if links.any?
meta = self.meta
meta = meta_for(association)
hash[:meta] = meta if meta hash[:meta] = meta if meta
hash hash
@ -36,20 +30,32 @@ module ActiveModelSerializers
protected protected
attr_reader :object, :scope, :data, :serializable_resource_options, attr_reader :parent_serializer, :serializable_resource_options, :association
:association_options, :links, :meta
private private
def data_for(serializer) def data_for(association)
serializer = association.serializer
if serializer.respond_to?(:each) if serializer.respond_to?(:each)
serializer.map { |s| ResourceIdentifier.new(s, serializable_resource_options).as_json } serializer.map { |s| ResourceIdentifier.new(s, serializable_resource_options).as_json }
elsif association_options[:virtual_value] elsif (virtual_value = association.options[:virtual_value])
association_options[:virtual_value] virtual_value
elsif serializer && serializer.object elsif serializer && serializer.object
ResourceIdentifier.new(serializer, serializable_resource_options).as_json ResourceIdentifier.new(serializer, serializable_resource_options).as_json
end end
end end
def links_for(association)
association.links.each_with_object({}) do |(key, value), hash|
result = Link.new(parent_serializer, value).as_json
hash[key] = result if result
end
end
def meta_for(association)
meta = association.meta
meta.respond_to?(:call) ? parent_serializer.instance_eval(&meta) : meta
end
end end
end end
end end

View File

@ -155,15 +155,17 @@ module ActiveModelSerializers
serializable_resource_options = {} # adapter.instance_options serializable_resource_options = {} # adapter.instance_options
meta = test_options.delete(:meta) options = test_options.delete(:options) || {}
options = test_options.delete(:options) options[:links] = test_options.delete(:links)
links = test_options.delete(:links) options[:meta] = test_options.delete(:meta)
association_serializer = @serializer association_serializer = @serializer
if association_serializer && association_serializer.object if association_serializer && association_serializer.object
association_name = association_serializer.json_key.to_sym association_name = association_serializer.json_key.to_sym
association = ::ActiveModel::Serializer::Association.new(association_name, association_serializer, options, links, meta) options[:serializer] = association_serializer
association = ::ActiveModel::Serializer::Association.new(association_name, options, nil)
else else
association = ::ActiveModel::Serializer::Association.new(:association_name_not_used, association, options, links, meta) options[:serializer] = association
association = ::ActiveModel::Serializer::Association.new(:association_name_not_used, options, nil)
end end
relationship = Relationship.new(parent_serializer, serializable_resource_options, association) relationship = Relationship.new(parent_serializer, serializable_resource_options, association)

View File

@ -31,13 +31,13 @@ module ActiveModel
case key case key
when :posts when :posts
assert_equal({ include_data: true }, options) assert_equal true, options.fetch(:include_data)
assert_kind_of(ActiveModelSerializers.config.collection_serializer, serializer) assert_kind_of(ActiveModelSerializers.config.collection_serializer, serializer)
when :bio when :bio
assert_equal({ include_data: true }, options) assert_equal true, options.fetch(:include_data)
assert_nil serializer assert_nil serializer
when :roles when :roles
assert_equal({ include_data: true }, options) assert_equal true, options.fetch(:include_data)
assert_kind_of(ActiveModelSerializers.config.collection_serializer, serializer) assert_kind_of(ActiveModelSerializers.config.collection_serializer, serializer)
else else
flunk "Unknown association: #{key}" flunk "Unknown association: #{key}"
@ -79,7 +79,7 @@ module ActiveModel
flunk "Unknown association: #{key}" flunk "Unknown association: #{key}"
end end
assert_equal({ include_data: true }, association.options) assert_equal true, association.options.fetch(:include_data)
end end
end end
@ -291,11 +291,23 @@ module ActiveModel
end end
class InheritedSerializerTest < ActiveSupport::TestCase class InheritedSerializerTest < ActiveSupport::TestCase
class PostSerializer < ActiveModel::Serializer
belongs_to :author
has_many :comments
belongs_to :blog
end
class InheritedPostSerializer < PostSerializer class InheritedPostSerializer < PostSerializer
belongs_to :author, polymorphic: true belongs_to :author, polymorphic: true
has_many :comments, key: :reviews has_many :comments, key: :reviews
end end
class AuthorSerializer < ActiveModel::Serializer
has_many :posts
has_many :roles
has_one :bio
end
class InheritedAuthorSerializer < AuthorSerializer class InheritedAuthorSerializer < AuthorSerializer
has_many :roles, polymorphic: true has_many :roles, polymorphic: true
has_one :bio, polymorphic: true has_one :bio, polymorphic: true
@ -333,9 +345,18 @@ module ActiveModel
end end
test 'a serializer inheriting from another serializer can redefine belongs_to associations' do test 'a serializer inheriting from another serializer can redefine belongs_to associations' do
expected = [:author, :comments, :blog].sort assert_equal [:author, :comments, :blog], @post_associations.map(&:name)
result = (@inherited_post_associations - @post_associations).map(&:name).sort assert_equal [:author, :comments, :blog, :comments], @inherited_post_associations.map(&:name)
assert_equal(result, expected)
refute @post_associations.detect { |assoc| assoc.name == :author }.options.key?(:polymorphic)
assert_equal true, @inherited_post_associations.detect { |assoc| assoc.name == :author }.options.fetch(:polymorphic)
refute @post_associations.detect { |assoc| assoc.name == :comments }.options.key?(:key)
original_comment_assoc, new_comments_assoc = @inherited_post_associations.select { |assoc| assoc.name == :comments }
refute original_comment_assoc.options.key?(:key)
assert_equal :reviews, new_comments_assoc.options.fetch(:key)
assert_equal @post_associations.detect { |assoc| assoc.name == :blog }, @inherited_post_associations.detect { |assoc| assoc.name == :blog }
end end
test 'a serializer inheriting from another serializer can have an additional association with the same name but with different key' do test 'a serializer inheriting from another serializer can have an additional association with the same name but with different key' do