diff --git a/lib/jsonapi/config.ex b/lib/jsonapi/config.ex index 0e7a3811..12ef2fbc 100644 --- a/lib/jsonapi/config.ex +++ b/lib/jsonapi/config.ex @@ -1,22 +1,35 @@ defmodule JSONAPI.Config do @moduledoc """ Configuration struct containing JSON API information for a request + + Much of the data in this struct is populated for you by various Plugs this + library offers if you choose to use them. + + `includes_post_processor`, if nil, will default to running all includes + through `Enum.uniq/1`. You can customize this behavior if needed with a + function that accepts two arguments: The includes about to be seriailzed and + the requested includes for the current request. Your function must return the + includes as you want them to be serialized. """ defstruct data: nil, fields: %{}, filter: [], include: [], + includes_post_processor: nil, opts: nil, sort: nil, view: nil, page: %{} + @type requested_include :: atom | {atom, any} + @type t :: %__MODULE__{ data: nil | map, fields: map, filter: keyword, - include: [atom | {atom, any}], + include: [requested_include], + includes_post_processor: nil | (keyword(), [requested_include] -> keyword()), opts: nil | keyword, sort: nil | keyword, view: any, diff --git a/lib/jsonapi/serializer.ex b/lib/jsonapi/serializer.ex index d855f527..0dfeb72d 100644 --- a/lib/jsonapi/serializer.ex +++ b/lib/jsonapi/serializer.ex @@ -19,20 +19,30 @@ defmodule JSONAPI.Serializer do @spec serialize(View.t(), View.data(), Conn.t() | nil, View.meta() | nil, View.options()) :: document() def serialize(view, data, conn \\ nil, meta \\ nil, options \\ []) do - {query_includes, query_page} = + {query_includes, query_page, includes_post_processor} = case conn do - %Conn{assigns: %{jsonapi_query: %Config{include: include, page: page}}} -> - {include, page} + %Conn{ + assigns: %{ + jsonapi_query: %Config{include: include, page: page, includes_post_processor: includes_post_processor} + } + } -> + {include, page, includes_post_processor} _ -> - {[], nil} + {[], nil, nil} end {to_include, encoded_data} = encode_data(view, data, conn, query_includes, options) + post_process_includes = + case includes_post_processor do + nil -> &Enum.uniq/1 + process -> &process.(&1, query_includes) + end + encoded_data = %{ data: encoded_data, - included: flatten_included(to_include) + included: flatten_included(to_include, post_process_includes) } encoded_data = @@ -296,12 +306,12 @@ defmodule JSONAPI.Serializer do end # Flatten and unique all the included objects - @spec flatten_included(keyword()) :: keyword() - def flatten_included(included) do + @spec flatten_included(keyword(), (keyword() -> keyword())) :: keyword() + def flatten_included(included, post_process) do included |> List.flatten() |> Enum.reject(&is_nil/1) - |> Enum.uniq() + |> post_process.() end defp assoc_loaded?(nil), do: serialize_nil_relationships?() diff --git a/test/jsonapi/serializer_test.exs b/test/jsonapi/serializer_test.exs index 5fb62c98..38579a92 100644 --- a/test/jsonapi/serializer_test.exs +++ b/test/jsonapi/serializer_test.exs @@ -618,6 +618,65 @@ defmodule JSONAPI.SerializerTest do assert Enum.count(encoded.included) == 4 end + test "serialize uses a custom include post-processing function if provided" do + data = %{ + id: 1, + username: "jim", + first_name: "Jimmy", + last_name: "Beam", + company: %{id: 2, name: "acme", industry: %{id: 4, name: "stuff"}} + } + + conn = + Plug.Conn.fetch_query_params(%Plug.Conn{ + assigns: %{ + jsonapi_query: %Config{ + include: [company: :industry], + includes_post_processor: fn included, requested -> + assert requested == [company: :industry] + Enum.filter(included, fn i -> i[:id] != "2" end) + end + } + } + }) + + encoded = Serializer.serialize(UserView, data, conn) + + assert Enum.count(encoded.included) == 1 + end + + test "serialize deduplicates includes by default" do + data = [ + %{ + id: 1, + username: "jim", + first_name: "Jimmy", + last_name: "Beam", + company: %{id: 2, name: "acme", industry: %{id: 4, name: "stuff"}} + }, + %{ + id: 2, + username: "jim", + first_name: "Jimmy", + last_name: "Beam", + company: %{id: 3, name: "globex", industry: %{id: 4, name: "stuff"}} + } + ] + + conn = + Plug.Conn.fetch_query_params(%Plug.Conn{ + assigns: %{ + jsonapi_query: %Config{ + include: [company: :industry] + } + } + }) + + encoded = Serializer.serialize(UserView, data, conn) + + assert Enum.count(encoded.included) == 3 + end + test "includes from the query when not included by default" do data = %{ id: 1,