Skip to content

Commit a1bcd21

Browse files
authored
feat: Make auto-links optional without removing other links (#407)
1 parent 58cf14b commit a1bcd21

3 files changed

Lines changed: 193 additions & 28 deletions

File tree

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,9 @@ config :jsonapi,
198198
scheme: "https",
199199
namespace: "/api",
200200
field_transformation: :underscore,
201-
remove_links: false,
202201
serialize_nil_relationships: false,
202+
remove_links: false,
203+
add_auto_links: false,
203204
json_library: Jason,
204205
paginator: nil
205206
```
@@ -218,12 +219,20 @@ config :jsonapi,
218219
`:dasherize`. If your API uses underscores (e.g. `"favorite_color": "red"`)
219220
set to `:underscore`. To transform only the top-level field keys, use
220221
`:camelize_shallow` or `:dasherize_shallow`.
221-
- **remove_links**. `links` data can optionally be removed from the payload via
222-
setting the configuration above to `true`. Defaults to `false`.
223222
- **serialize_nil_relationships**. By default, relationships on a resource that
224223
are `nil` will be omitted during serialization. Setting this to `true` will
225224
serialize these relationships, provided they are loaded on the resource.
226225
Defaults to `false`.
226+
- **remove_links**. `links` data can optionally be removed from the payload via
227+
setting the configuration above to `true`. Defaults to `false`. This setting
228+
removes all links, not just auto-added links.
229+
- **add_auto_links**. `links` data can optionally be auto-added by
230+
setting the configuration above to `true`. Defaults to `true`. This setting is
231+
overruled by `remove_links` for backwards compatibility -- setting both to
232+
`true` results in adding links and then removing them. Only setting
233+
`add_auto_links` to `true` (and leaving `remove_links` `false` as is its
234+
default) will serialize your explicitly created links but not add any
235+
automatically generated links on top.
227236
- **json_library**. Defaults to [Jason](https://hex.pm/packages/jason).
228237
- **paginator**. Module implementing pagination links generation. Defaults to `nil`.
229238

lib/jsonapi/serializer.ex

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ defmodule JSONAPI.Serializer do
4242
encoded_data
4343
end
4444

45-
merge_links(encoded_data, data, view, conn, query_page, remove_links?(), options)
45+
merge_links(encoded_data, data, view, conn, query_page, add_auto_links?(), options, true)
4646
end
4747

4848
def encode_data(_view, nil, _conn, _query_includes, _options), do: {[], nil}
@@ -67,7 +67,7 @@ defmodule JSONAPI.Serializer do
6767
relationships: %{}
6868
}
6969

70-
doc = merge_links(encoded_data, data, view, conn, nil, remove_links?(), options)
70+
doc = merge_links(encoded_data, data, view, conn, nil, add_auto_links?(), options, false)
7171

7272
doc =
7373
case view.meta(data, conn) do
@@ -210,42 +210,76 @@ defmodule JSONAPI.Serializer do
210210
data: encode_rel_data(rel_view, rel_data)
211211
}
212212

213-
merge_related_links(data, info, remove_links?())
213+
merge_related_links(data, info, add_auto_links?())
214214
end
215215

216-
defp merge_base_links(%{links: links} = doc, data, view, conn) do
217-
view_links = data |> view.links(conn) |> Map.merge(links)
218-
Map.merge(doc, %{links: view_links})
216+
defp merge_links(doc, data, view, conn, _page, false, _options, is_root_of_doc) do
217+
merge_view_links(doc, data, view, conn, is_root_of_doc)
219218
end
220219

221-
defp merge_links(doc, data, view, conn, page, false, options) when is_list(data) do
222-
links =
220+
defp merge_links(doc, data, view, conn, page, true, options, is_root_of_doc) do
221+
doc
222+
|> merge_auto_links(data, view, conn, page, options)
223+
|> merge_view_links(data, view, conn, is_root_of_doc)
224+
end
225+
226+
defp merge_view_links(doc, data, view, conn, is_root_of_doc)
227+
228+
defp merge_view_links(%{links: links} = doc, data, view, conn, false) do
229+
# intentionally take the view.links and merge them into the existing links,
230+
# allowing the custom defined links for the view to override any auto-links
231+
# generated by this library UNLESS configured to remove links (backwards
232+
# compatibility):
233+
view_links =
234+
if remove_links?() do
235+
%{}
236+
else
237+
Map.merge(links, view.links(data, conn))
238+
end
239+
240+
new_doc = Map.merge(doc, %{links: view_links})
241+
242+
case new_doc do
243+
%{links: links} when links == %{} ->
244+
Map.delete(doc, :links)
245+
246+
doc ->
247+
doc
248+
end
249+
end
250+
251+
defp merge_view_links(doc, data, view, conn, false),
252+
do: merge_view_links(Map.merge(doc, %{links: %{}}), data, view, conn, false)
253+
254+
defp merge_view_links(doc, _data, _view, _conn, true), do: doc
255+
256+
defp merge_auto_links(doc, data, view, conn, page, options)
257+
when is_list(data) do
258+
auto_links =
223259
data
224260
|> view.pagination_links(conn, page, options)
225-
|> Map.merge(%{self: view.url_for_pagination(data, conn, page)})
261+
|> Map.merge(%{
262+
self: view.url_for_pagination(data, conn, page)
263+
})
226264

227-
doc
228-
|> Map.merge(%{links: links})
229-
|> merge_base_links(data, view, conn)
265+
Map.merge(doc, %{links: auto_links})
230266
end
231267

232-
defp merge_links(doc, data, view, conn, _page, false, _options) do
233-
doc
234-
|> Map.merge(%{links: %{self: view.url_for(data, conn)}})
235-
|> merge_base_links(data, view, conn)
236-
end
268+
defp merge_auto_links(doc, data, view, conn, _page, _options) do
269+
auto_links = %{self: view.url_for(data, conn)}
237270

238-
defp merge_links(doc, _data, _view, _conn, _page, _remove_links, _options), do: doc
271+
Map.merge(doc, %{links: auto_links})
272+
end
239273

240274
defp merge_related_links(
241275
encoded_data,
242276
{rel_view, rel_data, rel_url, conn},
243-
false = _remove_links
277+
true = _add_auto_links
244278
) do
245279
Map.merge(encoded_data, %{links: %{self: rel_url, related: rel_view.url_for(rel_data, conn)}})
246280
end
247281

248-
defp merge_related_links(encoded_rel_data, _info, _remove_links), do: encoded_rel_data
282+
defp merge_related_links(encoded_rel_data, _info, _add_auto_links), do: encoded_rel_data
249283

250284
@spec encode_rel_data(module(), map() | list()) :: map() | nil
251285
def encode_rel_data(_view, nil), do: nil
@@ -302,7 +336,13 @@ defmodule JSONAPI.Serializer do
302336
|> List.flatten()
303337
end
304338

305-
defp remove_links?, do: Application.get_env(:jsonapi, :remove_links, false)
339+
defp remove_links? do
340+
Application.get_env(:jsonapi, :remove_links, false)
341+
end
342+
343+
defp add_auto_links? do
344+
Application.get_env(:jsonapi, :add_auto_links, !remove_links?())
345+
end
306346

307347
@spec serialize_nil_relationships? :: boolean()
308348
defp serialize_nil_relationships?, do: Application.get_env(:jsonapi, :serialize_nil_relationships, false)

test/jsonapi/serializer_test.exs

Lines changed: 120 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,27 @@ defmodule JSONAPI.SerializerTest do
1919
end
2020
end
2121

22+
defmodule PostViewWithLinks do
23+
use JSONAPI.View
24+
25+
def fields, do: [:text, :body, :full_description, :inserted_at]
26+
def meta(data, _conn), do: %{meta_text: "meta_#{data[:text]}"}
27+
def type, do: "mytype"
28+
29+
def relationships do
30+
[
31+
author: {JSONAPI.SerializerTest.UserView, :include},
32+
best_comments: {JSONAPI.SerializerTest.CommentView, :include}
33+
]
34+
end
35+
36+
def links(_data, _conn) do
37+
%{
38+
self: "https://website.com/api/posts/1"
39+
}
40+
end
41+
end
42+
2243
defmodule PageBasedPaginator do
2344
@moduledoc """
2445
Page based pagination strategy
@@ -867,6 +888,97 @@ defmodule JSONAPI.SerializerTest do
867888
Application.delete_env(:jsonapi, :remove_links)
868889
end
869890

891+
describe "when configured to not add auto links" do
892+
setup do
893+
Application.put_env(:jsonapi, :add_auto_links, false)
894+
895+
on_exit(fn ->
896+
Application.delete_env(:jsonapi, :add_auto_links)
897+
end)
898+
899+
{:ok, []}
900+
end
901+
902+
test "serialize does not include auto links" do
903+
data = %{
904+
id: 1,
905+
text: "Hello",
906+
body: "Hello world",
907+
full_description: "This_is_my_description",
908+
author: %{id: 2, username: "jbonds", first_name: "jerry", last_name: "bonds"},
909+
best_comments: [
910+
%{
911+
id: 5,
912+
text: "greatest comment ever",
913+
user: %{id: 4, username: "jack", last_name: "bronds"}
914+
}
915+
]
916+
}
917+
918+
encoded = Serializer.serialize(PostView, data, nil)
919+
920+
relationships = encoded[:data][:relationships]
921+
922+
refute relationships[:links]
923+
refute encoded[:data][:links]
924+
refute encoded[:links]
925+
end
926+
927+
test "serialize still adds custom links" do
928+
data = %{
929+
id: 1,
930+
text: "Hello",
931+
body: "Hello world",
932+
full_description: "This_is_my_description",
933+
author: %{id: 2, username: "jbonds", first_name: "jerry", last_name: "bonds"},
934+
best_comments: [
935+
%{
936+
id: 5,
937+
text: "greatest comment ever",
938+
user: %{id: 4, username: "jack", last_name: "bronds"}
939+
}
940+
]
941+
}
942+
943+
encoded = Serializer.serialize(PostViewWithLinks, data, nil)
944+
945+
relationships = encoded[:data][:relationships]
946+
947+
refute relationships[:links]
948+
assert encoded[:data][:links][:self] == "https://website.com/api/posts/1"
949+
refute encoded[:links]
950+
end
951+
952+
test "serialize honors older remove_links config and removes all links" do
953+
data = %{
954+
id: 1,
955+
text: "Hello",
956+
body: "Hello world",
957+
full_description: "This_is_my_description",
958+
author: %{id: 2, username: "jbonds", first_name: "jerry", last_name: "bonds"},
959+
best_comments: [
960+
%{
961+
id: 5,
962+
text: "greatest comment ever",
963+
user: %{id: 4, username: "jack", last_name: "bronds"}
964+
}
965+
]
966+
}
967+
968+
Application.put_env(:jsonapi, :remove_links, true)
969+
970+
encoded = Serializer.serialize(PostViewWithLinks, data, nil)
971+
972+
relationships = encoded[:data][:relationships]
973+
974+
refute relationships[:links]
975+
refute encoded[:data][:links]
976+
refute encoded[:links]
977+
978+
Application.delete_env(:jsonapi, :remove_links)
979+
end
980+
end
981+
870982
test "serialize includes pagination links if page-based pagination is requested" do
871983
data = [%{id: 1}]
872984
view = PaginatedPostView
@@ -905,9 +1017,13 @@ defmodule JSONAPI.SerializerTest do
9051017
test "serialize can include arbitrary, user-defined, links" do
9061018
data = %{id: 1}
9071019

908-
assert %{
909-
links: links
910-
} = Serializer.serialize(ExpensiveResourceView, data, nil)
1020+
assert %{data: resp, links: links} = Serializer.serialize(ExpensiveResourceView, data, nil)
1021+
1022+
assert %{links: resource_links} = resp
1023+
1024+
assert links == %{
1025+
self: "/expensive-resource/#{data.id}"
1026+
}
9111027

9121028
expected_links = %{
9131029
self: "/expensive-resource/#{data.id}",
@@ -920,7 +1036,7 @@ defmodule JSONAPI.SerializerTest do
9201036
}
9211037
}
9221038

923-
assert expected_links == links
1039+
assert expected_links == resource_links
9241040
end
9251041

9261042
test "serialize returns a null data if it receives a null data" do

0 commit comments

Comments
 (0)