Merge pull request #2026 from bf4/refactor_association

Refactor Association to make it eval reflection JIT
This commit is contained in:
Benjamin Fleischer 2017-04-30 15:41:09 -07:00 committed by GitHub
commit 0f59d64ed5
14 changed files with 299 additions and 194 deletions

View File

@ -332,18 +332,17 @@ module ActiveModel
# @param [JSONAPI::IncludeDirective] include_directive (defaults to the # @param [JSONAPI::IncludeDirective] include_directive (defaults to the
# +default_include_directive+ config value when not provided) # +default_include_directive+ config value when not provided)
# @return [Enumerator<Association>] # @return [Enumerator<Association>]
#
def associations(include_directive = ActiveModelSerializers.default_include_directive, include_slice = nil) def associations(include_directive = ActiveModelSerializers.default_include_directive, include_slice = nil)
include_slice ||= include_directive include_slice ||= include_directive
return unless object return Enumerator.new unless object
Enumerator.new do |y| Enumerator.new do |y|
self.class._reflections.values.each do |reflection| self.class._reflections.each do |key, reflection|
next if reflection.excluded?(self) next if reflection.excluded?(self)
key = reflection.options.fetch(:key, reflection.name)
next unless include_directive.key?(key) next unless include_directive.key?(key)
y.yield reflection.build_association(self, instance_options, include_slice) association = reflection.build_association(self, instance_options, include_slice)
y.yield association
end end
end end
end end
@ -351,31 +350,6 @@ module ActiveModel
# @return [Hash] containing the attributes and first level # @return [Hash] containing the attributes and first level
# associations, similar to how ActiveModel::Serializers::JSON is used # associations, similar to how ActiveModel::Serializers::JSON is used
# in ActiveRecord::Base. # in ActiveRecord::Base.
#
# TODO: Include <tt>ActiveModel::Serializers::JSON</tt>.
# So that the below is true:
# @param options [nil, Hash] The same valid options passed to `serializable_hash`
# (:only, :except, :methods, and :include).
#
# See
# https://github.com/rails/rails/blob/v5.0.0.beta2/activemodel/lib/active_model/serializers/json.rb#L17-L101
# https://github.com/rails/rails/blob/v5.0.0.beta2/activemodel/lib/active_model/serialization.rb#L85-L123
# https://github.com/rails/rails/blob/v5.0.0.beta2/activerecord/lib/active_record/serialization.rb#L11-L17
# https://github.com/rails/rails/blob/v5.0.0.beta2/activesupport/lib/active_support/core_ext/object/json.rb#L147-L162
#
# @example
# # The :only and :except options can be used to limit the attributes included, and work
# # similar to the attributes method.
# serializer.as_json(only: [:id, :name])
# serializer.as_json(except: [:id, :created_at, :age])
#
# # To include the result of some method calls on the model use :methods:
# serializer.as_json(methods: :permalink)
#
# # To include associations use :include:
# serializer.as_json(include: :posts)
# # Second level and higher order associations work as well:
# serializer.as_json(include: { posts: { include: { comments: { only: :body } }, only: :title } })
def serializable_hash(adapter_options = nil, options = {}, adapter_instance = self.class.serialization_adapter_instance) def serializable_hash(adapter_options = nil, options = {}, adapter_instance = self.class.serialization_adapter_instance)
adapter_options ||= {} adapter_options ||= {}
options[:include_directive] ||= ActiveModel::Serializer.include_directive_from_options(adapter_options) options[:include_directive] ||= ActiveModel::Serializer.include_directive_from_options(adapter_options)
@ -387,13 +361,6 @@ module ActiveModel
alias to_h serializable_hash alias to_h serializable_hash
# @see #serializable_hash # @see #serializable_hash
# TODO: When moving attributes adapter logic here, @see #serializable_hash
# So that the below is true:
# @param options [nil, Hash] The same valid options passed to `as_json`
# (:root, :only, :except, :methods, and :include).
# The default for `root` is nil.
# The default value for include_root is false. You can change it to true if the given
# JSON string includes a single root node.
def as_json(adapter_opts = nil) def as_json(adapter_opts = nil)
serializable_hash(adapter_opts) serializable_hash(adapter_opts)
end end
@ -424,14 +391,12 @@ module ActiveModel
# @api private # @api private
def associations_hash(adapter_options, options, adapter_instance) def associations_hash(adapter_options, options, adapter_instance)
relationships = {}
include_directive = options.fetch(:include_directive) include_directive = options.fetch(:include_directive)
associations(include_directive).each do |association| include_slice = options[:include_slice]
adapter_opts = adapter_options.merge(include_directive: include_directive[association.key]) associations(include_directive, include_slice).each_with_object({}) do |association, relationships|
relationships[association.key] ||= association.serializable_hash(adapter_opts, adapter_instance) adapter_opts = adapter_options.merge(include_directive: include_directive[association.key], adapter_instance: adapter_instance)
relationships[association.key] = association.serializable_hash(adapter_opts, adapter_instance)
end end
relationships
end end
protected protected

