Packages

A comprehensive library for working with both static and dynamic enum structures in Elixir.

Current section

Files

Jump to
enumex lib mix tasks enumex.static.gen.migration.ex
Raw

lib/mix/tasks/enumex.static.gen.migration.ex

# credo:disable-for-next-line Credo.Check.Refactor.ModuleDependencies
defmodule Mix.Tasks.Enumex.Static.Gen.Migration do
@shortdoc "Generates a migration for updating a database enum"
args = [
%{help: "Specifies an enum module", required: true, value_name: "MODULE"},
%{help: "Specifies an enum name", required: true, value_name: "ENUM_NAME"},
%{help: "Specifies an ecto repo", required: true, value_name: "ECTO_REPO"}
]
flags = [
%{help: "Specifies an enum adapter", long: "--adapter", short: "-a", value_name: "ADAPTER"},
%{
help: "Specifies a query prefix",
long: "--query-prefix",
short: "-q",
value_name: "QUERY_PREFIX"
}
]
task = Enumex.Generator.Task.new("enumex.static.gen.migration", args, flags)
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 Ecto.Repo
alias Enumex.Generator.Task
alias Enumex.Static.Adapter
alias Enumex.Static.Components
alias Enumex.Value
alias Mix.Generator
alias Mix.Project
alias Mix.Tasks.Ecto.Gen.Migration
alias Mix.Tasks.Enumex.Static.Gen.Migration.New
require Generator
@help help
@opts [
aliases: Task.aliases(~w[adapter query_prefix]a),
return_separator: true,
strict: Task.options(adapter: :string, query_prefix: :string)
]
@task_name task.name
@version Task.version(task)
@impl Mix.Task
def run(args) do
Task.static_migration_run(@task_name, args, @help, @version, @opts, &run(&1, &2, &3, &4, args))
end
@spec run(
[Value.enum_module() | Value.enum_name()],
[query_prefix: Adapter.query_prefix()],
Adapter.t(),
[],
OptionParser.argv()
) :: :ok
defp run([_module], _opts, _adapter, [], _all_args) do
Task.missing_args(@task_name, ~w[ENUM_NAME ECTO_REPO ECTO_GEN_MIGRATION_ARGS])
end
defp run([_module], _opts, _adapter, _ecto_args, _all_args) do
Task.missing_args(@task_name, ~w[ENUM_NAME ECTO_REPO])
end
defp run([_module, _name], _opts, _adapter, [], _all_args) do
Task.missing_args(@task_name, ~w[ECTO_REPO ECTO_GEN_MIGRATION_ARGS])
end
defp run([_module, _name, _repo], _opts, _adapter, [], _all_args) do
Task.missing_args(@task_name, ~w[ECTO_GEN_MIGRATION_ARGS])
end
@spec run(
[Value.enum_module() | Value.enum_name()],
[query_prefix: Adapter.query_prefix()],
Adapter.t(),
OptionParser.argv(),
OptionParser.argv()
) :: :ok
defp run([_module, _name], _opts, _adapter, _ecto_args, _all_args) do
Task.missing_args(@task_name, ~w[ECTO_REPO])
end
@spec run(
[Value.enum_module() | Value.enum_name() | String.t()],
[query_prefix: Adapter.query_prefix()],
Adapter.t(),
OptionParser.argv(),
OptionParser.argv()
) :: :ok | any()
defp run([module, name, ecto_repo], opts, adapter, ecto_args, all_args) do
{:ok, _aps} = Application.ensure_all_started(Project.config()[:app])
# credo:disable-for-next-line Credo.Check.Warning.UnsafeToAtom
repo = Module.concat([ecto_repo])
cond do
Code.ensure_loaded?(repo) == false ->
Task.print_errors(@task_name, ["repo module #{inspect(repo)} could not be loaded"])
{:behaviour, [Repo]} not in repo.__info__(:attributes) ->
Task.behaviour_not_implemented(@task_name, repo, Repo)
GenServer.whereis(repo) == nil ->
Task.print_errors(@task_name, [
"the #{inspect(repo)} has not been started, " <>
"ensure it's added to your application's supervisor tree"
])
{:behaviour, [Components.List]} not in module.__info__(:attributes) ->
Task.behaviour_not_implemented(@task_name, module, Components.List)
true ->
run(module, name, repo, adapter, opts, ecto_args, all_args)
end
end
defp run([_enum_module, _enum_name, _repo | args], _enum_opts, _adapter, ecto_args, _all_args) do
Task.static_migration_invalid_args(@task_name, args, ecto_args)
end
@spec run(
Value.enum_module(),
Value.enum_name(),
Repo.t(),
Adapter.t(),
[query_prefix: Adapter.query_prefix()],
OptionParser.argv(),
OptionParser.argv()
) :: any()
defp run(module, name, repo, adapter, opts, ecto_args, all_args) do
case adapter.get_values_with_index(repo, name, log: false, query_prefix: opts[:query_prefix]) do
{:error, %{postgres: %{pg_code: "42704"}}} ->
all_args
|> List.delete_at(2)
|> New.run()
{:ok, %{rows: values}} ->
values
# credo:disable-for-next-line Credo.Check.Warning.UnsafeToAtom
|> Map.new(fn [id, index] -> {index, String.to_atom(id)} end)
|> gen_migration(module.list(name), ecto_args,
adapter: adapter,
module: module,
name: name,
prefix: opts[:query_prefix]
)
end
end
@spec gen_migration(%{Value.index() => Value.id()}, [Value.t()], OptionParser.argv(),
adapter: Adapter.t(),
args: String.t(),
module: Value.enum_module(),
name: Value.enum_name()
) :: any()
defp gen_migration(db_values, values, ecto_args, assigns) do
db_values
|> gen_changes(Map.new(values, &{&1.index, &1.id}), assigns)
|> String.trim_trailing()
|> then(&changes_template([{:changes, &1} | assigns]))
|> String.trim_trailing()
# credo:disable-for-next-line Credo.Check.Refactor.Apply
|> then(&Migration.run(["--change", &1 | ecto_args]))
end
@spec gen_changes(
%{Value.index() => Value.id()},
%{Value.index() => Value.id()},
adapter: Adapter.t(),
args: String.t(),
module: Value.enum_module(),
name: Value.enum_name()
) :: String.t()
defp gen_changes(db_values, values, assigns) when is_map(db_values) do
db_values
|> Enum.reduce({values, [], [], []}, &collect_changes/2)
|> then(fn {adds, drops, renames, remaining} ->
{Map.to_list(adds), drops, renames, remaining}
end)
|> gen_changes(db_values, assigns)
end
@spec gen_changes(
{[{Value.index(), Value.id()}], [{Value.index(), Value.id()}],
[{Value.index(), Value.id(), Value.id()}], [Value.t()]},
%{Value.index() => Value.id()},
adapter: Adapter.t(),
args: String.t(),
module: Value.enum_module(),
name: Value.enum_name()
) :: String.t()
defp gen_changes({adds, drops, renames, remaining}, db_values, assigns) do
remaining
|> List.last(List.last(renames))
|> then(&{default_value_template(value: to_value(&1, assigns)), db_values})
|> do_gen_changes(renames, :renames, assigns)
|> do_gen_changes(adds, :adds, assigns)
|> do_gen_changes(drops, :drops, assigns)
|> then(fn {changes, _values} -> changes end)
end
@spec do_gen_changes(
acc,
[{Value.index(), Value.id()} | {Value.index(), Value.id(), Value.id()}],
:adds | :drops | :renames,
adapter: Adapter.t(),
args: String.t(),
module: Value.enum_module(),
name: Value.enum_name()
) :: acc
when acc: {String.t(), %{Value.index() => Value.id()}}
defp do_gen_changes(acc, data, :adds, assigns) do
Enum.reduce(data, acc, fn {index, id}, {changes, values} ->
change = add_value_template([{:value, to_value({index, id}, assigns)} | assigns])
{changes <> change, Map.put(values, index, id)}
end)
end
defp do_gen_changes(acc, data, :drops, assigns) do
Enum.reduce(data, acc, fn {index, id}, {changes, values} ->
change = drop_value_template([{:value, to_value({index, id}, assigns)} | assigns])
{changes <> change, Map.delete(values, index)}
end)
end
defp do_gen_changes(acc, data, :renames, assigns) do
Enum.reduce(data, acc, fn {index, old_id, new_id}, {changes, values} ->
[
{:new_value, to_value({index, new_id}, assigns)},
{:old_value, to_value({index, old_id}, assigns)} | assigns
]
|> rename_value_template()
|> then(&{changes <> &1, Map.replace(values, index, new_id)})
end)
end
@spec collect_changes({Value.index(), Value.id()}, acc) :: acc
when acc:
{%{Value.index() => Value.id()}, [{Value.index(), Value.id()}],
[{Value.index(), Value.id(), Value.id()}], [{Value.index(), Value.id()}]}
defp collect_changes({index, id}, {values, drops, renames, remaining})
when not is_map_key(values, index) do
{values, [{index, id} | drops], renames, remaining}
end
defp collect_changes({index, id}, {values, drops, renames, remaining})
when :erlang.map_get(index, values) != id do
{Map.delete(values, index), drops, [{index, id, values[index]} | renames], remaining}
end
defp collect_changes({index, id}, {values, drops, renames, remaining}) do
{Map.delete(values, index), drops, renames, [{index, id} | remaining]}
end
@spec to_value({Value.index(), Value.id()},
adapter: Adapter.t(),
args: String.t(),
module: Value.enum_module(),
name: Value.enum_name()
) :: Value.t()
defp to_value({index, id}, assigns), do: Value.new(assigns[:module], assigns[:name], id, index)
@spec changes_template(
adapter: Adapter.t(),
args: String.t(),
changes: String.t(),
module: Value.enum_module(),
name: Value.enum_name()
) :: String.t()
Generator.embed_template(:changes, """
alias Enumex.Static.Migration
alias Enumex.Value
alias <%= inspect(@module) %>
adapter = <%= inspect(@adapter) %>
<%= @changes %>
""")
@spec default_value_template(value: Value.t()) :: String.t()
Generator.embed_template(:default_value, """
# Note: a first valid value has been assigned as a default one
# in order to generate a fully reversible migration
# and you may be interested in changing it
default_value = <%= Task.gen_value_from_template(value: @value) %>
""")
@spec changes_template(
adapter: Adapter.t(),
args: String.t(),
module: Value.enum_module(),
name: Value.enum_name(),
value: Value.t()
) :: String.t()
Generator.embed_template(:add_value, """
# Info: adding <%= @value.id %> with index <%= @value.index %>
new_value = <%= Task.gen_value_from_template(value: @value) %>
Migration.add_value(adapter, new_value, default_value<%= @prefix && ", query_prefix: " <> inspect(@prefix) %>)
""")
@spec drop_value_template(
adapter: Adapter.t(),
args: String.t(),
module: Value.enum_module(),
name: Value.enum_name(),
value: Value.t()
) :: String.t()
Generator.embed_template(:drop_value, """
# Info: removing <%= @value.id %> with index <%= @value.index %>
old_value = <%= Task.gen_value_from_template(value: @value) %>
Migration.drop_value(adapter, old_value, default_value<%= @prefix && ", query_prefix: " <> inspect(@prefix) %>)
""")
@spec rename_value_template(
adapter: Adapter.t(),
args: String.t(),
module: Value.enum_module(),
name: Value.enum_name(),
new_value: Value.t(),
old_value: Value.t()
) :: String.t()
Generator.embed_template(:rename_value, """
# Info: renaming <%= @old_value.id %> with index <%= @old_value.index %> to <%= @new_value.id %>
old_value = <%= Task.gen_value_from_template(value: @old_value) %>
new_value = <%= Task.gen_value_from_template(value: @new_value) %>
Migration.rename_value(adapter, old_value, new_value<%= @prefix && ", query_prefix: " <> inspect(@prefix) %>)
""")
end