Skip to content

Commit f4bb4c8

Browse files
committed
Fix duplicate resources between data and included.
1 parent d02cd30 commit f4bb4c8

File tree

3 files changed

+132
-16
lines changed

3 files changed

+132
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Features:
2121
- [#1050](https://github.com/rails-api/active_model_serializers/pull/1050) Add support for toplevel jsonapi member (@beauby, @bf4)
2222

2323
Fixes:
24+
- [#1239](https://github.com/rails-api/active_model_serializers/pull/1239) Fix duplicates in JSON API compound documents (@beauby)
2425
- [#1214](https://github.com/rails-api/active_model_serializers/pull/1214) retrieve the key from the reflection options when building associations (@NullVoxPopuli, @hut8)
2526

2627
Misc:

lib/active_model/serializer/adapter/json_api.rb

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,18 @@ def fragment_cache(cached_hash, non_cached_hash)
8181

8282
def serializable_hash_for_collection(options)
8383
hash = { data: [] }
84+
included = []
8485
serializer.each do |s|
8586
result = self.class.new(s, instance_options.merge(fieldset: fieldset)).serializable_hash(options)
8687
hash[:data] << result[:data]
88+
next unless result[:included]
8789

88-
if result[:included]
89-
hash[:included] ||= []
90-
hash[:included] |= result[:included]
91-
end
90+
included |= result[:included]
9291
end
9392

93+
included.delete_if { |resource| hash[:data].include?(resource) }
94+
hash[:included] = included if included.any?
95+
9496
if serializer.paginated?
9597
hash[:links] ||= {}
9698
hash[:links].update(links_for(serializer, options))
@@ -102,9 +104,10 @@ def serializable_hash_for_collection(options)
102104
def serializable_hash_for_single_resource
103105
primary_data = primary_data_for(serializer)
104106
relationships = relationships_for(serializer)
105-
included = included_resources(@include_tree)
107+
primary_data[:relationships] = relationships if relationships.any?
106108
hash = { data: primary_data }
107-
hash[:data][:relationships] = relationships if relationships.any?
109+
110+
included = included_resources(@include_tree, [primary_data])
108111
hash[:included] = included if included.any?
109112

110113
hash
@@ -171,31 +174,31 @@ def relationships_for(serializer)
171174
end
172175
end
173176

174-
def included_resources(include_tree)
177+
def included_resources(include_tree, primary_data)
175178
included = []
176179

177180
serializer.associations(include_tree).each do |association|
178-
add_included_resources_for(association.serializer, include_tree[association.key], included)
181+
add_included_resources_for(association.serializer, include_tree[association.key], primary_data, included)
179182
end
180183

181184
included
182185
end
183186

184-
def add_included_resources_for(serializer, include_tree, included)
187+
def add_included_resources_for(serializer, include_tree, primary_data, included)
185188
if serializer.respond_to?(:each)
186-
serializer.each { |s| add_included_resources_for(s, include_tree, included) }
189+
serializer.each { |s| add_included_resources_for(s, include_tree, primary_data, included) }
187190
else
188191
return unless serializer && serializer.object
189192

190-
primary_data = primary_data_for(serializer)
193+
resource_object = primary_data_for(serializer)
191194
relationships = relationships_for(serializer)
192-
primary_data[:relationships] = relationships if relationships.any?
195+
resource_object[:relationships] = relationships if relationships.any?
193196

194-
return if included.include?(primary_data)
195-
included.push(primary_data)
197+
return if included.include?(resource_object) || primary_data.include?(resource_object)
198+
included.push(resource_object)
196199

197200
serializer.associations(include_tree).each do |association|
198-
add_included_resources_for(association.serializer, include_tree[association.key], included)
201+
add_included_resources_for(association.serializer, include_tree[association.key], primary_data, included)
199202
end
200203
end
201204
end

test/adapter/json_api/linked_test.rb

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
require 'test_helper'
2+
3+
NestedPost = Class.new(Model)
4+
class NestedPostSerializer < ActiveModel::Serializer
5+
has_many :nested_posts
6+
end
7+
28
module ActiveModel
39
class Serializer
410
module Adapter
511
class JsonApi
612
class LinkedTest < Minitest::Test
713
def setup
8-
ActionController::Base.cache_store.clear
914
@author1 = Author.new(id: 1, name: 'Steve K.')
1015
@author2 = Author.new(id: 2, name: 'Tenderlove')
1116
@bio1 = Bio.new(id: 1, content: 'AMS Contributor')
@@ -277,6 +282,113 @@ def test_nil_link_with_specified_serializer
277282
assert_equal expected, adapter.serializable_hash
278283
end
279284
end
285+
286+
class NoDuplicatesTest < Minitest::Test
287+
Post = Class.new(::Model)
288+
Author = Class.new(::Model)
289+
290+
class PostSerializer < ActiveModel::Serializer
291+
type 'posts'
292+
class AuthorSerializer < ActiveModel::Serializer
293+
type 'authors'
294+
has_many :posts, serializer: PostSerializer
295+
end
296+
belongs_to :author, serializer: AuthorSerializer
297+
end
298+
299+
def setup
300+
@author = Author.new(id: 1, posts: [], roles: [], bio: nil)
301+
@post1 = Post.new(id: 1, author: @author)
302+
@post2 = Post.new(id: 2, author: @author)
303+
@author.posts << @post1
304+
@author.posts << @post2
305+
306+
@nestedpost1 = ::NestedPost.new(id: 1, nested_posts: [])
307+
@nestedpost2 = ::NestedPost.new(id: 2, nested_posts: [])
308+
@nestedpost1.nested_posts << @nestedpost1
309+
@nestedpost1.nested_posts << @nestedpost2
310+
@nestedpost2.nested_posts << @nestedpost1
311+
@nestedpost2.nested_posts << @nestedpost2
312+
end
313+
314+
def test_no_duplicates
315+
hash = ActiveModel::SerializableResource.new(@post1, adapter: :json_api,
316+
serializer: PostSerializer,
317+
include: '*.*')
318+
.serializable_hash
319+
expected = [
320+
{
321+
type: 'authors', id: '1',
322+
relationships: {
323+
posts: {
324+
data: [
325+
{ type: 'posts', id: '1' },
326+
{ type: 'posts', id: '2' }
327+
]
328+
}
329+
}
330+
},
331+
{
332+
type: 'posts', id: '2',
333+
relationships: {
334+
author: {
335+
data: { type: 'authors', id: '1' }
336+
}
337+
}
338+
}
339+
]
340+
assert_equal(expected, hash[:included])
341+
end
342+
343+
def test_no_duplicates_collection
344+
hash = ActiveModel::SerializableResource.new(
345+
[@post1, @post2], adapter: :json_api,
346+
each_serializer: PostSerializer,
347+
include: '*.*')
348+
.serializable_hash
349+
expected = [
350+
{
351+
type: 'authors', id: '1',
352+
relationships: {
353+
posts: {
354+
data: [
355+
{ type: 'posts', id: '1' },
356+
{ type: 'posts', id: '2' }
357+
]
358+
}
359+
}
360+
}
361+
]
362+
assert_equal(expected, hash[:included])
363+
end
364+
365+
def test_no_duplicates_global
366+
hash = ActiveModel::SerializableResource.new(
367+
@nestedpost1,
368+
adapter: :json_api,
369+
include: '*').serializable_hash
370+
expected = [
371+
type: 'nested_posts', id: '2',
372+
relationships: {
373+
nested_posts: {
374+
data: [
375+
{ type: 'nested_posts', id: '1' },
376+
{ type: 'nested_posts', id: '2' }
377+
]
378+
}
379+
}
380+
]
381+
assert_equal(expected, hash[:included])
382+
end
383+
384+
def test_no_duplicates_collection_global
385+
hash = ActiveModel::SerializableResource.new(
386+
[@nestedpost1, @nestedpost2],
387+
adapter: :json_api,
388+
include: '*').serializable_hash
389+
assert_nil(hash[:included])
390+
end
391+
end
280392
end
281393
end
282394
end

0 commit comments

Comments
 (0)