View File

@ -1,50 +1,67 @@
require 'active_model/serializer/lazy_association'
module ActiveModel module ActiveModel
class Serializer class Serializer
# This class holds all information about serializer's association. # This class holds all information about serializer's association.
# #
# @attr [Symbol] name # @api private
# @attr [Hash{Symbol => Object}] options Association = Struct.new(:reflection, :association_options) do
# @attr [block] attr_reader :lazy_association
# delegate :object, :include_data?, :virtual_value, :collection?, to: :lazy_association
# @example
# Association.new(:comments, { serializer: CommentSummarySerializer }) def initialize(*)
# super
class Association < Field @lazy_association = LazyAssociation.new(reflection, association_options)
# @return [Symbol]
def key
options.fetch(:key, name)
end end
# @return [ActiveModel::Serializer, nil] # @return [Symbol]
def serializer delegate :name, to: :reflection
options[:serializer]
# @return [Symbol]
def key
reflection_options.fetch(:key, name)
end
# @return [True,False]
def key?
reflection_options.key?(:key)
end end
# @return [Hash] # @return [Hash]
def links def links
options.fetch(:links) || {} reflection_options.fetch(:links) || {}
end end
# @return [Hash, nil] # @return [Hash, nil]
# This gets mutated, so cannot use the cached reflection_options
def meta def meta
options[:meta] reflection.options[:meta]
end
def polymorphic?
true == reflection_options[:polymorphic]
end end
# @api private # @api private
def serializable_hash(adapter_options, adapter_instance) def serializable_hash(adapter_options, adapter_instance)
return options[:virtual_value] if options[:virtual_value] association_serializer = lazy_association.serializer
object = serializer && serializer.object return virtual_value if virtual_value
return unless object association_object = association_serializer && association_serializer.object
return unless association_object
serialization = serializer.serializable_hash(adapter_options, {}, adapter_instance) serialization = association_serializer.serializable_hash(adapter_options, {}, adapter_instance)
if options[:polymorphic] && serialization if polymorphic? && serialization
polymorphic_type = object.class.name.underscore polymorphic_type = association_object.class.name.underscore
serialization = { type: polymorphic_type, polymorphic_type.to_sym => serialization } serialization = { type: polymorphic_type, polymorphic_type.to_sym => serialization }
end end
serialization serialization
end end
private
delegate :reflection_options, to: :lazy_association
end end
end end
end end

View File

@ -1,7 +1,7 @@
module ActiveModel module ActiveModel
class Serializer class Serializer
# @api private # @api private
class BelongsToReflection < SingularReflection class BelongsToReflection < Reflection
end end
end end
end end

View File

@ -1,7 +0,0 @@
module ActiveModel
class Serializer
# @api private
class CollectionReflection < Reflection
end
end
end

View File

@ -193,12 +193,14 @@ module ActiveModel
cache_keys << object_cache_key(serializer, adapter_instance) cache_keys << object_cache_key(serializer, adapter_instance)
serializer.associations(include_directive).each do |association| serializer.associations(include_directive).each do |association|
if association.serializer.respond_to?(:each) # TODO(BF): Process relationship without evaluating lazy_association
association.serializer.each do |sub_serializer| association_serializer = association.lazy_association.serializer
if association_serializer.respond_to?(:each)
association_serializer.each do |sub_serializer|
cache_keys << object_cache_key(sub_serializer, adapter_instance) cache_keys << object_cache_key(sub_serializer, adapter_instance)
end end
else else
cache_keys << object_cache_key(association.serializer, adapter_instance) cache_keys << object_cache_key(association_serializer, adapter_instance)
end end
end end
end end

View File

@ -1,7 +1,10 @@
module ActiveModel module ActiveModel
class Serializer class Serializer
# @api private # @api private
class HasManyReflection < CollectionReflection class HasManyReflection < Reflection
def collection?
true
end
end end
end end
end end

View File

@ -1,7 +1,7 @@
module ActiveModel module ActiveModel
class Serializer class Serializer
# @api private # @api private
class HasOneReflection < SingularReflection class HasOneReflection < Reflection
end end
end end
end end

View File

