Current section
Files
Jump to
Current section
Files
lib/mix/tasks/enumex.dynamic.gen.enum.ex
# credo:disable-for-next-line Credo.Check.Refactor.ModuleDependencies
defmodule Mix.Tasks.Enumex.Dynamic.Gen.Enum do
@shortdoc "Generates an enum module with dynamic values."
args = [
%{help: "Specifies an enum module", required: true, value_name: "MODULE"},
%{help: "Specifies an enum name", required: true, value_name: "ENUM_NAME"},
%{
help: "Fields in format 'original:type' or 'original:type:modified'",
value_name: "FIELD_1 FIELD_2 … FIELD_N"
}
]
flags = [
%{help: "Specifies file path for enum", long: "--file", short: "-f", value_name: "FILE_PATH"},
%{
help: "Specifies a query prefix",
long: "--query-prefix",
short: "-q",
value_name: "QUERY_PREFIX"
},
%{help: "Specifies an ecto repo", long: "--repo", short: "-r", value_name: "ECTO_REPO"},
%{
help: "Specifies a table prefix",
long: "--table-prefix",
short: "-t",
value_name: "TABLE_PREFIX"
}
]
task = Enumex.Generator.Task.new("enumex.dynamic.gen.enum", 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)
@moduledoc Enumex.Generator.Task.moduledoc(help)
use Mix.Task
alias Enumex.Generator.Task
alias Mix.Generator
require Generator
@help help
@opts [
aliases: Task.aliases(~w[file table_prefix query_prefix]a),
strict: Task.options(file: :string, query_prefix: :string, table_prefix: :string)
]
@task_name task.name
@version Task.version(task)
@impl Mix.Task
def run(args) do
Task.run(@task_name, args, @help, @version, @opts, fn
[_module], _opts -> Task.missing_args(@task_name, ["ENUM_NAME"])
[module, name | fields], opts -> generate_enum(module, name, fields, opts)
end)
end
@spec generate_enum(
String.t(),
String.t(),
OptionParser.argv(),
OptionParser.options()
) :: [boolean()]
defp generate_enum(module, name, fields, opts) do
~r/^.+(?<last_dot>\.)[^\.]+$/
|> Regex.split(module, on: [:last_dot])
|> then(fn [parent_module, module_part] ->
generate_enum(parent_module, module_part, name, fields, opts)
end)
end
@spec generate_enum(
String.t(),
String.t(),
String.t(),
OptionParser.argv(),
OptionParser.options()
) :: [boolean()]
defp generate_enum(parent_module, module_part, name, fields, opts) do
Mix.Task.run("loadpaths")
{file_path, opts} = Keyword.pop(opts, :file, gen_file_path(parent_module))
{chardata, enum_opts} = Enum.reduce(fields, {[], []}, &prepare_field/2)
contents =
enum_template(
enum_opts: format_enum_opts(enum_opts),
fields: IO.iodata_to_binary(chardata),
module: parent_module,
module_part: module_part,
# credo:disable-for-next-line Credo.Check.Warning.UnsafeToAtom
name: String.to_atom(name),
opts: format_opts(opts)
)
file_path
|> tap(fn path ->
path
|> Path.dirname()
|> Generator.create_directory()
end)
|> tap(&Generator.create_file(&1, contents))
|> List.wrap()
end
@spec format_enum_opts(keyword(atom())) :: String.t()
defp format_enum_opts(enum_opts) do
enum_opts
|> List.keysort(0)
|> Enum.map_join(", ", fn {key, value} -> "#{key}: #{inspect(value)}" end)
end
@spec format_opts(keyword(String.t())) :: String.t()
defp format_opts(opts) do
Enum.map_join(opts, ",\n ", fn {key, value} -> "#{key}: #{inspect(value)}" end)
end
@spec gen_file_path(String.t()) :: Path.t()
defp gen_file_path(module) do
module
|> Macro.underscore()
|> then(&Path.join(["lib", &1 <> ".ex"]))
end
@spec prepare_field(String.t(), acc) :: acc when acc: {IO.chardata(), keyword(String.t())}
defp prepare_field(arg, {acc, opts}) do
arg
|> String.split(":")
|> prepare_field(acc, opts)
end
@spec prepare_field([String.t()], IO.chardata(), keyword(String.t())) ::
{IO.chardata(), keyword(String.t())}
defp prepare_field([original, type], acc, opts) when original in ~w[enum_name id index] do
# credo:disable-for-next-line Credo.Check.Warning.UnsafeToAtom
{acc, Keyword.put(opts, :"#{original}_type", String.to_atom(type))}
end
defp prepare_field([original, type], acc, opts) do
# credo:disable-for-next-line Credo.Check.Warning.UnsafeToAtom
{[acc, "\n ", field_template(name: String.to_atom(original), type: String.to_atom(type))],
opts}
end
defp prepare_field([original, type, modified], acc, opts)
when original in ~w[enum_name id index] do
{acc,
Keyword.merge(
opts,
[
# credo:disable-for-next-line Credo.Check.Warning.UnsafeToAtom
{String.to_existing_atom(original), String.to_atom(modified)},
# credo:disable-for-next-line Credo.Check.Warning.UnsafeToAtom
{:"#{original}_type", String.to_atom(type)}
]
)}
end
defp prepare_field([original, type, source], acc, opts) do
{[
acc,
"\n ",
field_with_source_template(
# credo:disable-for-next-line Credo.Check.Warning.UnsafeToAtom
name: String.to_atom(original),
# credo:disable-for-next-line Credo.Check.Warning.UnsafeToAtom
source: String.to_atom(source),
# credo:disable-for-next-line Credo.Check.Warning.UnsafeToAtom
type: String.to_atom(type)
)
], opts}
end
@spec enum_template(
enum_opts: String.t(),
fields: String.t(),
module: String.t(),
module_part: String.t(),
name: atom(),
opts: String.t()
) :: String.t()
Generator.embed_template(:enum, """
defmodule <%= @module %> do
use Enumex.Dynamic, components: [
# Enumex.Dynamic.Components.Context,
# Enumex.Dynamic.Components.Convert,
# Enumex.Dynamic.Components.EctoChangeset,
<%= if @opts == "", do: "# Enumex.Dynamic.Components.EctoSchema", else: "# {Enumex.Dynamic.Components.EctoSchema, " <> @opts <> "}" %>,
# Enumex.Dynamic.Components.Typespecs
]
enum <%= @module_part %>, <%= inspect(@name) %><%= if @enum_opts != "", do: ", " <> @enum_opts %> do<%= if @fields != "", do: @fields %>
end
end
""")
@spec field_template(name: atom(), type: atom()) :: String.t()
Generator.embed_template(:field, ~s[field <%= inspect(@name) %>, <%= inspect(@type) %>])
@spec field_template(name: atom(), source: atom(), type: atom()) :: String.t()
Generator.embed_template(
:field_with_source,
~s[field <%= inspect(@name) %>, <%= inspect(@type) %>, source: <%= inspect(@source) %>]
)
end