Current section

Files

Jump to
resourceful lib resourceful registry.ex
Raw

lib/resourceful/registry.ex

defmodule Resourceful.Registry do
@doc """
Instances of `Resourceful.Type` are intended to be used in conjunction with a
registry in most circumstances. The application, and even the client, will
likely understand a resource type by its string name/identifier. Types
themselves, when associated with a registry, will be able to reference each
other as well, which forms the basis for relationships.
A module using the `Registry` behaviour becomes a key/value store.
`defresourcefultype/1` allows the types to be evaluated entirely at compile
time.
"""
defmodule DuplicateTypeNameError do
defexception message: "type with name already exists"
end
defmodule InvalidType do
defexception message: "result of block must be a `Resourceful.Type`"
end
defmodule NotRegisteredError do
defexception message: "type is not registered"
end
alias Resourceful.{Error, Type}
alias Resourceful.Type.{GraphedField, Relationship}
defmacro __using__(_) do
quote do
import unquote(__MODULE__), only: [type: 1, type: 2]
import Resourceful.Type,
only: [
id: 2,
max_depth: 2,
max_filters: 2,
max_sorters: 2,
meta: 3,
name: 2,
new: 1,
new: 2
]
import Resourceful.Type.Builders
import Resourceful.Type.Ecto, only: [type_with_schema: 1, type_with_schema: 2]
@before_compile {unquote(__MODULE__), :__before_compile__}
@rtypes %{}
def fetch(name), do: unquote(__MODULE__).fetch(all(), name)
def fetch!(name), do: unquote(__MODULE__).fetch!(all(), name)
def fetch_field_graph(name), do: unquote(__MODULE__).fetch(field_graphs(), name)
def fetch_field_graph!(name), do: unquote(__MODULE__).fetch!(field_graphs(), name)
def has_type?(name), do: Map.has_key?(all(), name)
end
end
@doc """
Builds a field graph for a `Resourceful.Type`. Since types have a `max_depth`,
all possible graphed fields can be computed and cached at compile time when
using a registry. This allows nested fields to be treated like local fields in
the sense that they are available in a flat map.
For example, a song type would have a `title` field. Once graphed, it would
also have an `album.title` field. If the depth was set to 2, access to a field
like `album.artist.name` would also be available.
This prevents a lot of recursion logic from being applied at every lookup and
by instecting the field graph for a type it's easy to see all of the possible
mappings.
Graphed fields are wrapped in a `Resourceful.Type.GraphedField` struct which
contains relational information about the field in addition to the field data
itself.
See `Resourceful.Type.max_depth/2` for information about what is intended to
be included in a field graph based on the depth setting.
"""
@spec build_field_graph(%{String.t() => %Type{}}, String.t()) :: Type.field_graph()
def build_field_graph(types_map, type_name) do
type = Map.get(types_map, type_name)
do_build_field_graph(%{}, types_map, type, type.max_depth, nil, nil, [])
end
def do_build_field_graph(field_graph, _, _, -1, _, _, _), do: field_graph
def do_build_field_graph(
field_graph,
types_map,
%Type{} = type,
depth,
parent,
name_prefix,
map_to_prefix
) do
Enum.reduce(type.fields, field_graph, fn {name, field}, new_field_graph ->
new_map_to = map_to_prefix ++ [field.map_to]
new_name = qualify_name(name_prefix, name)
field_data = GraphedField.new(field, new_name, new_map_to, parent)
new_field_graph = maybe_put_field_data(new_field_graph, field_data, depth)
case field do
%Relationship{embedded?: embedded?, graph?: true} ->
new_depth = if embedded?, do: depth, else: depth - 1
do_build_field_graph(
new_field_graph,
types_map,
Map.fetch!(types_map, field.related_type),
new_depth,
field_data,
new_name,
new_map_to
)
_ ->
new_field_graph
end
end)
end
defp maybe_put_field_data(
field_graph,
%GraphedField{field: %Relationship{embedded?: false}},
0
) do
field_graph
end
defp maybe_put_field_data(field_graph, graphed_field, _) do
Map.put(field_graph, graphed_field.name, graphed_field)
end
defp qualify_name(nil, name), do: name
defp qualify_name(prefix, name), do: "#{prefix}.#{name}"
@doc """
A name can be optionally passed to the `type/2` macro. If provided, this name
will override the name of the resource created in provided block.
"""
@spec maybe_put_name(%Type{}, String.t() | nil) :: %Type{}
def maybe_put_name(%Type{} = type, nil), do: type
def maybe_put_name(%Type{} = type, name) when is_binary(name) do
Type.name(type, name)
end
@spec fetch(map(), String.t()) :: {:ok, %Type{}} | Error.contextual()
def fetch(rtypes, name) do
case Map.get(rtypes, name) do
nil -> Error.with_key(:resource_type_not_registered, name)
type -> {:ok, type}
end
end
@spec fetch!(map(), String.t()) :: %Type{}
def fetch!(rtypes, name) do
case Map.get(rtypes, name) do
nil -> raise NotRegisteredError, "type with name `#{name}` is not registered"
type -> type
end
end
@spec fetch_field_graph(map(), String.t() | %Type{}) ::
{:ok, %{String.t() => %GraphedField{}}} | Error.contextual()
def fetch_field_graph(field_graphs, %Type{name: name}) do
fetch_field_graph(field_graphs, name)
end
def fetch_field_graph(field_graphs, name) do
case Map.get(field_graphs, name) do
nil -> Error.with_key(:field_graphs_not_registered, name)
graphed_field -> {:ok, graphed_field}
end
end
@spec fetch_field_graph!(map(), String.t() | %Type{}) ::
%{String.t() => %GraphedField{}}
def fetch_field_graph!(field_graphs, name) do
case Map.get(field_graphs, name) do
nil -> raise NotRegisteredError, "field graph for `#{name}` is not registered"
field_graph -> field_graph
end
end
@doc """
Ensures a value is a `Resourceful.Type` and that no type of the same name
exists in the map. Raises an exception if both conditions are not met.
"""
@spec validate_type!(any(), %{String.t() => %Type{}}) :: %Type{}
def validate_type!(%Type{} = type, types) do
if Map.has_key?(types, type.name) do
raise __MODULE__.DuplicateTypeNameError,
message: "type with name `#{type.name}` already exists"
end
type
end
def validate_type!(_, _), do: raise(__MODULE__.InvalidType)
@doc """
Assigns the resource specified in the `block` and makes it part of the
registry. If `name` is provided, it will rename the resource and use that
`name` as the key.
If `block` does not result in a `Resourceful.Type` an exception will be
raised.
"""
defmacro type(name \\ nil, do: block) do
quote do
@rtype unquote(block)
|> Resourceful.Type.finalize()
|> Resourceful.Type.register(__MODULE__)
|> unquote(__MODULE__).maybe_put_name(unquote(name))
|> unquote(__MODULE__).validate_type!(@rtypes)
@rtypes Map.put(@rtypes, @rtype.name, @rtype)
end
end
defmacro __before_compile__(_) do
quote do
def all(), do: @rtypes
@rtype_field_graphs Map.new(@rtypes, fn {name, _} ->
{name, unquote(__MODULE__).build_field_graph(@rtypes, name)}
end)
def field_graphs(), do: @rtype_field_graphs
@rtype_names Map.keys(@rtypes)
def names(), do: @rtype_names
end
end
end