Skip to content

Commit

Permalink
Support renaming of relationships (#270)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattpolzin committed Jan 14, 2023
1 parent 1fb6380 commit c9ed4bb
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 20 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,25 @@ or `"index.json"` normally.
If you'd like to use this without Phoenix simply use the `JSONAPI.View` and call
`JSONAPI.Serializer.serialize(MyApp.PostView, data, conn, meta)`.

## Renaming relationships
If a relationship has a different name in the backend than you would like it to in your API,
you can rewrite its name in the `JSONAPI.View`. You pair the view with the name of the relationship
used in the data (e.g. Ecto schema) to achieve this. Note that you can use a triple instead
of a pair to add the instruction to always include the relation if desired.

```elixir
defmodule MyApp.PostView do
use JSONAPI.View, type: "posts"

def relationships do
# The `author` will be exposed as `creator` and the `comments` will be
# exposed as `critiques` (for some reason).
[creator: {:author, MyApp.UserView, :include},
critiques: {:comments, MyApp.CommentView}]
end
end
```

## Parsing and validating a JSONAPI Request

In your controller you may add
Expand Down
90 changes: 70 additions & 20 deletions lib/jsonapi/serializer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -79,29 +79,26 @@ defmodule JSONAPI.Serializer do
@spec encode_relationships(Conn.t(), document(), tuple(), list()) :: tuple()
def encode_relationships(conn, doc, {view, data, _, _} = view_info, options) do
view.relationships()
|> Enum.filter(&assoc_loaded?(Map.get(data, elem(&1, 0))))
|> Enum.filter(&data_loaded?(Map.get(data, get_data_key(&1))))
|> Enum.map_reduce(doc, &build_relationships(conn, view_info, &1, &2, options))
end

@spec build_relationships(Conn.t(), tuple(), tuple(), tuple(), list()) :: tuple()
defp get_data_key(rel_config), do: elem(extrapolate_relationship_config(rel_config), 1)

@spec build_relationships(Conn.t(), tuple(), term(), term(), module(), tuple(), list()) ::
tuple()
def build_relationships(
conn,
{view, data, query_includes, valid_includes},
{key, include_view},
{parent_view, parent_data, query_includes, valid_includes},
relationship_name,
rel_data,
rel_view,
acc,
options
) do
rel_view =
case include_view do
{view, :include} -> view
view -> view
end

rel_data = Map.get(data, key)

# Build the relationship url
rel_key = transform_fields(key)
rel_url = view.url_for_rel(data, rel_key, conn)
rel_key = transform_fields(relationship_name)
rel_url = parent_view.url_for_rel(parent_data, rel_key, conn)

# Build the relationship
acc =
Expand All @@ -111,14 +108,14 @@ defmodule JSONAPI.Serializer do
encode_relation({rel_view, rel_data, rel_url, conn})
)

valid_include_view = include_view(valid_includes, key)
valid_include_view = include_view(valid_includes, relationship_name)

if {rel_view, :include} == valid_include_view && data_loaded?(rel_data) do
rel_query_includes =
if is_list(query_includes) do
query_includes
|> Enum.reduce([], fn
{^key, value}, acc -> acc ++ [value]
{^relationship_name, value}, acc -> acc ++ [value]
_, acc -> acc
end)
|> List.flatten()
Expand All @@ -135,6 +132,54 @@ defmodule JSONAPI.Serializer do
end
end

@spec build_relationships(Conn.t(), tuple(), tuple(), tuple(), list()) :: tuple()
def build_relationships(
conn,
{_parent_view, data, _query_includes, _valid_includes} = parent_info,
rel_config,
acc,
options
) do
{rewrite_key, data_key, rel_view, _include} = extrapolate_relationship_config(rel_config)

rel_data = Map.get(data, data_key)

build_relationships(
conn,
parent_info,
rewrite_key,
rel_data,
rel_view,
acc,
options
)
end

@doc """
Given the relationship config entry provided by a JSONAPI.View, produce
the extrapolated config tuple containing:
- The name of the relationship to be used when serializing
- The key in the data the relationship is found under
- The relationship resource's JSONAPI.View module
- A boolean for whether the relationship is included by default or not
"""
@spec extrapolate_relationship_config(tuple()) :: {atom(), atom(), module(), boolean()}
def extrapolate_relationship_config({rewrite_key, {data_key, view, :include}}) do
{rewrite_key, data_key, view, true}
end

def extrapolate_relationship_config({data_key, {view, :include}}) do
{data_key, data_key, view, true}
end

