# 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/^.+(?\.)[^\.]+$/ |> 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