Skip to content

Commit 49eb531

Browse files
committed
Merge pull request #1239 from beauby/fix-duplicate-jsonapi
Fix duplicate resources inside included in compound document.
2 parents e4342aa + eccb359 commit 49eb531

File tree

3 files changed

+131
-16
lines changed

3 files changed

+131
-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: 112 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,112 @@ 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+
belongs_to :author
293+
end
294+
295+
class AuthorSerializer < ActiveModel::Serializer
296+
type 'authors'
297+
has_many :posts
298+
end
299+
300+
def setup
301+
@author = Author.new(id: 1, posts: [], roles: [], bio: nil)
302+
@post1 = Post.new(id: 1, author: @author)
303+
@post2 = Post.new(id: 2, author: @author)
304+
@author.posts << @post1
305+
@author.posts << @post2
306+
307+
@nestedpost1 = ::NestedPost.new(id: 1, nested_posts: [])
308+
@nestedpost2 = ::NestedPost.new(id: 2, nested_posts: [])
309+
@nestedpost1.nested_posts << @nestedpost1
310+
@nestedpost1.nested_posts << @nestedpost2
311+
@nestedpost2.nested_posts << @nestedpost1
312+
@nestedpost2.nested_posts << @nestedpost2
313+
end
314+
315+
def test_no_duplicates
316+
hash = ActiveModel::SerializableResource.new(@post1, adapter: :json_api,
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+
include: '*.*')
347+
.serializable_hash
348+
expected = [
349+
{
350+
type: 'authors', id: '1',
351+
relationships: {
352+
posts: {
353+
data: [
354+
{ type: 'posts', id: '1' },
355+
{ type: 'posts', id: '2' }
356+
]
357+
}
358+
}
359+
}
360+
]
361+
assert_equal(expected, hash[:included])
362+
end
363+
364+
def test_no_duplicates_global
365+
hash = ActiveModel::SerializableResource.new(
366+
@nestedpost1,
367+
adapter: :json_api,
368+
include: '*').serializable_hash
369+
expected = [
370+
type: 'nested_posts', id: '2',
371+
relationships: {
372+
nested_posts: {
373+
data: [
374+
{ type: 'nested_posts', id: '1' },
375+
{ type: 'nested_posts', id: '2' }
376+
]
377+
}
378+
}
379+
]
380+
assert_equal(expected, hash[:included])
381+
end
382+
383+
def test_no_duplicates_collection_global
384+
hash = ActiveModel::SerializableResource.new(
385+
[@nestedpost1, @nestedpost2],
386+
adapter: :json_api,
387+
include: '*').serializable_hash
388+
assert_nil(hash[:included])
389+
end
390+
end
280391
end
281392
end
282393
end

0 commit comments

Comments
 (0)