@ -0,0 +1,95 @@
module ActiveModel
class Serializer
# @api private
LazyAssociation = Struct.new(:reflection, :association_options) do
REFLECTION_OPTIONS = %i(key links polymorphic meta serializer virtual_value namespace).freeze
delegate :collection?, to: :reflection
def reflection_options
@reflection_options ||= reflection.options.dup.reject { |k, _| !REFLECTION_OPTIONS.include?(k) }
end
def object
@object ||= reflection.value(
association_options.fetch(:parent_serializer),
association_options.fetch(:include_slice)
)
end
alias_method :eval_reflection_block, :object
def include_data?
eval_reflection_block if reflection.block
reflection.include_data?(
association_options.fetch(:include_slice)
)
end
# @return [ActiveModel::Serializer, nil]
def serializer
return @serializer if defined?(@serializer)
if serializer_class
serialize_object!(object)
elsif !object.nil? && !object.instance_of?(Object)
cached_result[:virtual_value] = object
end
@serializer = cached_result[:serializer]
end
def virtual_value
cached_result[:virtual_value] || reflection_options[:virtual_value]
end
def serializer_class
return @serializer_class if defined?(@serializer_class)
serializer_for_options = { namespace: namespace }
serializer_for_options[:serializer] = reflection_options[:serializer] if reflection_options.key?(:serializer)
@serializer_class = association_options.fetch(:parent_serializer).class.serializer_for(object, serializer_for_options)
end
private
def cached_result
@cached_result ||= {}
end
def serialize_object!(object)
if collection?
if (serializer = instantiate_collection_serializer(object)).nil?
# BUG: per #2027, JSON API resource relationships are only id and type, and hence either
# *require* a serializer or we need to be a little clever about figuring out the id/type.
# In either case, returning the raw virtual value will almost always be incorrect.
#
# Should be reflection_options[:virtual_value] or adapter needs to figure out what to do
# with an object that is non-nil and has no defined serializer.
cached_result[:virtual_value] = object.try(:as_json) || object
else
cached_result[:serializer] = serializer
end
else
cached_result[:serializer] = instantiate_serializer(object)
end
end
def instantiate_serializer(object)
serializer_options = association_options.fetch(:parent_serializer_options).except(:serializer)
serializer_options[:serializer_context_class] = association_options.fetch(:parent_serializer).class
serializer = reflection_options.fetch(:serializer, nil)
serializer_options[:serializer] = serializer if serializer
serializer_class.new(object, serializer_options)
end
def instantiate_collection_serializer(object)
serializer = catch(:no_serializer) do
instantiate_serializer(object)
end
serializer
end
def namespace
reflection_options[:namespace] ||
association_options.fetch(:parent_serializer_options)[:namespace]
end
end
end
end

View File

@ -1,4 +1,5 @@
require 'active_model/serializer/field' require 'active_model/serializer/field'
require 'active_model/serializer/association'
module ActiveModel module ActiveModel
class Serializer class Serializer
@ -37,12 +38,12 @@ module ActiveModel
# 2) as 'object.comments.last(1)' and named 'last_comments'. # 2) as 'object.comments.last(1)' and named 'last_comments'.
# #
# PostSerializer._reflections # => # PostSerializer._reflections # =>
# # [ # # {
# # HasOneReflection.new(:author, serializer: AuthorSerializer), # # author: HasOneReflection.new(:author, serializer: AuthorSerializer),
# # HasManyReflection.new(:comments) # # comments: HasManyReflection.new(:comments)
# # HasManyReflection.new(:comments, { key: :last_comments }, #<Block>) # # last_comments: HasManyReflection.new(:comments, { key: :last_comments }, #<Block>)
# # HasManyReflection.new(:secret_meta_data, { if: :is_admin? }) # # secret_meta_data: HasManyReflection.new(:secret_meta_data, { if: :is_admin? })
# # ] # # }
# #
# So you can inspect reflections in your Adapters. # So you can inspect reflections in your Adapters.
class Reflection < Field class Reflection < Field
@ -70,8 +71,8 @@ module ActiveModel
# meta ids: ids # meta ids: ids
# end # end
# end # end
def link(name, value = nil, &block) def link(name, value = nil)
options[:links][name] = block || value options[:links][name] = block_given? ? Proc.new : value
:nil :nil
end end
@ -85,8 +86,8 @@ module ActiveModel
# href object.blog.id.to_s # href object.blog.id.to_s
# meta(id: object.blog.id) # meta(id: object.blog.id)
# end # end
def meta(value = nil, &block) def meta(value = nil)
options[:meta] = block || value options[:meta] = block_given? ? Proc.new : value
:nil :nil
end end
@ -118,6 +119,20 @@ module ActiveModel
:nil :nil
end end
def collection?
false
end
def include_data?(include_slice)
include_data_setting = options[:include_data_setting]
case include_data_setting
when :if_sideloaded then include_slice.key?(name)
when true then true
when false then false
else fail ArgumentError, "Unknown include_data_setting '#{include_data_setting.inspect}'"
end
end
# @param serializer [ActiveModel::Serializer] # @param serializer [ActiveModel::Serializer]
# @yield [ActiveModel::Serializer] # @yield [ActiveModel::Serializer]
# @return [:nil, associated resource or resource collection] # @return [:nil, associated resource or resource collection]
@ -156,62 +171,18 @@ module ActiveModel
# #
# @api private # @api private
def build_association(parent_serializer, parent_serializer_options, include_slice = {}) def build_association(parent_serializer, parent_serializer_options, include_slice = {})
reflection_options = options.dup association_options = {
parent_serializer: parent_serializer,
# Pass the parent's namespace onto the child serializer parent_serializer_options: parent_serializer_options,
reflection_options[:namespace] ||= parent_serializer_options[:namespace] include_slice: include_slice
}
association_value = value(parent_serializer, include_slice) Association.new(self, association_options)
serializer_class = parent_serializer.class.serializer_for(association_value, reflection_options)
reflection_options[:include_data] = include_data?(include_slice)
reflection_options[:links] = options[:links]
reflection_options[:meta] = options[:meta]
if serializer_class
serializer = catch(:no_serializer) do
serializer_class.new(
association_value,
serializer_options(parent_serializer, parent_serializer_options, reflection_options)
)
end
if serializer.nil?
reflection_options[:virtual_value] = association_value.try(:as_json) || association_value
else
reflection_options[:serializer] = serializer
end
elsif !association_value.nil? && !association_value.instance_of?(Object)
reflection_options[:virtual_value] = association_value
end
block = nil
Association.new(name, reflection_options, block)
end end
protected protected
# used in instance exec # used in instance exec
attr_accessor :object, :scope attr_accessor :object, :scope
private
def include_data?(include_slice)
include_data_setting = options[:include_data_setting]
case include_data_setting
when :if_sideloaded then include_slice.key?(name)
when true then true
when false then false
else fail ArgumentError, "Unknown include_data_setting '#{include_data_setting.inspect}'"
end
end
def serializer_options(parent_serializer, parent_serializer_options, reflection_options)
serializer = reflection_options.fetch(:serializer, nil)
serializer_options = parent_serializer_options.except(:serializer)
serializer_options[:serializer] = serializer if serializer
serializer_options[:serializer_context_class] = parent_serializer.class
serializer_options
end
end end
end end
end end

