defmodule Mix.Tasks.Mod.New do
use Mix.Task
@shortdoc "Create a new module in the current application"
import Modkit.Cli
def run(argv) do
Mix.Task.run("app.config")
{opts, args} =
command(__MODULE__)
|> option(:gen_server, :boolean,
alias: :g,
doc: "use GenServer and define base functions",
default: false
)
|> option(:supervisor, :boolean,
alias: :s,
doc: "use Supervisor and define base functions",
default: false
)
|> option(:path, :string,
alias: :p,
doc: "The path of the module to write. Unnecessary if the module prefix is mounted."
)
|> option(:overwrite, :boolean,
alias: :o,
doc: "Overwrite the file if it exists. Always prompt.",
default: false
)
|> argument(:module, required: true, cast: &Module.concat([&1]))
|> parse(argv)
opts =
opts
|> check_exclusive_opts()
|> add_path_opt(args.module)
parts_init = %{uses: [], attrs: [], apis: [], impls: []}
parts =
opts
|> Map.take([:gen_server, :supervisor])
|> Enum.filter(&elem(&1, 1))
|> Keyword.keys()
|> Enum.reduce(parts_init, &collect_parts/2)
code =
assemble_parts(args.module, parts)
|> :erlang.iolist_to_binary()
|> Code.format_string!(formatter_opts())
write_code(opts, code)
success_stop("wrote code to #{opts.path}")
end
defp write_code(%{path: path, overwrite: over?}, code) do
if can_write?(path, over?) do
:ok = ensure_dir(Path.dirname(path))
File.write!(path, code)
else
abort("file exists: #{path}")
end
end
defp ensure_dir(path) do
if File.dir?(path) do
:ok
else
ensure_dir(Path.dirname(path))
File.mkdir!(path)
end
end
defp can_write?(path, prompt?) do
if File.exists?(path) do
if prompt? do
Mix.Shell.IO.yes?("Overwrite file #{path}?", default: :no)
else
false
end
else
true
end
end
defp check_exclusive_opts(%{gen_server: true, supervisor: true}) do
abort("--gen-server and --supervisor are mutually exclusive")
end
defp check_exclusive_opts(opts) do
opts
end
defp add_path_opt(%{path: path}, _),
do: path
defp add_path_opt(opts, module) do
project = Modkit.Config.current_project()
mount = Modkit.Config.mount(project)
case Modkit.Mod.preferred_path(module, mount) do
{:error, :no_mount_point} ->
abort("The --path option is required when the module prefix is not mounted.")
{:ok, path} ->
Map.put(opts, :path, path)
end
end
def collect_parts(:gen_server, parts) do
parts
|> add_part(:uses, "use GenServer")
|> add_part(:attrs, "@gen_opts ~w(name timeout debug spawn_opt hibernate_after)a")
|> add_part(:apis, """
def start_link(opts) do
{gen_opts, opts} = Keyword.split(opts, @gen_opts)
GenServer.start_link(__MODULE__, opts, gen_opts)
end
""")
|> add_part(:apis, """
@impl GenServer
def init(opts) do
{:ok, opts}
end
""")
end
def collect_parts(:supervisor, parts) do
parts
|> add_part(:uses, "use Supervisor")
|> add_part(:attrs, "@gen_opts ~w(name)a")
|> add_part(:apis, """
def start_link(opts) do
{gen_opts, opts} = Keyword.split(opts, @gen_opts)
Supervisor.start_link(__MODULE__, opts, gen_opts)
end
""")
|> add_part(:apis, """
@impl Supervisor
def init(_init_arg) do
children = [
{Worker, key: :value}
]
Supervisor.init(children, strategy: :one_for_one)
end
""")
end
defp add_part(parts, group, code) do
Map.update!(parts, group, &[code | &1])
end
defp assemble_parts(module, %{uses: uses, attrs: attrs, apis: apis, impls: impls}) do
uses = :lists.reverse(uses)
apis = :lists.reverse(apis)
impls = :lists.reverse(impls)
attrs = :lists.reverse(attrs)
[
"defmodule #{inspect(module)} do",
~S(
@moduledoc """
TODO Start documenting the module by writing a short description of its purpose.
"""
),
uses,
attrs,
apis,
impls,
"end"
]
|> :lists.flatten()
|> Enum.intersperse("\n\n")
end
defp formatter_opts do
file = ".formatter.exs"
if File.regular?(file) do
{opts, _} = Code.eval_file(file)
opts
else
[]
end
end
end