Skip to content

Commit f10e33d

Browse files
committed
Improve how avoid duplicate includes
1 parent 2f4e8ba commit f10e33d

2 files changed

Lines changed: 89 additions & 18 deletions

File tree

‎lib/hawk/json_api/document.ex‎

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -238,10 +238,7 @@ defmodule Hawk.JsonApi.Document do
238238
defp put_included(document, models, opts) do
239239
primary_resources = MapSet.new(models, &resource_identity/1)
240240

241-
included =
242-
models
243-
|> included_resources(Keyword.get(opts, :preloads, []), opts)
244-
|> Enum.reject(&MapSet.member?(primary_resources, {&1.type, &1.id}))
241+
{included, _seen} = included_resources(models, Keyword.get(opts, :preloads, []), opts, primary_resources)
245242

246243
if included == [] do
247244
document
@@ -254,28 +251,40 @@ defmodule Hawk.JsonApi.Document do
254251
{Schema.metadata(model).type, to_string(Map.get(model, Schema.identity(model)))}
255252
end
256253

257-
defp included_resources(models, preloads, opts) do
258-
models
259-
|> Enum.flat_map(&included_resources_for_model(&1, preloads, opts))
260-
|> Enum.uniq_by(&{&1.type, &1.id})
254+
defp included_resources(models, preloads, opts, seen) do
255+
Enum.reduce(models, {[], seen}, fn model, {included, seen} ->
256+
{model_included, seen} = included_resources_for_model(model, preloads, opts, seen)
257+
{included ++ model_included, seen}
258+
end)
261259
end
262260

263-
defp included_resources_for_model(model, preloads, opts) do
264-
Enum.flat_map(preloads, fn
265-
name when is_atom(name) ->
266-
direct_included_resources(model, name, [], opts)
261+
defp included_resources_for_model(model, preloads, opts, seen) do
262+
Enum.reduce(preloads, {[], seen}, fn
263+
name, {included, seen} when is_atom(name) ->
264+
{direct, seen} = direct_included_resources(model, name, [], opts, seen)
265+
{included ++ direct, seen}
267266

268-
{name, nested} when is_atom(name) ->
269-
direct_included_resources(model, name, nested, opts)
267+
{name, nested}, {included, seen} when is_atom(name) ->
268+
{direct, seen} = direct_included_resources(model, name, nested, opts, seen)
269+
{included ++ direct, seen}
270270
end)
271271
end
272272

273-
defp direct_included_resources(model, name, nested, opts) do
273+
defp direct_included_resources(model, name, nested, opts, seen) do
274274
model
275275
|> related_models(name)
276-
|> Enum.flat_map(fn related ->
277-
[resource_object(related, Keyword.put(opts, :preloads, nested))] ++
278-
included_resources_for_model(related, nested, opts)
276+
|> Enum.reduce({[], seen}, fn related, {included, seen} ->
277+
identity = resource_identity(related)
278+
279+
if MapSet.member?(seen, identity) do
280+
{nested_included, seen} = included_resources_for_model(related, nested, opts, seen)
281+
{included ++ nested_included, seen}
282+
else
283+
seen = MapSet.put(seen, identity)
284+
object = resource_object(related, Keyword.put(opts, :preloads, nested))
285+
{nested_included, seen} = included_resources_for_model(related, nested, opts, seen)
286+
{included ++ [object | nested_included], seen}
287+
end
279288
end)
280289
end
281290

‎test/hawk/json_api/document_test.exs‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,44 @@
1+
defmodule Hawk.JsonApi.DocumentTest.Author do
2+
@moduledoc false
3+
4+
use Hawk.Model
5+
use Hawk.JsonApi.Resource
6+
7+
model "document_test_authors" do
8+
field(:name, :string)
9+
end
10+
11+
type("document-test-authors")
12+
attribute(:name, resolver: &__MODULE__.render_name/1)
13+
14+
def render_name(%{name: :raise_if_rendered}) do
15+
raise "duplicate author was rendered"
16+
end
17+
18+
def render_name(author), do: author.name
19+
end
20+
21+
defmodule Hawk.JsonApi.DocumentTest.Article do
22+
@moduledoc false
23+
24+
use Hawk.Model
25+
use Hawk.JsonApi.Resource
26+
27+
model "document_test_articles" do
28+
field(:title, :string)
29+
belongs_to(:author, Hawk.JsonApi.DocumentTest.Author)
30+
end
31+
32+
type("document-test-articles")
33+
attribute(:title, [])
34+
relationship(:author, [])
35+
end
36+
137
defmodule Hawk.JsonApi.DocumentTest do
238
use ExUnit.Case, async: true
339

440
alias Hawk.JsonApi.Document
41+
alias Hawk.JsonApi.DocumentTest.{Article, Author}
542

643
test "renders a collection without a json_api_by_model override, resolving per record" do
744
courses = [
@@ -21,6 +58,31 @@ defmodule Hawk.JsonApi.DocumentTest do
2158
assert Enum.map(document.data, & &1.attributes.title) == ["Math", "History"]
2259
end
2360

61+
test "renders duplicate included resources once" do
62+
author_id = "00000000-0000-0000-0000-000000000021"
63+
author = %Author{id: author_id, name: "Ada"}
64+
duplicate_author = %Author{id: author_id, name: :raise_if_rendered}
65+
66+
articles = [
67+
%Article{
68+
id: "00000000-0000-0000-0000-000000000001",
69+
title: "One",
70+
author_id: author.id,
71+
author: author
72+
},
73+
%Article{
74+
id: "00000000-0000-0000-0000-000000000002",
75+
title: "Two",
76+
author_id: duplicate_author.id,
77+
author: duplicate_author
78+
}
79+
]
80+
81+
document = Document.document(articles, preloads: [:author])
82+
83+
assert [%{type: "document-test-authors", id: ^author_id}] = document.included
84+
end
85+
2486
test "renders sparse fieldsets for attributes and relationships" do
2587
course = %Videdal.Course{
2688
id: "00000000-0000-0000-0000-000000000007",

0 commit comments

Comments
 (0)