Skip to content

Commit aa4eb82

Browse files
authored
feat: support custom includes post-processing functions (#424)
1 parent 31ea497 commit aa4eb82

3 files changed

Lines changed: 91 additions & 9 deletions

File tree

lib/jsonapi/config.ex

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,35 @@
11
defmodule JSONAPI.Config do
22
@moduledoc """
33
Configuration struct containing JSON API information for a request
4+
5+
Much of the data in this struct is populated for you by various Plugs this
6+
library offers if you choose to use them.
7+
8+
`includes_post_processor`, if nil, will default to running all includes
9+
through `Enum.uniq/1`. You can customize this behavior if needed with a
10+
function that accepts two arguments: The includes about to be seriailzed and
11+
the requested includes for the current request. Your function must return the
12+
includes as you want them to be serialized.
413
"""
514

615
defstruct data: nil,
716
fields: %{},
817
filter: [],
918
include: [],
19+
includes_post_processor: nil,
1020
opts: nil,
1121
sort: nil,
1222
view: nil,
1323
page: %{}
1424

25+
@type requested_include :: atom | {atom, any}
26+
1527
@type t :: %__MODULE__{
1628
data: nil | map,
1729
fields: map,
1830
filter: keyword,
19-
include: [atom | {atom, any}],
31+
include: [requested_include],
32+
includes_post_processor: nil | (keyword(), [requested_include] -> keyword()),
2033
opts: nil | keyword,
2134
sort: nil | keyword,
2235
view: any,

lib/jsonapi/serializer.ex

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,30 @@ defmodule JSONAPI.Serializer do
1919
@spec serialize(View.t(), View.data(), Conn.t() | nil, View.meta() | nil, View.options()) ::
2020
document()
2121
def serialize(view, data, conn \\ nil, meta \\ nil, options \\ []) do
22-
{query_includes, query_page} =
22+
{query_includes, query_page, includes_post_processor} =
2323
case conn do
24-
%Conn{assigns: %{jsonapi_query: %Config{include: include, page: page}}} ->
25-
{include, page}
24+
%Conn{
25+
assigns: %{
26+
jsonapi_query: %Config{include: include, page: page, includes_post_processor: includes_post_processor}
27+
}
28+
} ->
29+
{include, page, includes_post_processor}
2630

2731
_ ->
28-
{[], nil}
32+
{[], nil, nil}
2933
end
3034

3135
{to_include, encoded_data} = encode_data(view, data, conn, query_includes, options)
3236

37+
post_process_includes =
38+
case includes_post_processor do
39+
nil -> &Enum.uniq/1
40+
process -> &process.(&1, query_includes)
41+
end
42+
3343
encoded_data = %{
3444
data: encoded_data,
35-
included: flatten_included(to_include)
45+
included: flatten_included(to_include, post_process_includes)
3646
}
3747

3848
encoded_data =
@@ -296,12 +306,12 @@ defmodule JSONAPI.Serializer do
296306
end
297307

298308
# Flatten and unique all the included objects
299-
@spec flatten_included(keyword()) :: keyword()
300-
def flatten_included(included) do
309+
@spec flatten_included(keyword(), (keyword() -> keyword())) :: keyword()
310+
def flatten_included(included, post_process) do
301311
included
302312
|> List.flatten()
303313
|> Enum.reject(&is_nil/1)
304-
|> Enum.uniq()
314+
|> post_process.()
305315
end
306316

307317
defp assoc_loaded?(nil), do: serialize_nil_relationships?()

test/jsonapi/serializer_test.exs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,65 @@ defmodule JSONAPI.SerializerTest do
618618
assert Enum.count(encoded.included) == 4
619619
end
620620

621+
test "serialize uses a custom include post-processing function if provided" do
622+
data = %{
623+
id: 1,
624+
username: "jim",
625+
first_name: "Jimmy",
626+
last_name: "Beam",
627+
company: %{id: 2, name: "acme", industry: %{id: 4, name: "stuff"}}
628+
}
629+
630+
conn =
631+
Plug.Conn.fetch_query_params(%Plug.Conn{
632+
assigns: %{
633+
jsonapi_query: %Config{
634+
include: [company: :industry],
635+
includes_post_processor: fn included, requested ->
636+
assert requested == [company: :industry]
637+
Enum.filter(included, fn i -> i[:id] != "2" end)
638+
end
639+
}
640+
}
641+
})
642+
643+
encoded = Serializer.serialize(UserView, data, conn)
644+
645+
assert Enum.count(encoded.included) == 1
646+
end
647+
648+
test "serialize deduplicates includes by default" do
649+
data = [
650+
%{
651+
id: 1,
652+
username: "jim",
653+
first_name: "Jimmy",
654+
last_name: "Beam",
655+
company: %{id: 2, name: "acme", industry: %{id: 4, name: "stuff"}}
656+
},
657+
%{
658+
id: 2,
659+
username: "jim",
660+
first_name: "Jimmy",
661+
last_name: "Beam",
662+
company: %{id: 3, name: "globex", industry: %{id: 4, name: "stuff"}}
663+
}
664+
]
665+
666+
conn =
667+
Plug.Conn.fetch_query_params(%Plug.Conn{
668+
assigns: %{
669+
jsonapi_query: %Config{
670+
include: [company: :industry]
671+
}
672+
}
673+
})
674+
675+
encoded = Serializer.serialize(UserView, data, conn)
676+
677+
assert Enum.count(encoded.included) == 3
678+
end
679+
621680
test "includes from the query when not included by default" do
622681
data = %{
623682
id: 1,

0 commit comments

Comments
 (0)