defmodule Mix.Tasks.Enumex.Dynamic.Gen.Migration do @shortdoc "Generates an enum table migration." args = [ %{help: "A table for storing enum values", required: true, value_name: "TABLE"}, %{ help: "Enum field names mapper in format 'original:modified'", value_name: "OPTION_1 OPTION_2 … OPTION_N" } ] task = Enumex.Generator.Task.new("enumex.dynamic.gen.migration", args, []) if Mix.env() == :test do try do :meck.new(:io, [:unstick, :passthrough]) rescue _exception -> :ok end :meck.expect(:io, :columns, fn -> {:ok, 80} end) end help = Enumex.Generator.Task.help(task, @shortdoc, true) @moduledoc Enumex.Generator.Task.moduledoc(help) use Mix.Task alias Enumex.Generator.Task alias Mix.Generator alias Mix.Tasks.Ecto.Gen.Migration require Generator @help help @opts [ aliases: Task.aliases(), return_separator: true, strict: Task.options() ] @task_name task.name @version Task.version(task) @impl Mix.Task def run(args) do Task.run(@task_name, args, @help, @version, @opts, fn [table | args], [] -> case Enum.split_while(args, &(&1 != "--")) do {enum_bindings, ["--" | ecto_args]} when ecto_args != [] -> run(table, enum_bindings, ecto_args) {_enum_bindings, _ecto_args} -> Task.missing_args(@task_name, ~w[ECTO_GEN_MIGRATION_ARGS]) end end) end @spec run(String.t(), OptionParser.argv(), OptionParser.argv()) :: any() defp run(table, enum_bindings, ecto_args) do bindings = prepare_bindings(enum_bindings) bindings |> generate_unique_indexes() |> Enum.map_join("", &unique_index_template(fields: &1, table: table)) |> then(&[indexes: &1, opts: prepare_enum_opts(bindings), table: table]) |> changes_template() |> String.trim_trailing() # credo:disable-for-next-line Credo.Check.Refactor.Apply |> then(&apply(Migration, :run, [["--change", &1 | ecto_args]])) end @spec prepare_bindings(OptionParser.argv()) :: keyword(atom()) defp prepare_bindings(bindings) do bindings |> Enum.reduce([], fn option, acc -> option |> String.split(":") |> prepare_bindings(acc) end) |> Enum.sort() end @spec prepare_bindings(OptionParser.argv(), acc) :: acc when acc: keyword(atom()) defp prepare_bindings([original, modified], acc) when original not in ~w[enum_name id index] or modified == original do acc end defp prepare_bindings([original, modified], acc) do # credo:disable-for-next-line Credo.Check.Warning.UnsafeToAtom [{String.to_existing_atom(original), String.to_atom(modified)} | acc] end defp prepare_bindings(_list, acc), do: acc @spec generate_unique_indexes(keyword(atom())) :: [[atom()]] defp generate_unique_indexes(opts) do enum_name = opts[:enum_name] || :enum_name id = opts[:id] || :id index = opts[:index] || :index Enum.sort([[enum_name, id], [enum_name, id, index], [enum_name, index]]) end @spec prepare_enum_opts(keyword(atom())) :: String.t() defp prepare_enum_opts(bindings) do Enum.map_join(bindings, ", ", fn {key, value} -> "#{key}: #{inspect(value)}" end) end @spec changes_template(indexes: String.t(), opts: String.t(), table: String.t()) :: String.t() Generator.embed_template(:changes, """ import Enumex.Dynamic.Migration create table(<%= inspect(@table) %>) do enum_fields(<%= @opts %>) end <%= @indexes %> """) @spec unique_index_template(fields: [[atom()]], table: String.t()) :: String.t() Generator.embed_template(:unique_index, """ create unique_index(<%= inspect(@table) %>, <%= inspect(@fields) %>) """) end