Packages

Provides basic functionalities to implement a JSON API-compliant API.

Current section

Files

Jump to
solage lib jsonapi_kit serializer.ex
Raw

lib/jsonapi_kit/serializer.ex

defmodule Solage.Serializer do
@moduledoc """
Everything needed to serialize data and includes.
This makes few assumptions:
## The render of the data structure is entirely in the client code.
We don’t implement the typical keys for a JSON API response. Rendering of the `type`, `id`, `attributes`
or `relationships` is the responsability of the client. This provides a powerful way
to implement features such as sparse fields.
By passing an optional `Plug.Conn` to the `render` function, we can implement
custom relationships links with no weird DSL forced by the module.
## Only 2 functions required
By providing a module with a `render/3` function and the necessary
`relation_view/1` functions, we can have the results of what will be in a standard
JSON API response for the `data` and `included` keys.
## Examples
Views:
```
defmodule PostView do
def render(data, _config, _conn) do
%{
id: data["id"],
relationships: %{
user: data["author_id"]
}
}
end
def relation_view(:author), do: UserView
end
defmodule UserView do
def render(data, _config, _conn) do
%{
id: data["id"],
name: data["fullname"]
}
end
end
```
Endpoint:
```
def show(conn, _) do
data = Solage.Serializer.render(PostView, conn.assigns[:post], conn.assigns[:jsonapi_query], conn)
included = Solage.Serializer.included(PostView, conn.assigns[:post], conn.assigns[:jsonapi_query], conn)
json(conn, 200, %{data: data, included: included})
end
```
Response:
```
{
"included": [
{
"id": "my-author-id",
"name": "Testy"
}
],
"data": [
{
"id": "my-post-id",
"relationships": {
"author": "my-author-id"
}
]
}
```
"""
alias Solage.QueryConfig
alias Plug.Conn
@typep serializable :: list | map
@typep config :: QueryConfig.t
@typep optional_conn :: Conn.t | nil
@doc """
Proxy for the render call on the view module.
"""
@spec render(atom, serializable, config, optional_conn) :: list
def render(view, data, config, conn \\ nil)
def render(view, data, config, conn) when is_map(data), do: [view.render(data, config, conn)]
def render(view, data, config, conn) when is_list(data) do
data
|> Enum.map(&(render(view, &1, config, conn)))
|> List.flatten
end
@doc """
Recursively get include data from the Solage.QueryConfig `include` option.
If you’re using Ecto as the data, you will need the preload the assocations present
in the `include` option.
"""
@spec included(atom, serializable, config, optional_conn) :: list
def included(view, data, config, conn \\ nil) do
config.include
|> Enum.reduce(MapSet.new, fn(include, acc) ->
view
|> handle_include(data, include, config, conn)
|> Enum.reduce(acc, &(MapSet.put(&2, &1)))
end)
|> MapSet.to_list
end
defp handle_include(view, data, include, config, conn) when is_list(data) do
data
|> Enum.map(&(handle_include(view, &1, include, config, conn)))
|> List.flatten
end
defp handle_include(view, data, {attribute, rest}, config, conn) do
{new_data, new_view} = get_data_view(view, data, attribute)
handle_include(new_view, new_data, rest, config, conn)
end
defp handle_include(view, data, attribute, config, conn) do
{new_data, new_view} = get_data_view(view, data, attribute)
render(new_view, new_data, config, conn)
end
defp get_data_view(view, data, attribute) do
{Map.get(data, attribute), view.relation_view(attribute)}
end
end