lib/phil_columns/factory.ex

defmodule PhilColumns.Factory do
  alias Ecto.Changeset

  defmacro __using__(opts) do
    quote do
      @before_compile PhilColumns.Factory
      Module.register_attribute(__MODULE__, :repo, accumulate: false)
      Module.put_attribute(__MODULE__, :repo, unquote(opts[:repo]))

      import PhilColumns.Factory, only: [sequence: 1, sequence: 2]

      def build(factory_name, attrs \\ %{}) do
        PhilColumns.Factory.build(__MODULE__, factory_name, attrs)
      end

      # def build_pair(factory_name, attrs \\ %{}) do
      # PhilColumns.Factory.build_pair(__MODULE__, factory_name, attrs)
      # end

      # def build_list(number_of_factories, factory_name, attrs \\ %{}) do
      # PhilColumns.Factory.build_list(__MODULE__, factory_name, attrs)
      # end

      def insert(factory_name, attrs \\ %{}) do
        PhilColumns.Factory.insert(__MODULE__, factory_name, attrs)
      end

      def params_for(factory_name, attrs \\ %{}) do
        PhilColumns.Factory.params_for(__MODULE__, factory_name, attrs)
      end

      def factory(factory_name) do
        raise "UndefinedFactoryError: factory(:#{factory_name})"
      end

      def factory(factory_name, _) do
        raise "UndefinedFactoryError: factory(:#{factory_name}, attrs)"
      end

      defoverridable factory: 1,
                     factory: 2
    end
  end

  @doc false
  defmacro __before_compile__(env) do
    repo = Module.get_attribute(env.module, :repo)

    quote do
      def repo, do: unquote(repo)
    end
  end

  def build(module, factory_name, attrs \\ %{}) do
    attrs = Enum.into(attrs, %{})

    apply(module, :factory, [factory_name])
    |> handle_build(attrs)
  end

  def insert(module, factory_name, attrs \\ %{}) do
    attrs = Enum.into(attrs, %{})
    repo = module.repo

    apply(module, :factory, [factory_name])
    |> handle_insert(attrs, repo)
  end

  def params_for(module, factory_name, attrs \\ %{}) do
    attrs = Enum.into(attrs, %{})
    # |> do_merge(attrs)
    apply(module, :factory, [factory_name])
    |> handle_params_for(attrs)
  end

  def sequence(name), do: PhilColumns.Sequence.next(name)

  def sequence(name, formatter), do: PhilColumns.Sequence.next(name, formatter)

  # Private ##########

  defp handle_build(%Changeset{} = changeset, attrs) do
    changeset
    |> Changeset.apply_changes()
    |> do_merge(attrs)
  end

  defp handle_build(%{__meta__: _} = record, attrs) do
    record
    |> do_merge(attrs)
  end

  defp handle_insert(%Changeset{} = changeset, attrs, repo) do
    record =
      changeset
      |> Changeset.apply_changes()
      |> do_merge(attrs)

    record
    |> repo.insert!
  end

  defp handle_insert(%{__meta__: _} = record, attrs, repo) do
    record
    |> do_merge(attrs)
    |> repo.insert!
  end

  defp handle_params_for(%Changeset{} = changeset, attrs) do
    changeset
    |> Changeset.apply_changes()
    |> do_merge(attrs)
    |> drop_ecto_fields
  end

  defp handle_params_for(%{__meta__: _} = record, attrs) do
    record
    |> do_merge(attrs)
    |> drop_ecto_fields
  end

  defp do_merge(%{__struct__: _} = record, attrs) do
    struct!(record, attrs)
  end

  defp do_merge(record, attrs) do
    Map.merge(record, attrs)
  end

  defp drop_ecto_fields(
         record = %{__struct__: struct, __meta__: %{__struct__: Ecto.Schema.Metadata}}
       ) do
    record
    |> Map.from_struct()
    |> Map.delete(:__meta__)
    |> Map.drop(struct.__schema__(:associations))
    |> Map.drop(struct.__schema__(:primary_key))
  end

  defp drop_ecto_fields(record) do
    raise ArgumentError, "#{inspect(record)} is not an Ecto model. Use `build` instead."
  end

  # defp module_from_struct(%{__struct__: struct_name}) do
  # struct_name
  # end

  # defp name_from_struct(%{__struct__: struct_name}) do
  # struct_name
  # |> Module.split
  # |> List.last
  # |> underscore
  # |> String.downcase
  # |> String.to_atom
  # end

  # defp underscore(name) do
  # Regex.split(~r/(?=[A-Z])/, name)
  # |> Enum.join("_")
  # end
end