lib/factori/ecto.ex

defmodule Factori.Ecto do
  def to_ecto_type("uuid"), do: Ecto.UUID
  def to_ecto_type(type), do: type

  def dump_value(nil, _), do: nil

  def dump_value(value, column) when is_struct(value) and not is_nil(column.struct_embed) do
    {_, _, key_columns} = column.struct_embed

    value
    |> Map.from_struct()
    |> Enum.map(fn {struct_key, struct_value} ->
      key_column = Enum.find(key_columns, &(&1.name === struct_key))

      key_column =
        if key_column && key_column.ecto_type === Ecto.UUID,
          do: %{key_column | ecto_type: nil},
          else: key_column

      {struct_key, dump_value(struct_value, key_column)}
    end)
    |> Map.new()
  end

  def dump_value(value, column) when is_struct(value) and column.type in ~w(json jsonb) do
    Map.from_struct(value)
  end

  def dump_value(value, column) when is_list(value),
    do: Enum.map(value, &dump_value(&1, column))

  def dump_value(value, %{ecto_schema: nil, enum: enum}) when is_struct(enum),
    do: to_string(value)

  def dump_value(value, %{enum: %{name: enum_name}, type: column_type})
      when enum_name === column_type,
      do: to_string(value)

  def dump_value(value, column) when column.type === "varchar", do: to_string(value)
  def dump_value(value, column) when column.type === "_varchar", do: to_string(value)

  def dump_value(value, %{ecto_type: ecto_type_module})
      when is_atom(ecto_type_module) do
    with {:module, ecto_type_module} <- Code.ensure_loaded(ecto_type_module),
         true <- function_exported?(ecto_type_module, :dump, 1),
         {:ok, value} <- Ecto.Type.dump(ecto_type_module, value) do
      value
    else
      _ -> value
    end
  end

  def dump_value(value, _), do: value

  def load_value(nil, _), do: nil

  def load_value(value, column) when is_list(value),
    do: Enum.map(value, &load_value(&1, column))

  def load_value(value, %{enum: enum}) when is_struct(enum) do
    case List.keyfind(enum.mappings, value, 1) do
      {enum_value, _} -> enum_value
      _ -> value
    end
  end

  def load_value(value, column) when not is_nil(column.struct_embed) do
    {_, struct_module, key_columns} = column.struct_embed

    values =
      Enum.map(value, fn {map_key, map_value} ->
        key_column = Enum.find(key_columns, &(to_string(&1.name) === map_key))

        key_column =
          if key_column && key_column.ecto_type === Ecto.UUID,
            do: %{key_column | ecto_type: nil},
            else: key_column

        {key_column.name, load_value(map_value, key_column)}
      end)

    struct(struct_module, values)
  end

  def load_value(value, %{ecto_type: Ecto.UUID}) do
    case Ecto.Type.load(Ecto.UUID, value) do
      {:ok, value} -> value
      :error -> nil
    end
  end

  def load_value(value, %{ecto_type: ecto_type})
      when ecto_type in ~w(utc_datetime utc_datetime_usec)a and
             is_struct(value, NaiveDateTime) do
    DateTime.from_naive!(value, "Etc/UTC")
  end

  def load_value(value, %{ecto_type: ecto_type})
      when ecto_type in ~w(utc_datetime utc_datetime_usec)a and
             is_binary(value) do
    case DateTime.from_iso8601(value) do
      {:ok, datetime, _} -> datetime
      _ -> value
    end
  end

  def load_value(value, %{ecto_type: ecto_type})
      when ecto_type in ~w(naive_datetime naive_datetime_usec)a and
             is_binary(value) do
    case NaiveDateTime.from_iso8601(value) do
      {:ok, datetime} -> datetime
      _ -> value
    end
  end

  def load_value(value, %{ecto_type: ecto_type})
      when ecto_type in ~w(date)a and
             is_binary(value) do
    case Date.from_iso8601(value) do
      {:ok, date} -> date
      _ -> value
    end
  end

  def load_value(value, _column), do: value
end