-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix duplicate resources inside included in compound document. #1239
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -81,16 +81,18 @@ def fragment_cache(cached_hash, non_cached_hash) | |
|
||
def serializable_hash_for_collection(options) | ||
hash = { data: [] } | ||
included = [] | ||
serializer.each do |s| | ||
result = self.class.new(s, instance_options.merge(fieldset: fieldset)).serializable_hash(options) | ||
hash[:data] << result[:data] | ||
next unless result[:included] | ||
|
||
if result[:included] | ||
hash[:included] ||= [] | ||
hash[:included] |= result[:included] | ||
end | ||
included |= result[:included] | ||
end | ||
|
||
included.delete_if { |resource| hash[:data].include?(resource) } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see we are deleting here the duplicated resource, but it seems hacky, it's not addressing the problem itself, just working around it, did you found the main issue causing this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is a structural issue. When the primary resource is a single resource ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. got it 😉 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. deletes resource from this included stuff is getting messy. needs a refactor before it sends even more vines through the class There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe it is the best we can achieve without refactoring the adapter so that it does not call itself recursively for collections (which I'm in favor of, in a subsequent PR). The scope of this PR was to fix a behavior breaking JSON API spec, without modifying the adapter too much. |
||
hash[:included] = included if included.any? | ||
|
||
if serializer.paginated? | ||
hash[:links] ||= {} | ||
hash[:links].update(links_for(serializer, options)) | ||
|
@@ -102,9 +104,10 @@ def serializable_hash_for_collection(options) | |
def serializable_hash_for_single_resource | ||
primary_data = primary_data_for(serializer) | ||
relationships = relationships_for(serializer) | ||
included = included_resources(@include_tree) | ||
primary_data[:relationships] = relationships if relationships.any? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. simple refactor, nice |
||
hash = { data: primary_data } | ||
hash[:data][:relationships] = relationships if relationships.any? | ||
|
||
included = included_resources(@include_tree, [primary_data]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this is to ensure, preemptively, that we do not include any of the primary data in the |
||
hash[:included] = included if included.any? | ||
|
||
hash | ||
|
@@ -171,31 +174,31 @@ def relationships_for(serializer) | |
end | ||
end | ||
|
||
def included_resources(include_tree) | ||
def included_resources(include_tree, primary_data) | ||
included = [] | ||
|
||
serializer.associations(include_tree).each do |association| | ||
add_included_resources_for(association.serializer, include_tree[association.key], included) | ||
add_included_resources_for(association.serializer, include_tree[association.key], primary_data, included) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and |
||
end | ||
|
||
included | ||
end | ||
|
||
def add_included_resources_for(serializer, include_tree, included) | ||
def add_included_resources_for(serializer, include_tree, primary_data, included) | ||
if serializer.respond_to?(:each) | ||
serializer.each { |s| add_included_resources_for(s, include_tree, included) } | ||
serializer.each { |s| add_included_resources_for(s, include_tree, primary_data, included) } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and primary_data (which is |
||
else | ||
return unless serializer && serializer.object | ||
|
||
primary_data = primary_data_for(serializer) | ||
resource_object = primary_data_for(serializer) | ||
relationships = relationships_for(serializer) | ||
primary_data[:relationships] = relationships if relationships.any? | ||
resource_object[:relationships] = relationships if relationships.any? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice refactor |
||
|
||
return if included.include?(primary_data) | ||
included.push(primary_data) | ||
return if included.include?(resource_object) || primary_data.include?(resource_object) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And we add ( which is true when i.e. primary data is for same serializer) as condition to return early |
||
included.push(resource_object) | ||
|
||
serializer.associations(include_tree).each do |association| | ||
add_included_resources_for(association.serializer, include_tree[association.key], included) | ||
add_included_resources_for(association.serializer, include_tree[association.key], primary_data, included) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and pass along our |
||
end | ||
end | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,16 @@ | ||
require 'test_helper' | ||
|
||
NestedPost = Class.new(Model) | ||
class NestedPostSerializer < ActiveModel::Serializer | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd say because this model and serializer are created here, and specific to this test, they should be namespaced under the test. :-\ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we agree we'll start doing that soon There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Let's leave them there for now, and take care of it during the Great Test Refactor. |
||
has_many :nested_posts | ||
end | ||
|
||
module ActiveModel | ||
class Serializer | ||
module Adapter | ||
class JsonApi | ||
class LinkedTest < Minitest::Test | ||
def setup | ||
ActionController::Base.cache_store.clear | ||
@author1 = Author.new(id: 1, name: 'Steve K.') | ||
@author2 = Author.new(id: 2, name: 'Tenderlove') | ||
@bio1 = Bio.new(id: 1, content: 'AMS Contributor') | ||
|
@@ -277,6 +282,112 @@ def test_nil_link_with_specified_serializer | |
assert_equal expected, adapter.serializable_hash | ||
end | ||
end | ||
|
||
class NoDuplicatesTest < Minitest::Test | ||
Post = Class.new(::Model) | ||
Author = Class.new(::Model) | ||
|
||
class PostSerializer < ActiveModel::Serializer | ||
type 'posts' | ||
belongs_to :author | ||
end | ||
|
||
class AuthorSerializer < ActiveModel::Serializer | ||
type 'authors' | ||
has_many :posts | ||
end | ||
|
||
def setup | ||
@author = Author.new(id: 1, posts: [], roles: [], bio: nil) | ||
@post1 = Post.new(id: 1, author: @author) | ||
@post2 = Post.new(id: 2, author: @author) | ||
@author.posts << @post1 | ||
@author.posts << @post2 | ||
|
||
@nestedpost1 = ::NestedPost.new(id: 1, nested_posts: []) | ||
@nestedpost2 = ::NestedPost.new(id: 2, nested_posts: []) | ||
@nestedpost1.nested_posts << @nestedpost1 | ||
@nestedpost1.nested_posts << @nestedpost2 | ||
@nestedpost2.nested_posts << @nestedpost1 | ||
@nestedpost2.nested_posts << @nestedpost2 | ||
end | ||
|
||
def test_no_duplicates | ||
hash = ActiveModel::SerializableResource.new(@post1, adapter: :json_api, | ||
include: '*.*') | ||
.serializable_hash | ||
expected = [ | ||
{ | ||
type: 'authors', id: '1', | ||
relationships: { | ||
posts: { | ||
data: [ | ||
{ type: 'posts', id: '1' }, | ||
{ type: 'posts', id: '2' } | ||
] | ||
} | ||
} | ||
}, | ||
{ | ||
type: 'posts', id: '2', | ||
relationships: { | ||
author: { | ||
data: { type: 'authors', id: '1' } | ||
} | ||
} | ||
} | ||
] | ||
assert_equal(expected, hash[:included]) | ||
end | ||
|
||
def test_no_duplicates_collection | ||
hash = ActiveModel::SerializableResource.new( | ||
[@post1, @post2], adapter: :json_api, | ||
include: '*.*') | ||
.serializable_hash | ||
expected = [ | ||
{ | ||
type: 'authors', id: '1', | ||
relationships: { | ||
posts: { | ||
data: [ | ||
{ type: 'posts', id: '1' }, | ||
{ type: 'posts', id: '2' } | ||
] | ||
} | ||
} | ||
} | ||
] | ||
assert_equal(expected, hash[:included]) | ||
end | ||
|
||
def test_no_duplicates_global | ||
hash = ActiveModel::SerializableResource.new( | ||
@nestedpost1, | ||
adapter: :json_api, | ||
include: '*').serializable_hash | ||
expected = [ | ||
type: 'nested_posts', id: '2', | ||
relationships: { | ||
nested_posts: { | ||
data: [ | ||
{ type: 'nested_posts', id: '1' }, | ||
{ type: 'nested_posts', id: '2' } | ||
] | ||
} | ||
} | ||
] | ||
assert_equal(expected, hash[:included]) | ||
end | ||
|
||
def test_no_duplicates_collection_global | ||
hash = ActiveModel::SerializableResource.new( | ||
[@nestedpost1, @nestedpost2], | ||
adapter: :json_api, | ||
include: '*').serializable_hash | ||
assert_nil(hash[:included]) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
refactor