View File

@ -1,7 +0,0 @@
module ActiveModel
class Serializer
# @api private
class SingularReflection < Reflection
end
end
end

View File

@ -257,7 +257,8 @@ module ActiveModelSerializers
def process_relationships(serializer, include_slice) def process_relationships(serializer, include_slice)
serializer.associations(include_slice).each do |association| serializer.associations(include_slice).each do |association|
process_relationship(association.serializer, include_slice[association.key]) # TODO(BF): Process relationship without evaluating lazy_association
process_relationship(association.lazy_association.serializer, include_slice[association.key])
end end
end end

View File

@ -15,9 +15,7 @@ module ActiveModelSerializers
def as_json def as_json
hash = {} hash = {}
if association.options[:include_data] hash[:data] = data_for(association) if association.include_data?
hash[:data] = data_for(association)
end
links = links_for(association) links = links_for(association)
hash[:links] = links if links.any? hash[:links] = links if links.any?
@ -35,14 +33,38 @@ module ActiveModelSerializers
private private
# TODO(BF): Avoid db hit on belong_to_ releationship by using foreign_key on self
def data_for(association) def data_for(association)
serializer = association.serializer if association.collection?
if serializer.respond_to?(:each) data_for_many(association)
serializer.map { |s| ResourceIdentifier.new(s, serializable_resource_options).as_json } else
elsif (virtual_value = association.options[:virtual_value]) data_for_one(association)
end
end
def data_for_one(association)
# TODO(BF): Process relationship without evaluating lazy_association
serializer = association.lazy_association.serializer
if (virtual_value = association.virtual_value)
virtual_value virtual_value
elsif serializer && serializer.object elsif serializer && association.object
ResourceIdentifier.new(serializer, serializable_resource_options).as_json ResourceIdentifier.new(serializer, serializable_resource_options).as_json
else
nil
end
end
def data_for_many(association)
# TODO(BF): Process relationship without evaluating lazy_association
collection_serializer = association.lazy_association.serializer
if collection_serializer.respond_to?(:each)
collection_serializer.map do |serializer|
ResourceIdentifier.new(serializer, serializable_resource_options).as_json
end
elsif (virtual_value = association.virtual_value)
virtual_value
else
[]
end end
end end

View File

