defmodule Mix.Tasks.Coh.Clean do
@moduledoc """
This task will clean most of the files installed by the `mix coh.install` task.
Projects created with both `coh.install` and `coherence.install` can be
cleaned with this task. It will auto detect the project structure and remove
the appropriate files.
## Examples
# Clean all the installed files
mix coh.clean --all
# Clean only the installed view and template files
mix coh.clean --views --templates
# Clean all but the models
mix coh.clean --all --no-models
# Prompt once to confirm the removal
mix coh.clean --all --confirm-once
# Clean installed options
mix coh.clean --options rememberable
mix coh.clean --options "registerable invitable trackable"
The following options are supported:
* `--all` -- clean all files
* `--views` -- clean view files
* `--templates` -- clean template files
* `--models` -- clean models files
* `--controllers` -- clean controller files
* `--email` -- clean email files
* `--web` -- clean the web/coherence_web.ex file
* `--messages` -- clean the web/coherence_messages.ex file
* `--migrations` -- clean the migration files
* `--options` -- Clean one or more specific options
* `--dry-run` -- Show what will be removed, but don't actually remove any files
* `--confirm-once` -- confirm once before removing all selected files
Disable options:
* `--no-confirm` - don't confirm before removing files
"""
use Mix.Task
import Coherence.Mix.Utils
import Mix.Ecto
alias Mix.Tasks.Coh.Install
@shortdoc "Clean files created by the coherence installer."
@dialyzer [
{:nowarn_function, raise_options_error!: 2}
]
@config_file "config/config.exs"
@remove_opts ~w(views templates models controllers emails web messages migrations config)a
@default_opts ~w(confirm)a
@clean_opts [{:options, :string}]
@switches Enum.map([:all, :confirm_once | @remove_opts] ++ @default_opts, &{&1, :boolean}) ++
@clean_opts ++ [dry_run: :boolean]
@spec run([String.t()] | []) :: any
def run(args) do
args
|> OptionParser.parse(switches: @switches)
|> do_config
|> do_clean
|> do_clean_options
end
defp do_clean(config) do
confirm_once(config, fn config ->
Enum.reduce(@remove_opts, config, &remove!(&2, &1))
end)
end
defp do_clean_options(config) do
confirm_once(config, fn config ->
remove!(config, :options)
end)
end
defp confirm_once(%{confirm_once: true} = config, fun) do
config = Map.put(config, :confirm, false)
if Mix.shell().yes?("Are you sure you want to delete coherence files?") do
fun.(config)
end
config
end
defp confirm_once(config, fun) do
fun.(config)
end
defp valid_option?(all_options, option) do
Enum.any?(all_options, &(&1 == option))
end
defp validate_options!(options, all_options) do
case Enum.filter(options, &(not valid_option?(all_options, &1))) do
[] -> :ok
list -> raise_options_error!(list, all_options)
end
end
defp option_string(options) do
options
|> Enum.map(&Atom.to_string/1)
|> Enum.join(", ")
end
defp raise_options_error!(list, all_options) do
Mix.raise("""
Invalid Option(s): '#{option_string(list)}' are not valid option(s)
Please choose from list:
'#{option_string(all_options)}'
""")
end
defp get_current_options(options) do
:coherence
|> Application.get_env(:opts)
|> log_invalid_options(options)
end
defp log_invalid_options(current_options, options) do
case Enum.filter(options, &(not valid_option?(current_options, &1))) do
[] ->
options
invalid_list ->
Mix.shell().info(
"Option(s) #{option_string(invalid_list)} where not found in your configuration. They will not be removed."
)
options -- invalid_list
end
end
defp remove_view_file!(option, config) do
view_files = Install.view_files()
case view_files[option] do
nil ->
:ok
file_name ->
path = web_path(["views", "coherence", file_name])
confirm(config, path, fn -> rm!(path) end)
end
option
end
defp remove_template_files!(option, config) do
files = Install.template_files()
files
|> Enum.find(fn {_, {opt, _}} -> opt == option end)
|> case do
nil ->
:ok
{path, _} ->
path = web_path(["templates", "coherence", "#{path}"])
confirm(config, path, fn -> rm_dir!(path) end)
end
option
end
defp remove_controller_files!(option, config) do
case controller_files()[option] do
nil ->
:ok
file_name ->
path = web_path(["controllers", "coherence", file_name])
confirm(config, path, fn -> rm!(path) end)
end
option
end
defp remove_config_options!(options, config) do
with contents when is_binary(contents) <- File.read!(@config_file),
[_, opts_string] when is_binary(opts_string) <-
Regex.run(~r/opts:\s*(\[.*?\])/, contents) do
remove_config_options!(config, contents, options, opts_string)
else
error ->
Mix.raise("""
Problem updating config, error: #{inspect(error)}
""")
end
end
defp remove_config_options!(config, contents, options, opts_string) do
new_opts_string =
Enum.reduce(options, opts_string, fn option, acc ->
String.replace(acc, ~r/(,\s*:#{option})|(:#{option}\s*,?\s*)/, "")
end)
message = ~s'"opts: #{stringify(opts_string)}" with "opts: #{stringify(new_opts_string)}"'
confirm_config(config, message, fn ->
File.write!(@config_file, String.replace(contents, opts_string, new_opts_string))
end)
config
end
defp stringify(item) do
item
|> inspect
|> String.replace("\"", "")
end
defp remove_option!(config, option) do
option
|> remove_view_file!(config)
|> remove_template_files!(config)
|> remove_controller_files!(config)
end
defp remove!(%{options: options} = config, :options) do
all_options = Install.all_options()
options =
options
|> String.split(" ", trim: true)
|> Enum.map(&String.replace(&1, "-", "_"))
|> Enum.map(&String.to_atom/1)
validate_options!(options, all_options)
options
|> get_current_options
|> Enum.map(&remove_option!(config, &1))
|> remove_config_options!(config)
config
end
defp remove!(%{templates: true} = config, :templates) do
path = web_path("templates/coherence")
confirm(config, path, fn -> rm_dir!(path) end)
end
defp remove!(%{views: true} = config, :views) do
path = web_path("views/coherence")
confirm(config, path, fn -> rm_dir!(path) end)
end
defp remove!(%{controllers: true} = config, :controllers) do
path = web_path("controllers/coherence")
confirm(config, path, fn -> rm_dir!(path) end)
end
defp remove!(%{models: true} = config, :models) do
path = lib_path("coherence")
confirm(config, path, fn -> rm_dir!(path) end)
end
defp remove!(%{web: true} = config, :web) do
path = web_path("coherence_web.ex")
confirm(config, path, fn -> rm!(path) end)
end
defp remove!(%{messages: true} = config, :messages) do
path = web_path("coherence_messages.ex")
confirm(config, path, fn -> rm!(path) end)
end
defp remove!(%{emails: true} = config, :emails) do
path = web_path(~w(emails coherence))
confirm(config, path, fn -> rm_dir!(path) end)
end
defp remove!(%{migrations: true} = config, :migrations) do
case Application.get_env(:coherence, :repo) do
nil ->
Mix.shell().error("Config.repo not configured. Skipping migration removal!")
repo ->
do_remove!(config, repo)
end
end
defp remove!(%{config: true} = config, :config) do
confirm(config, "coherence config", fn ->
regex = ~r/# %% Coherence Configuration %%.+?# %% End Coherence Configuration %%/s
conf = File.read!(@config_file)
File.write!(@config_file, String.replace(conf, regex, ""))
end)
end
defp remove!(config, _), do: config
defp do_remove!(config, repo) do
ensure_repo(repo, [])
path = Path.relative_to(migrations_path(repo), Mix.Project.app_path())
case Path.wildcard(path <> "/*coherence*") do
[] ->
config
files ->
confirmed? =
if config[:confirm] do
Mix.shell().info("Found migrations: " <> Enum.join(files, ", "))
Mix.shell().yes?("Delete them?")
else
true
end
if confirmed? do
Enum.each(files, &rm!(&1))
end
config
end
end
defp confirm_config(%{dry_run: true} = config, message, _fun) do
Mix.shell().info("Update config " <> message)
config
end
defp confirm_config(%{confirm: true} = config, message, fun) do
if Mix.shell().yes?("Update config " <> message <> "?") do
Mix.shell().info("Updating config " <> message)
fun.()
end
config
end
defp confirm_config(config, message, fun) do
Mix.shell().info("Updating config " <> message)
fun.()
config
end
defp confirm(%{dry_run: true} = config, path, _fun) do
Mix.shell().info("Delete #{path}")
config
end
defp confirm(%{confirm: true} = config, path, fun) do
if File.exists?(path) do
if Mix.shell().yes?("Delete #{path}?") do
fun.()
end
else
Mix.shell().error("Problem removing #{path}")
end
config
end
defp confirm(config, _path, fun) do
fun.()
config
end
###############
# configuration
defp do_config({opts, parsed, unknown}) do
opts
|> verify_opts(parsed, unknown)
|> Enum.into(%{})
|> do_all_config
|> do_default_config
end
defp do_all_config(%{all: true} = config) do
Enum.reduce(@remove_opts, config, &Map.put_new(&2, &1, true))
end
defp do_all_config(config), do: config
defp do_default_config(config) do
Enum.reduce(@default_opts, config, fn opt, config ->
Map.put(config, opt, Map.get(config, opt, true))
end)
end
defp verify_opts(opts, parsed, unknown) do
verify_args!(parsed, unknown)
switch_keys = Keyword.keys(@switches)
case opts |> Keyword.keys() |> Enum.filter(&(not (&1 in switch_keys))) do
[] -> opts
list -> raise_option_errors(list)
end
end
defp coh? do
not File.exists?("web")
end
defp web_path(path) when is_binary(path), do: Path.join(web_path(), path)
defp web_path(paths), do: Path.join([web_path() | paths])
defp web_path do
if coh?(), do: Path.join("lib", otp_app() <> "_web"), else: "web"
end
defp lib_path, do: Path.join("lib", otp_app())
defp lib_path(path) when is_binary(path), do: Path.join(lib_path(), path)
# defp lib_path(paths), do: Path.join([lib_path() | paths])
defp otp_app do
Mix.Project.config()[:app] |> to_string
end
end