def extrapolate_relationship_config({rewrite_key, {data_key, view}}) do
{rewrite_key, data_key, view, false}
end

def extrapolate_relationship_config({data_key, view}) do
{data_key, data_key, view, false}
end

defp include_view(valid_includes, key) when is_list(valid_includes) do
valid_includes
|> Keyword.get(key)
Expand All @@ -143,7 +188,9 @@ defmodule JSONAPI.Serializer do

defp include_view(view, _key), do: generate_view_tuple(view)

defp generate_view_tuple({_rewrite_key, view, :include}), do: {view, :include}
defp generate_view_tuple({view, :include}), do: {view, :include}
defp generate_view_tuple({_rewrite_key, view}), do: {view, :include}
defp generate_view_tuple(view) when is_atom(view), do: {view, :include}

@spec data_loaded?(map() | list()) :: boolean()
Expand Down Expand Up @@ -229,10 +276,13 @@ defmodule JSONAPI.Serializer do
defp get_default_includes(view) do
rels = view.relationships()

Enum.filter(rels, fn
{_k, {_v, :include}} -> true
_ -> false
end)
Enum.filter(rels, &include_rel_by_default/1)
end

defp include_rel_by_default(rel_config) do
{_rel_key, _data_key, _view, include_by_default} = extrapolate_relationship_config(rel_config)

include_by_default
end

defp get_query_includes(view, query_includes) do
Expand Down
92 changes: 92 additions & 0 deletions test/jsonapi/serializer_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,23 @@ defmodule JSONAPI.SerializerTest do
end
end

defmodule CommentaryView do
use JSONAPI.View, type: "comment"

def fields, do: [:text]

def relationships do
# renames "user1" data property to "commenter" JSON:API relationship (and specifies default inclusion)
# renames "user2" to "witness"
# leaves "user3" name alone
[
commenter: {:user1, JSONAPI.SerializerTest.UserView, :include},
witness: {:user2, JSONAPI.SerializerTest.UserView},
user3: JSONAPI.SerializerTest.UserView
]
end
end

defmodule NotIncludedView do
use JSONAPI.View

Expand Down Expand Up @@ -325,6 +342,49 @@ defmodule JSONAPI.SerializerTest do
assert encoded[:links][:self] == "http://www.example.com/mytype"
end

test "serialize will rename relationships" do
data = %{
id: 1,
text: "hello world",
user1: %{
id: 2,
username: "hi",
first_name: "hello",
last_name: "world"
},
user2: %{
id: 3,
username: "hi",
first_name: "hello",
last_name: "world"
},
user3: %{
id: 4,
username: "hi",
first_name: "hello",
last_name: "world"
}
}

conn =
%Plug.Conn{
assigns: %{
jsonapi_query: %Config{}
}
}
|> Plug.Conn.fetch_query_params()

encoded = Serializer.serialize(CommentaryView, data, conn)

assert encoded.data.relationships.commenter != nil
assert encoded.data.relationships.witness != nil
assert encoded.data.relationships.user3 != nil
refute Map.has_key?(encoded.data.relationships, :user1)
refute Map.has_key?(encoded.data.relationships, :user2)

assert Enum.count(encoded.included) == 1
end

test "serialize handles including from the query" do
data = %{
id: 1,
Expand Down Expand Up @@ -679,4 +739,36 @@ defmodule JSONAPI.SerializerTest do
assert List.first(encoded[:data])[:links][:self] ==
"http://www.example.com/mytype/1"
end

test "extrapolates relationship config simplest case" do
config =
IndustryView.relationships()
|> List.first()
|> Serializer.extrapolate_relationship_config()

assert config == {:tags, :tags, JSONAPI.SerializerTest.TagView, false}
end

test "extrapolates relationship config with default include" do
configs =
PostView.relationships()
|> Enum.map(&Serializer.extrapolate_relationship_config/1)

assert configs == [
{:author, :author, JSONAPI.SerializerTest.UserView, true},
{:best_comments, :best_comments, JSONAPI.SerializerTest.CommentView, true}
]
end

test "extrapolates relationship config with rewritten name" do
configs =
CommentaryView.relationships()
|> Enum.map(&Serializer.extrapolate_relationship_config/1)

assert configs == [
{:commenter, :user1, JSONAPI.SerializerTest.UserView, true},
{:witness, :user2, JSONAPI.SerializerTest.UserView, false},
{:user3, :user3, JSONAPI.SerializerTest.UserView, false}
]
end
end

0 comments on commit c9ed4bb

Please sign in to comment.