@ -30,18 +30,17 @@ module ActiveModel
def test_has_many_and_has_one def test_has_many_and_has_one
@author_serializer.associations.each do |association| @author_serializer.associations.each do |association|
key = association.key key = association.key
serializer = association.serializer serializer = association.lazy_association.serializer
options = association.options
case key case key
when :posts when :posts
assert_equal true, options.fetch(:include_data) assert_equal true, association.include_data?
assert_kind_of(ActiveModelSerializers.config.collection_serializer, serializer) assert_kind_of(ActiveModelSerializers.config.collection_serializer, serializer)
when :bio when :bio
assert_equal true, options.fetch(:include_data) assert_equal true, association.include_data?
assert_nil serializer assert_nil serializer
when :roles when :roles
assert_equal true, options.fetch(:include_data) assert_equal true, association.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}"
@ -56,12 +55,11 @@ module ActiveModel
end end
post_serializer_class.new(@post).associations.each do |association| post_serializer_class.new(@post).associations.each do |association|
key = association.key key = association.key
serializer = association.serializer serializer = association.lazy_association.serializer
options = association.options
assert_equal :tags, key assert_equal :tags, key
assert_nil serializer assert_nil serializer
assert_equal [{ id: 'tagid', name: '#hashtagged' }].to_json, options[:virtual_value].to_json assert_equal [{ id: 'tagid', name: '#hashtagged' }].to_json, association.virtual_value.to_json
end end
end end
@ -70,7 +68,7 @@ module ActiveModel
.associations .associations
.detect { |assoc| assoc.key == :comments } .detect { |assoc| assoc.key == :comments }
comment_serializer = association.serializer.first comment_serializer = association.lazy_association.serializer.first
class << comment_serializer class << comment_serializer
def custom_options def custom_options
instance_options instance_options
@ -82,7 +80,7 @@ module ActiveModel
def test_belongs_to def test_belongs_to
@comment_serializer.associations.each do |association| @comment_serializer.associations.each do |association|
key = association.key key = association.key
serializer = association.serializer serializer = association.lazy_association.serializer
case key case key
when :post when :post
@ -93,7 +91,7 @@ module ActiveModel
flunk "Unknown association: #{key}" flunk "Unknown association: #{key}"
end end
assert_equal true, association.options.fetch(:include_data) assert_equal true, association.include_data?
end end
end end
@ -203,11 +201,11 @@ module ActiveModel
@post_serializer.associations.each do |association| @post_serializer.associations.each do |association|
case association.key case association.key
when :comments when :comments
assert_instance_of(ResourceNamespace::CommentSerializer, association.serializer.first) assert_instance_of(ResourceNamespace::CommentSerializer, association.lazy_association.serializer.first)
when :author when :author
assert_instance_of(ResourceNamespace::AuthorSerializer, association.serializer) assert_instance_of(ResourceNamespace::AuthorSerializer, association.lazy_association.serializer)
when :description when :description
assert_instance_of(ResourceNamespace::DescriptionSerializer, association.serializer) assert_instance_of(ResourceNamespace::DescriptionSerializer, association.lazy_association.serializer)
else else
flunk "Unknown association: #{key}" flunk "Unknown association: #{key}"
end end
@ -245,11 +243,11 @@ module ActiveModel
@post_serializer.associations.each do |association| @post_serializer.associations.each do |association|
case association.key case association.key
when :comments when :comments
assert_instance_of(PostSerializer::CommentSerializer, association.serializer.first) assert_instance_of(PostSerializer::CommentSerializer, association.lazy_association.serializer.first)
when :author when :author
assert_instance_of(PostSerializer::AuthorSerializer, association.serializer) assert_instance_of(PostSerializer::AuthorSerializer, association.lazy_association.serializer)
when :description when :description
assert_instance_of(PostSerializer::DescriptionSerializer, association.serializer) assert_instance_of(PostSerializer::DescriptionSerializer, association.lazy_association.serializer)
else else
flunk "Unknown association: #{key}" flunk "Unknown association: #{key}"
end end
@ -260,7 +258,7 @@ module ActiveModel
def test_conditional_associations def test_conditional_associations
model = Class.new(::Model) do model = Class.new(::Model) do
attributes :true, :false attributes :true, :false
associations :association associations :something
end.new(true: true, false: false) end.new(true: true, false: false)
scenarios = [ scenarios = [
@ -284,7 +282,7 @@ module ActiveModel
scenarios.each do |s| scenarios.each do |s|
serializer = Class.new(ActiveModel::Serializer) do serializer = Class.new(ActiveModel::Serializer) do
belongs_to :association, s[:options] belongs_to :something, s[:options]
def true def true
true true
@ -296,7 +294,7 @@ module ActiveModel
end end
hash = serializable(model, serializer: serializer).serializable_hash hash = serializable(model, serializer: serializer).serializable_hash
assert_equal(s[:included], hash.key?(:association), "Error with #{s[:options]}") assert_equal(s[:included], hash.key?(:something), "Error with #{s[:options]}")
end end
end end
@ -341,8 +339,8 @@ module ActiveModel
@author_serializer = AuthorSerializer.new(@author) @author_serializer = AuthorSerializer.new(@author)
@inherited_post_serializer = InheritedPostSerializer.new(@post) @inherited_post_serializer = InheritedPostSerializer.new(@post)
@inherited_author_serializer = InheritedAuthorSerializer.new(@author) @inherited_author_serializer = InheritedAuthorSerializer.new(@author)
@author_associations = @author_serializer.associations.to_a @author_associations = @author_serializer.associations.to_a.sort_by(&:name)
@inherited_author_associations = @inherited_author_serializer.associations.to_a @inherited_author_associations = @inherited_author_serializer.associations.to_a.sort_by(&:name)
@post_associations = @post_serializer.associations.to_a @post_associations = @post_serializer.associations.to_a
@inherited_post_associations = @inherited_post_serializer.associations.to_a @inherited_post_associations = @inherited_post_serializer.associations.to_a
end end
@ -361,28 +359,35 @@ module ActiveModel
test 'a serializer inheriting from another serializer can redefine has_many and has_one associations' do test 'a serializer inheriting from another serializer can redefine has_many and has_one associations' do
expected = [:roles, :bio].sort expected = [:roles, :bio].sort
result = (@inherited_author_associations - @author_associations).map(&:name).sort result = (@inherited_author_associations.map(&:reflection) - @author_associations.map(&:reflection)).map(&:name)
assert_equal(result, expected) assert_equal(result, expected)
assert_equal [true, false, true], @inherited_author_associations.map(&:polymorphic?)
assert_equal [false, false, false], @author_associations.map(&:polymorphic?)
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
assert_equal [:author, :comments, :blog], @post_associations.map(&:name) assert_equal [:author, :comments, :blog], @post_associations.map(&:name)
assert_equal [:author, :comments, :blog, :comments], @inherited_post_associations.map(&:name) assert_equal [:author, :comments, :blog, :comments], @inherited_post_associations.map(&:name)
refute @post_associations.detect { |assoc| assoc.name == :author }.options.key?(:polymorphic) refute @post_associations.detect { |assoc| assoc.name == :author }.polymorphic?
assert_equal true, @inherited_post_associations.detect { |assoc| assoc.name == :author }.options.fetch(:polymorphic) assert @inherited_post_associations.detect { |assoc| assoc.name == :author }.polymorphic?
refute @post_associations.detect { |assoc| assoc.name == :comments }.options.key?(:key) refute @post_associations.detect { |assoc| assoc.name == :comments }.key?
original_comment_assoc, new_comments_assoc = @inherited_post_associations.select { |assoc| assoc.name == :comments } original_comment_assoc, new_comments_assoc = @inherited_post_associations.select { |assoc| assoc.name == :comments }
refute original_comment_assoc.options.key?(:key) refute original_comment_assoc.key?
assert_equal :reviews, new_comments_assoc.options.fetch(:key) assert_equal :reviews, new_comments_assoc.key
assert_equal @post_associations.detect { |assoc| assoc.name == :blog }, @inherited_post_associations.detect { |assoc| assoc.name == :blog } original_blog = @post_associations.detect { |assoc| assoc.name == :blog }
inherited_blog = @inherited_post_associations.detect { |assoc| assoc.name == :blog }
original_parent_serializer = original_blog.lazy_association.association_options.delete(:parent_serializer)
inherited_parent_serializer = inherited_blog.lazy_association.association_options.delete(:parent_serializer)
assert_equal PostSerializer, original_parent_serializer.class
assert_equal InheritedPostSerializer, inherited_parent_serializer.class
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
expected = [:author, :comments, :blog, :reviews].sort expected = [:author, :comments, :blog, :reviews].sort
result = @inherited_post_serializer.associations.map { |a| a.options.fetch(:key, a.name) }.sort result = @inherited_post_serializer.associations.map(&:key).sort
assert_equal(result, expected) assert_equal(result, expected)
end end
end end

View File

@ -25,6 +25,10 @@ module ActiveModel
@instance_options = {} @instance_options = {}
end end
def evaluate_association_value(association)
association.lazy_association.eval_reflection_block
end
# TODO: Remaining tests # TODO: Remaining tests
# test_reflection_value_block_with_scope # test_reflection_value_block_with_scope
# test_reflection_value_uses_serializer_instance_method # test_reflection_value_uses_serializer_instance_method
@ -57,7 +61,7 @@ module ActiveModel
assert_equal true, reflection.options.fetch(:include_data_setting) assert_equal true, reflection.options.fetch(:include_data_setting)
include_slice = :does_not_matter include_slice = :does_not_matter
assert_equal @model.blog, reflection.value(serializer_instance, include_slice) assert_equal @model.blog, reflection.send(:value, serializer_instance, include_slice)
end end
def test_reflection_value_block def test_reflection_value_block
@ -77,7 +81,7 @@ module ActiveModel
assert_equal true, reflection.options.fetch(:include_data_setting) assert_equal true, reflection.options.fetch(:include_data_setting)
include_slice = :does_not_matter include_slice = :does_not_matter
assert_equal @model.blog, reflection.value(serializer_instance, include_slice) assert_equal @model.blog, reflection.send(:value, serializer_instance, include_slice)
end end
def test_reflection_value_block_with_explicit_include_data_true def test_reflection_value_block_with_explicit_include_data_true
@ -98,7 +102,7 @@ module ActiveModel
assert_equal true, reflection.options.fetch(:include_data_setting) assert_equal true, reflection.options.fetch(:include_data_setting)
include_slice = :does_not_matter include_slice = :does_not_matter
assert_equal @model.blog, reflection.value(serializer_instance, include_slice) assert_equal @model.blog, reflection.send(:value, serializer_instance, include_slice)
end end
def test_reflection_value_block_with_include_data_false_mutates_the_reflection_include_data def test_reflection_value_block_with_include_data_false_mutates_the_reflection_include_data
@ -117,7 +121,7 @@ module ActiveModel
assert_respond_to reflection.block, :call assert_respond_to reflection.block, :call
assert_equal true, reflection.options.fetch(:include_data_setting) assert_equal true, reflection.options.fetch(:include_data_setting)
include_slice = :does_not_matter include_slice = :does_not_matter
assert_nil reflection.value(serializer_instance, include_slice) assert_nil reflection.send(:value, serializer_instance, include_slice)
assert_equal false, reflection.options.fetch(:include_data_setting) assert_equal false, reflection.options.fetch(:include_data_setting)
end end
@ -137,7 +141,7 @@ module ActiveModel
assert_respond_to reflection.block, :call assert_respond_to reflection.block, :call
assert_equal true, reflection.options.fetch(:include_data_setting) assert_equal true, reflection.options.fetch(:include_data_setting)
include_slice = {} include_slice = {}
assert_nil reflection.value(serializer_instance, include_slice) assert_nil reflection.send(:value, serializer_instance, include_slice)
assert_equal :if_sideloaded, reflection.options.fetch(:include_data_setting) assert_equal :if_sideloaded, reflection.options.fetch(:include_data_setting)
end end
@ -157,7 +161,7 @@ module ActiveModel
assert_respond_to reflection.block, :call assert_respond_to reflection.block, :call
assert_equal true, reflection.options.fetch(:include_data_setting) assert_equal true, reflection.options.fetch(:include_data_setting)
include_slice = { blog: :does_not_matter } include_slice = { blog: :does_not_matter }
assert_equal @model.blog, reflection.value(serializer_instance, include_slice) assert_equal @model.blog, reflection.send(:value, serializer_instance, include_slice)
assert_equal :if_sideloaded, reflection.options.fetch(:include_data_setting) assert_equal :if_sideloaded, reflection.options.fetch(:include_data_setting)
end end
@ -175,6 +179,13 @@ module ActiveModel
# Build Association # Build Association
association = reflection.build_association(serializer_instance, @instance_options) association = reflection.build_association(serializer_instance, @instance_options)
# Assert association links empty when not yet evaluated
assert_equal @empty_links, reflection.options.fetch(:links)
assert_equal @empty_links, association.links
evaluate_association_value(association)
assert_equal @expected_links, association.links assert_equal @expected_links, association.links
assert_equal @expected_links, reflection.options.fetch(:links) assert_equal @expected_links, reflection.options.fetch(:links)
end end
@ -195,9 +206,16 @@ module ActiveModel
# Build Association # Build Association
association = reflection.build_association(serializer_instance, @instance_options) association = reflection.build_association(serializer_instance, @instance_options)
# Assert association links empty when not yet evaluated
assert_equal @empty_links, association.links
evaluate_association_value(association)
# Assert before instance_eval link # Assert before instance_eval link
link = association.links.fetch(:self) link = association.links.fetch(:self)
assert_respond_to link, :call assert_respond_to link, :call
assert_respond_to reflection.options.fetch(:links).fetch(:self), :call
# Assert after instance_eval link # Assert after instance_eval link
assert_equal @expected_links.fetch(:self), reflection.instance_eval(&link) assert_equal @expected_links.fetch(:self), reflection.instance_eval(&link)
@ -218,6 +236,9 @@ module ActiveModel
# Build Association # Build Association
association = reflection.build_association(serializer_instance, @instance_options) association = reflection.build_association(serializer_instance, @instance_options)
evaluate_association_value(association)
assert_equal @expected_meta, association.meta assert_equal @expected_meta, association.meta
assert_equal @expected_meta, reflection.options.fetch(:meta) assert_equal @expected_meta, reflection.options.fetch(:meta)
end end
@ -239,6 +260,9 @@ module ActiveModel
# Build Association # Build Association
association = reflection.build_association(serializer_instance, @instance_options) association = reflection.build_association(serializer_instance, @instance_options)
# Assert before instance_eval meta # Assert before instance_eval meta
evaluate_association_value(association)
assert_respond_to association.meta, :call assert_respond_to association.meta, :call
assert_respond_to reflection.options.fetch(:meta), :call assert_respond_to reflection.options.fetch(:meta), :call
@ -271,6 +295,8 @@ module ActiveModel
assert_nil association.meta assert_nil association.meta
assert_nil reflection.options.fetch(:meta) assert_nil reflection.options.fetch(:meta)
evaluate_association_value(association)
link = association.links.fetch(:self) link = association.links.fetch(:self)
assert_respond_to link, :call assert_respond_to link, :call
assert_respond_to reflection.options.fetch(:links).fetch(:self), :call assert_respond_to reflection.options.fetch(:links).fetch(:self), :call
@ -279,7 +305,7 @@ module ActiveModel
# Assert after instance_eval link # Assert after instance_eval link
assert_equal 'no_uri_validation', reflection.instance_eval(&link) assert_equal 'no_uri_validation', reflection.instance_eval(&link)
assert_equal @expected_meta, reflection.options.fetch(:meta) assert_equal @expected_meta, reflection.options.fetch(:meta)
assert_nil association.meta assert_equal @expected_meta, association.meta
end end
# rubocop:enable Metrics/AbcSize # rubocop:enable Metrics/AbcSize
@ -307,6 +333,9 @@ module ActiveModel
assert_nil reflection.options.fetch(:meta) assert_nil reflection.options.fetch(:meta)
# Assert before instance_eval link # Assert before instance_eval link
evaluate_association_value(association)
link = association.links.fetch(:self) link = association.links.fetch(:self)
assert_nil reflection.options.fetch(:meta) assert_nil reflection.options.fetch(:meta)
assert_respond_to link, :call assert_respond_to link, :call
@ -317,11 +346,11 @@ module ActiveModel
assert_respond_to association.links.fetch(:self), :call assert_respond_to association.links.fetch(:self), :call
# Assert before instance_eval link meta # Assert before instance_eval link meta
assert_respond_to reflection.options.fetch(:meta), :call assert_respond_to reflection.options.fetch(:meta), :call
assert_nil association.meta assert_respond_to association.meta, :call
# Assert after instance_eval link meta # Assert after instance_eval link meta
assert_equal @expected_meta, reflection.instance_eval(&reflection.options.fetch(:meta)) assert_equal @expected_meta, reflection.instance_eval(&reflection.options.fetch(:meta))
assert_nil association.meta assert_respond_to association.meta, :call
end end
# rubocop:enable Metrics/AbcSize # rubocop:enable Metrics/AbcSize
@ -342,6 +371,9 @@ module ActiveModel
# Build Association # Build Association
association = reflection.build_association(serializer_instance, @instance_options) association = reflection.build_association(serializer_instance, @instance_options)
# Assert before instance_eval link # Assert before instance_eval link
evaluate_association_value(association)
link = association.links.fetch(:self) link = association.links.fetch(:self)
assert_respond_to link, :call assert_respond_to link, :call
@ -365,6 +397,9 @@ module ActiveModel
reflection = serializer_class._reflections.fetch(:blog) reflection = serializer_class._reflections.fetch(:blog)
assert_nil reflection.options.fetch(:meta) assert_nil reflection.options.fetch(:meta)
association = reflection.build_association(serializer_instance, @instance_options) association = reflection.build_association(serializer_instance, @instance_options)
evaluate_association_value(association)
assert_equal model1_meta, association.meta assert_equal model1_meta, association.meta
assert_equal model1_meta, reflection.options.fetch(:meta) assert_equal model1_meta, reflection.options.fetch(:meta)
@ -380,6 +415,9 @@ module ActiveModel
assert_equal model1_meta, reflection.options.fetch(:meta) assert_equal model1_meta, reflection.options.fetch(:meta)
association = reflection.build_association(serializer_instance, @instance_options) association = reflection.build_association(serializer_instance, @instance_options)
evaluate_association_value(association)
assert_equal model2_meta, association.meta assert_equal model2_meta, association.meta
assert_equal model2_meta, reflection.options.fetch(:meta) assert_equal model2_meta, reflection.options.fetch(:meta)
end end