Current section

Files

Jump to
ja_serializer lib ja_serializer relationship.ex
Raw

lib/ja_serializer/relationship.ex

defmodule JaSerializer.AssociationNotLoadedError do
defexception [:message]
def exception(opts) do
msg = """
The #{opts[:rel]} relationship returned %Ecto.Association.NotLoaded{}.
Please pre-fetch the relationship before serialization or override the
#{opts[:name]}/2 function in your serializer.
Example:
def #{opts[:name]}(struct, conn) do
case struct.#{opts[:rel]} do
%Ecto.Association.NotLoaded{} ->
struct
|> Ecto.assoc(:#{opts[:rel]})
|> Repo.all
other -> other
end
end
"""
%JaSerializer.AssociationNotLoadedError{message: msg}
end
end
defmodule JaSerializer.Relationship do
@moduledoc false
defmodule HasMany do
@moduledoc """
Struct to represent a HasMany relationship.
The fields are:
* `serializer` - A Serializer (often a PhoenixView) implementing the JaSerializer.Serializer behaviour.
* `include` - Should this relationship be included (sideloaded) by default. Overriden by `include` opt to JaSerializer.format/4
* `data` - A list of structs representing the data.
* `identifiers` - Should "resource identifiers be included, options are `:when_included` and `:always`. Defaults to `:when_included`
* `links` - A keyword list of links, `self` and `related` are most common.
* `name` - Name of the relationship, automatically set.
Used when defining relationships without the DSL using the
JaSerializer.relationships/2 callback. For example:
def relationships(article, _conn) do
%{
comments: %HasMany{
serializer: MyApp.CommentView,
include: true,
data: article.comments,
}
}
end
See JaSerializer.DSL.has_many/2 for information on defining different types
of relationships.
"""
defstruct links: [],
type: nil,
serializer: nil,
include: false,
data: nil,
identifiers: :when_included,
name: nil
@doc false
def from_dsl(name, dsl_opts) do
%__MODULE__{
links: dsl_opts[:links] || [],
type: dsl_opts[:type],
serializer: dsl_opts[:serializer],
include: dsl_opts[:include],
data: dsl_opts[:data] || name,
identifiers: dsl_opts[:identifiers] || :when_included,
name: name
}
end
end
defmodule HasOne do
@moduledoc """
Struct representing a HasOne (or belongs to) relationship.
The fields are:
* `serializer` - A Serializer (often a PhoenixView) implementing the JaSerializer.Serializer behaviour.
* `include` - Should this relationship be included (sideloaded) by default. Overriden by `include` opt to JaSerializer.format/4
* `data` - A struct representing the data for serialization.
* `identifiers` - Should "resource identifiers be included, options are `:when_included` and `:always`. Defaults to `:always`
* `links` - A keyword list of links, `self` and `related` are most common.
* `name` - Name of the relationship, automatically set.
Used when defining relationships without the DSL using the
JaSerializer.relationships/2 callback. For example:
def relationships(article, _conn) do
%{
comments: %HasOne{
serializer: MyApp.CommentView,
include: true,
data: article.comments,
}
}
end
See JaSerializer.DSL.has_many/2 for information on defining different types
of relationships.
"""
defstruct links: [],
type: nil,
serializer: nil,
include: false,
data: nil,
identifiers: :always,
name: nil
@doc false
def from_dsl(name, dsl_opts) do
%__MODULE__{
links: dsl_opts[:links] || [],
type: dsl_opts[:type],
serializer: dsl_opts[:serializer],
include: dsl_opts[:include],
data: dsl_opts[:data] || name,
identifiers: dsl_opts[:identifiers] || :always,
name: name
}
end
end
@doc false
def default_function(name, opts) do
quote bind_quoted: [name: name, opts: opts] do
def unquote(name)(struct) do
JaSerializer.Relationship.get_data(struct, unquote(name), unquote(opts))
end
def unquote(name)(struct, _conn) do
unquote(name)(struct)
end
defoverridable [{name, 1}, {name, 2}]
end
end
@error JaSerializer.AssociationNotLoadedError
def get_data(struct, name, opts) do
rel = opts[:field] || name
struct
|> Map.get(rel)
|> case do
%{__struct__: Ecto.Association.NotLoaded} ->
handle_association_not_loaded(struct, rel, name, opts)
other ->
other
end
end
# If the association is not loaded we only want to raise an exception
# if the association was intended to be loaded. Otherwise we try and
# work out the resource identifiers and return those if possible.
defp handle_association_not_loaded(struct, rel, name, opts) do
foreign_key = opts[:foreign_key] || String.to_atom("#{name}_id")
if opts[:include] do
raise @error, rel: rel, name: name
else
case Map.fetch(struct, foreign_key) do
{:ok, nil} ->
nil
{:ok, id} ->
%{id: id}
:error ->
raise @error, rel: rel, name: name
end
end
end
end