defmodule Mecto.SchemaExtractor do
@moduledoc """
Converts an Ecto schema into a map, for use in `Mecto.SchemaValidator`.
"""
@spec convert_from(module()) :: map() | {:error, atom()}
def convert_from(module) do
case Code.ensure_compiled(module) do
{:module, module} ->
if function_exported?(module, :__schema__, 1) do
convert_schema_to_map(module, [module])
else
{:error, :missing_schema}
end
error ->
error
end
end
defp convert_schema_to_map(module, visited) do
fields =
:fields
|> module.__schema__()
|> Enum.map(fn field -> {field, module.__schema__(:type, field)} end)
|> Enum.into(%{})
associations =
:associations
|> module.__schema__()
|> Enum.map(&list_associations(module, &1))
|> Enum.group_by(fn {type, _} -> type end)
associations
|> Map.get(:direct, [])
|> Enum.map(&map_association(module, visited, &1))
|> Enum.reject(&is_nil/1)
|> Enum.into(fields)
|> link_indirect_associations(module, Map.get(associations, :indirect))
end
defp list_associations(module, association) do
relationship = module.__schema__(:association, association)
case Map.get(relationship || %{}, :related) do
nil -> {:indirect, association}
_otherwise -> {:direct, association}
end
end
defp map_association(module, visited, {:direct, association}) do
relationship = module.__schema__(:association, association)
associated_module = relationship.related
if Enum.member?(visited, associated_module) do
nil
else
{
association,
{
relationship.cardinality,
convert_schema_to_map(associated_module, [associated_module | visited])
}
}
end
end
defp link_indirect_associations(schema, _module, nil), do: schema
defp link_indirect_associations(schema, module, associations) do
Enum.reduce(associations, schema, fn {:indirect, association}, schema ->
relationship = module.__schema__(:association, association)
path =
relationship.through
|> Enum.flat_map(&[&1, Access.elem(1)])
|> Enum.drop(-1)
case get_in(schema, path) do
nil -> schema
association_to_insert -> Map.put(schema, association, association_to_insert)
end
end)
end
end