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