diff --git a/README.md b/README.md index 2fb34e4e..01c6716d 100644 --- a/README.md +++ b/README.md @@ -198,8 +198,9 @@ config :jsonapi, scheme: "https", namespace: "/api", field_transformation: :underscore, - remove_links: false, serialize_nil_relationships: false, + remove_links: false, + add_auto_links: false, json_library: Jason, paginator: nil ``` @@ -218,12 +219,20 @@ config :jsonapi, `:dasherize`. If your API uses underscores (e.g. `"favorite_color": "red"`) set to `:underscore`. To transform only the top-level field keys, use `:camelize_shallow` or `:dasherize_shallow`. -- **remove_links**. `links` data can optionally be removed from the payload via - setting the configuration above to `true`. Defaults to `false`. - **serialize_nil_relationships**. By default, relationships on a resource that are `nil` will be omitted during serialization. Setting this to `true` will serialize these relationships, provided they are loaded on the resource. Defaults to `false`. +- **remove_links**. `links` data can optionally be removed from the payload via + setting the configuration above to `true`. Defaults to `false`. This setting + removes all links, not just auto-added links. +- **add_auto_links**. `links` data can optionally be auto-added by + setting the configuration above to `true`. Defaults to `true`. This setting is + overruled by `remove_links` for backwards compatibility -- setting both to + `true` results in adding links and then removing them. Only setting + `add_auto_links` to `true` (and leaving `remove_links` `false` as is its + default) will serialize your explicitly created links but not add any + automatically generated links on top. - **json_library**. Defaults to [Jason](https://hex.pm/packages/jason). - **paginator**. Module implementing pagination links generation. Defaults to `nil`. diff --git a/lib/jsonapi/serializer.ex b/lib/jsonapi/serializer.ex index d76be9ae..69a7dbff 100644 --- a/lib/jsonapi/serializer.ex +++ b/lib/jsonapi/serializer.ex @@ -42,7 +42,7 @@ defmodule JSONAPI.Serializer do encoded_data end - merge_links(encoded_data, data, view, conn, query_page, remove_links?(), options) + merge_links(encoded_data, data, view, conn, query_page, add_auto_links?(), options, true) end def encode_data(_view, nil, _conn, _query_includes, _options), do: {[], nil} @@ -67,7 +67,7 @@ defmodule JSONAPI.Serializer do relationships: %{} } - doc = merge_links(encoded_data, data, view, conn, nil, remove_links?(), options) + doc = merge_links(encoded_data, data, view, conn, nil, add_auto_links?(), options, false) doc = case view.meta(data, conn) do @@ -210,42 +210,76 @@ defmodule JSONAPI.Serializer do data: encode_rel_data(rel_view, rel_data) } - merge_related_links(data, info, remove_links?()) + merge_related_links(data, info, add_auto_links?()) end - defp merge_base_links(%{links: links} = doc, data, view, conn) do - view_links = data |> view.links(conn) |> Map.merge(links) - Map.merge(doc, %{links: view_links}) + defp merge_links(doc, data, view, conn, _page, false, _options, is_root_of_doc) do + merge_view_links(doc, data, view, conn, is_root_of_doc) end - defp merge_links(doc, data, view, conn, page, false, options) when is_list(data) do - links = + defp merge_links(doc, data, view, conn, page, true, options, is_root_of_doc) do + doc + |> merge_auto_links(data, view, conn, page, options) + |> merge_view_links(data, view, conn, is_root_of_doc) + end + + defp merge_view_links(doc, data, view, conn, is_root_of_doc) + + defp merge_view_links(%{links: links} = doc, data, view, conn, false) do + # intentionally take the view.links and merge them into the existing links, + # allowing the custom defined links for the view to override any auto-links + # generated by this library UNLESS configured to remove links (backwards + # compatibility): + view_links = + if remove_links?() do + %{} + else + Map.merge(links, view.links(data, conn)) + end + + new_doc = Map.merge(doc, %{links: view_links}) + + case new_doc do + %{links: links} when links == %{} -> + Map.delete(doc, :links) + + doc -> + doc + end + end + + defp merge_view_links(doc, data, view, conn, false), + do: merge_view_links(Map.merge(doc, %{links: %{}}), data, view, conn, false) + + defp merge_view_links(doc, _data, _view, _conn, true), do: doc + + defp merge_auto_links(doc, data, view, conn, page, options) + when is_list(data) do + auto_links = data |> view.pagination_links(conn, page, options) - |> Map.merge(%{self: view.url_for_pagination(data, conn, page)}) + |> Map.merge(%{ + self: view.url_for_pagination(data, conn, page) + }) - doc - |> Map.merge(%{links: links}) - |> merge_base_links(data, view, conn) + Map.merge(doc, %{links: auto_links}) end - defp merge_links(doc, data, view, conn, _page, false, _options) do - doc - |> Map.merge(%{links: %{self: view.url_for(data, conn)}}) - |> merge_base_links(data, view, conn) - end + defp merge_auto_links(doc, data, view, conn, _page, _options) do + auto_links = %{self: view.url_for(data, conn)} - defp merge_links(doc, _data, _view, _conn, _page, _remove_links, _options), do: doc + Map.merge(doc, %{links: auto_links}) + end defp merge_related_links( encoded_data, {rel_view, rel_data, rel_url, conn}, - false = _remove_links + true = _add_auto_links ) do Map.merge(encoded_data, %{links: %{self: rel_url, related: rel_view.url_for(rel_data, conn)}}) end - defp merge_related_links(encoded_rel_data, _info, _remove_links), do: encoded_rel_data + defp merge_related_links(encoded_rel_data, _info, _add_auto_links), do: encoded_rel_data @spec encode_rel_data(module(), map() | list()) :: map() | nil def encode_rel_data(_view, nil), do: nil @@ -302,7 +336,13 @@ defmodule JSONAPI.Serializer do |> List.flatten() end - defp remove_links?, do: Application.get_env(:jsonapi, :remove_links, false) + defp remove_links? do + Application.get_env(:jsonapi, :remove_links, false) + end + + defp add_auto_links? do + Application.get_env(:jsonapi, :add_auto_links, !remove_links?()) + end @spec serialize_nil_relationships? :: boolean() defp serialize_nil_relationships?, do: Application.get_env(:jsonapi, :serialize_nil_relationships, false) diff --git a/test/jsonapi/serializer_test.exs b/test/jsonapi/serializer_test.exs index 262bc0d2..a7fbc35c 100644 --- a/test/jsonapi/serializer_test.exs +++ b/test/jsonapi/serializer_test.exs @@ -19,6 +19,27 @@ defmodule JSONAPI.SerializerTest do end end + defmodule PostViewWithLinks do + use JSONAPI.View + + def fields, do: [:text, :body, :full_description, :inserted_at] + def meta(data, _conn), do: %{meta_text: "meta_#{data[:text]}"} + def type, do: "mytype" + + def relationships do + [ + author: {JSONAPI.SerializerTest.UserView, :include}, + best_comments: {JSONAPI.SerializerTest.CommentView, :include} + ] + end + + def links(_data, _conn) do + %{ + self: "https://website.com/api/posts/1" + } + end + end + defmodule PageBasedPaginator do @moduledoc """ Page based pagination strategy @@ -867,6 +888,97 @@ defmodule JSONAPI.SerializerTest do Application.delete_env(:jsonapi, :remove_links) end + describe "when configured to not add auto links" do + setup do + Application.put_env(:jsonapi, :add_auto_links, false) + + on_exit(fn -> + Application.delete_env(:jsonapi, :add_auto_links) + end) + + {:ok, []} + end + + test "serialize does not include auto links" do + data = %{ + id: 1, + text: "Hello", + body: "Hello world", + full_description: "This_is_my_description", + author: %{id: 2, username: "jbonds", first_name: "jerry", last_name: "bonds"}, + best_comments: [ + %{ + id: 5, + text: "greatest comment ever", + user: %{id: 4, username: "jack", last_name: "bronds"} + } + ] + } + + encoded = Serializer.serialize(PostView, data, nil) + + relationships = encoded[:data][:relationships] + + refute relationships[:links] + refute encoded[:data][:links] + refute encoded[:links] + end + + test "serialize still adds custom links" do + data = %{ + id: 1, + text: "Hello", + body: "Hello world", + full_description: "This_is_my_description", + author: %{id: 2, username: "jbonds", first_name: "jerry", last_name: "bonds"}, + best_comments: [ + %{ + id: 5, + text: "greatest comment ever", + user: %{id: 4, username: "jack", last_name: "bronds"} + } + ] + } + + encoded = Serializer.serialize(PostViewWithLinks, data, nil) + + relationships = encoded[:data][:relationships] + + refute relationships[:links] + assert encoded[:data][:links][:self] == "https://website.com/api/posts/1" + refute encoded[:links] + end + + test "serialize honors older remove_links config and removes all links" do + data = %{ + id: 1, + text: "Hello", + body: "Hello world", + full_description: "This_is_my_description", + author: %{id: 2, username: "jbonds", first_name: "jerry", last_name: "bonds"}, + best_comments: [ + %{ + id: 5, + text: "greatest comment ever", + user: %{id: 4, username: "jack", last_name: "bronds"} + } + ] + } + + Application.put_env(:jsonapi, :remove_links, true) + + encoded = Serializer.serialize(PostViewWithLinks, data, nil) + + relationships = encoded[:data][:relationships] + + refute relationships[:links] + refute encoded[:data][:links] + refute encoded[:links] + + Application.delete_env(:jsonapi, :remove_links) + end + end + test "serialize includes pagination links if page-based pagination is requested" do data = [%{id: 1}] view = PaginatedPostView @@ -905,9 +1017,13 @@ defmodule JSONAPI.SerializerTest do test "serialize can include arbitrary, user-defined, links" do data = %{id: 1} - assert %{ - links: links - } = Serializer.serialize(ExpensiveResourceView, data, nil) + assert %{data: resp, links: links} = Serializer.serialize(ExpensiveResourceView, data, nil) + + assert %{links: resource_links} = resp + + assert links == %{ + self: "/expensive-resource/#{data.id}" + } expected_links = %{ self: "/expensive-resource/#{data.id}", @@ -920,7 +1036,7 @@ defmodule JSONAPI.SerializerTest do } } - assert expected_links == links + assert expected_links == resource_links end test "serialize returns a null data if it receives a null data" do