defmodule Factori.Attributes do
require Logger
defmodule CyclicNonNullableReferenceError do
defexception [:source_column, :column]
@impl true
def message(%{source_column: source, column: column}) do
"""
Cyclic dependencies are not supported.
"#{source.table_name}"."#{source.name}" is not nullable and references "#{column.table_name}"."#{column.name}" who is alos not nullable.
To fix this, make one of them nullable.
"""
end
end
@spec map(
Factori.Config.t(),
fun(),
String.t(),
Keyword.t(),
Factori.Bootstrap.ColumnDefinition.t()
) :: {Keyword.t(), Keyword.t()}
def map(config, insert_func, table_name, attrs, source_column) do
columns =
table_name
|> config.storage.get(config.storage_name)
|> Enum.reject(& &1.options.ignore)
|> Enum.sort_by(&((&1.reference && 1) || 0))
{db_attrs, struct_attrs} =
Enum.split_with(attrs, fn {attr, _} ->
Enum.find(columns, &(&1.name === attr))
end)
db_attrs =
Enum.reduce(columns, db_attrs, fn column, attrs ->
new_value =
if column.reference do
fn -> fetch_reference(config, insert_func, columns, attrs, column, source_column) end
else
fn ->
value_mapping =
Enum.find_value(config.mappings, fn mapping ->
value = find_mapping_value(mapping, column, config.options)
value !== :not_found && {:ok, value}
end)
case value_mapping do
{:ok, value} ->
Enum.reduce(config.mappings, value, fn mapping, acc ->
find_transformed_value(mapping, column, acc)
end)
_ ->
Logger.warn("Can't find a mapping for #{inspect(column)}")
nil
end
end
end
value = Keyword.get_lazy(attrs, column.name, new_value)
value = Factori.Ecto.dump_value(value, column)
[{column.name, value} | attrs]
end)
{Enum.uniq(db_attrs), Enum.uniq(struct_attrs)}
end
defp fetch_reference(config, insert_func, columns, attrs, column, source_column) do
{reference_table_name, reference_column_name} = column.reference
existing_reference_value =
columns
|> Enum.filter(&(&1.reference === column.reference))
|> Enum.map(&Keyword.get(attrs, &1.name))
|> Enum.reject(&is_nil/1)
|> List.first()
cond do
not is_nil(existing_reference_value) ->
existing_reference_value
source_column && source_column.table_name === reference_table_name ->
if column.options.null do
nil
else
raise CyclicNonNullableReferenceError,
source_column: source_column,
column: column
end
true ->
reference = insert_func.(config, reference_table_name, [], column, nil)
Map.get(reference, reference_column_name)
end
end
defp find_transformed_value(_mapping, _column, nil), do: nil
defp find_transformed_value(mapping, column, value) when is_list(mapping) do
if mapping[:transform],
do: mapping[:transform].(column, value),
else: value
end
defp find_transformed_value(mapping, _column, value) when is_function(mapping) do
value
end
defp find_transformed_value(mapping, column, value) when is_atom(mapping) do
if function_exported?(mapping, :transform, 2),
do: mapping.transform(column, value),
else: value
end
defp find_mapping_value(mapping, column, options) when is_list(mapping) do
maybe_nil(column, options, fn -> mapping[:match].(column) end)
rescue
FunctionClauseError -> :not_found
end
defp find_mapping_value(mapping, column, options) when is_function(mapping) do
maybe_nil(column, options, fn -> mapping.(column) end)
rescue
FunctionClauseError -> :not_found
end
defp find_mapping_value(module, column, options) when is_atom(module) do
maybe_nil(column, options, fn -> module.match(column) end)
rescue
FunctionClauseError -> :not_found
end
defp maybe_nil(column, options, func) do
if column.options.null && nil_probability?(options), do: nil, else: func.()
end
defp nil_probability?(options), do: :rand.uniform() < options.nil_probability
end