Current section

Files

Jump to
maru_entity lib maru entity.ex
Raw

lib/maru/entity.ex

alias Maru.Entity.Utils
alias Maru.Entity.Struct.{Batch, Serializer, Exposure}
alias Maru.Entity.Struct.Exposure.{Runtime, Information}
defmodule Maru.Entity do
@moduledoc ~S"""
## Defining Entities
### Basic Exposure
expose :id
expose with `Map.get(instance, :id)`
`%{id: 1}` => `%{id: 1}`
### Exposing with a Presenter
expose field with another entity and another key:
expose :response, source: :reply, using: Reply.Entity
`%{reply: reply}` => `%{response: Reply.Entity.serializer(reply)}`
expose list-type field with another entity:
expose :replies, using: List[Reply.Entity]
`%{replies: [reply1, reply2]}` => `%{replies: [Reply.Entity.serializer(reply1), Reply.Entity.serializer(reply2)]}`
### Conditional Exposure
Use `:if` or `:unless` to expose fields conditionally.
expose :username, if: fn(user, _options) -> user[:public?] end
`%{username: "user1", public?: true}` => `%{username: "user1"}`
`%{username: "user1", public?: false}` => `%{}`
### Custom Present Function
expose :username, [], fn user, _options ->
"#{user[:first_name]} #{user[:last_name]}"
end
`%{first_name: "X", last_name: "Y"}` => `%{username: "X Y"}`
"""
@type instance :: map
@type object :: map
@type options :: map
@type group :: list(atom)
@type one_or_many :: :one | :many
@doc false
defmacro __using__(_) do
quote do
Module.register_attribute __MODULE__, :exposures, persist: true
@group []
@exposures []
import unquote(__MODULE__)
@before_compile unquote(__MODULE__)
end
end
@doc """
Extend another entity.
Example:
defmodule UserData do
use Maru.Entity
expose :name
expose :address do
expose :address1
expose :address2
expose :address_state
expose :address_city
end
expose :email
expose :phone
end
defmodule MailingAddress do
use Maru.Entity
extend UserData, only: [
address: [:address1, :address2]
]
end
defmodule BasicInfomation do
use Maru.Entity
extend UserData, except: [:address]
end
"""
defmacro extend(module, options \\ []) do
func =
case {options[:only], options[:except]} do
{nil, nil} -> quote do fn _ -> true end end
{nil, except} -> quote do fn i -> not Utils.attr_match?(i.information.attr_group, unquote(except)) end end
{only, nil} -> quote do fn i -> Utils.attr_match?(i.information.attr_group, unquote(only)) end end
{_, _} -> raise ":only and :except conflict"
end
quote do
unquote(module).__info__(:attributes)
|> Keyword.get(:exposures)
|> Enum.filter(unquote(func))
|> Enum.each(fn exposure ->
@exposures @exposures ++ [exposure]
end)
end
end
@doc """
Expose a field with Map.get.
"""
defmacro expose(attr_name) when is_atom(attr_name) do
quote do
@exposures @exposures ++ [parse(
[
attr_name: unquote(attr_name),
group: @group ++ [unquote(attr_name)],
],
unquote(Macro.escape(__CALLER__))
)]
end
end
@doc """
Nested Exposure.
"""
defmacro expose(group, [do: block]) when is_atom(group) do
quote do
group = @group
@group @group ++ [unquote(group)]
@exposures @exposures ++ [%Exposure{
runtime: default_runtime(:group, @group),
information: %Information{attr_group: @group},
}]
unquote(block)
@group group
end
end
@doc """
Expose a field with Map.get and options.
"""
defmacro expose(attr_name, options) when is_atom(attr_name) and is_list(options) do
options = Macro.escape(options)
quote do
@exposures @exposures ++ [
unquote(options)
|> Keyword.put(:attr_name, unquote(attr_name))
|> Keyword.put(:group, @group ++ [unquote(attr_name)])
|> parse(unquote(Macro.escape(__CALLER__)))
]
end
end
@doc """
Expose a field with custom function and options.
"""
defmacro expose(attr_name, options, do_func) when is_atom(attr_name) and is_list(options) do
options = Macro.escape(options)
quote do
@exposures @exposures ++ [
unquote(options)
|> Keyword.put(:attr_name, unquote(attr_name))
|> Keyword.put(:group, @group ++ [unquote(attr_name)])
|> Keyword.put(:do_func, unquote(Macro.escape(do_func)))
|> parse(unquote(Macro.escape(__CALLER__)))
]
end
end
@spec parse(Keyword.t, Module.t) :: Maru.Entity.Struct.Exposure.t
def parse(options, caller) do
pipeline = [
:attr_name, :serializer, :if_func, :do_func, :build_struct,
]
accumulator = %{
options: options,
runtime: quote do %Runtime{} end,
information: %Information{},
}
Enum.reduce(pipeline, accumulator, &(do_parse(&1, &2, caller)))
end
defp do_parse(:attr_name, %{options: options, runtime: runtime, information: information}, _caller) do
group = options |> Keyword.fetch!(:group)
attr_name = options |> Keyword.fetch!(:attr_name)
param_key = options |> Keyword.get(:source, attr_name)
options = options |> Keyword.drop([:attr_name, :group, :source]) |> Keyword.put(:param_key, param_key)
%{ options: options,
runtime: quote do
%{ unquote(runtime) |
attr_group: unquote(group),
}
end,
information: %{information | attr_group: group},
}
end
defp do_parse(:if_func, %{options: options, runtime: runtime, information: information}, _caller) do
if_func = options |> Keyword.get(:if)
unless_func = options |> Keyword.get(:unless)
options = options |> Keyword.drop([:if, :unless])
is_nil(if_func) or is_nil(unless_func) || raise ":if and :unless conflict"
func =
case {if_func, unless_func} do
{nil, nil} -> quote do
fn(_, _) -> true end
end
{nil, {:fn, _, _}=f} ->
quote do
fn(instance, options) ->
case unquote(f).(instance, options) do
x when x in [false, nil] -> true
_ -> false
end
end
end
{{:fn, _, _}=f, nil} ->
quote do
fn(instance, options) ->
case unquote(f).(instance, options) do
x when x in [false, nil] -> false
_ -> true
end
end
end
end
%{ options: options,
runtime: quote do
%{ unquote(runtime) |
if_func: unquote(func)
}
end,
information: information,
}
end
defp do_parse(:serializer, %{options: options, runtime: runtime, information: information}, caller) do
serializer =
case Keyword.get(options, :using, nil) do
nil -> nil
{:__aliases__, _, _} = module ->
%Serializer{module: Macro.expand(module, caller), type: :one}
{{:., _, [Access, :get]}, _, [{:__aliases__, _, [:List]}, {:__aliases__, _, _} = module,]} ->
%Serializer{module: Macro.expand(module, caller), type: :many}
end
%{ options: options |> Keyword.drop([:using]),
runtime: quote do
%{ unquote(runtime) |
serializer: unquote(Macro.escape(serializer))
}
end,
information: information,
}
end
defp do_parse(:do_func, %{options: options, runtime: runtime, information: information}, _caller) do
do_func = options |> Keyword.get(:do_func)
param_key = options |> Keyword.fetch!(:param_key)
batch = Keyword.get(options, :batch)
options = options |> Keyword.drop([:do_func, :param_key, :batch])
func =
cond do
not is_nil(batch) ->
quote do
fn(instance, options) ->
%Batch{
module: unquote(batch),
key: unquote(batch).key(instance, options),
}
end
end
is_nil(do_func) ->
quote do
fn(instance, _options) ->
Map.get(instance, unquote(param_key))
end
end
true ->
do_func
end
%{ options: options,
runtime: quote do
%{ unquote(runtime) |
do_func: unquote(func)
}
end,
information: information,
}
end
defp do_parse(:build_struct, %{runtime: runtime, information: information}, _caller) do
%Exposure{runtime: runtime, information: information}
end
@doc """
Generate default runtime struct.
"""
@spec default_runtime(atom(), group()) :: Macro.t
def default_runtime(:group, group) do
quote do
%Runtime{
attr_group: unquote(group),
if_func: fn (_, _) -> true end,
do_func: fn (_, _) -> %{} end,
}
end
end
defmacro __before_compile__(env) do
exposures =
Module.get_attribute(env.module, :exposures)
|> Enum.map(fn(e) ->
e.runtime
end)
quote do
@doc """
Return list of exposures.
"""
@spec __exposures__ :: list(Runtime.t)
def __exposures__ do
unquote(exposures)
end
@doc """
Serialize given instance into an object.
"""
@spec serialize(Entity.instance, Entity.options, Keyword.t) :: Maru.Entity.object
def serialize(instance, options \\ %{}, entity_options \\ []) do
%Serializer{
module: __MODULE__,
type: is_list(instance) && :many || :one,
options: options,
} |> Maru.Entity.Runtime.serialize(instance, entity_options)
end
end
end
end