Current section
Files
Jump to
Current section
Files
lib/mix/tasks/whatwasit.install.ex
defmodule Mix.Tasks.Whatwasit.Install do
@moduledoc """
Setup the Whatwasit package for your Phoenix application.
Adds a migration for the Version model used to trackage changes to
the desired models.
Prints example configuration that should be added to your
config/config.exs file.
## Examples
# create the migration and print configuration
mix whatwasit.install
# print configuration
mix whatwasit.install --no-migrations
# Add current user tracking
mix whatwasit.install --whodoneit
# use a different user model
mix whatwasit.install --whodoneit --model="Account accounts"
# use a simple whodoneit_id field
mix whatwasit.install --whodoneit-id=integer
# store the whodoneit model in a map
mix whatwasit.install --whodoneit-map
# set the whodoneit model foreign key type to uuid
mix whatwasit.install --whodoneit-id-type=uuid
The following options are available:
* `--model` -- The authentication model and table_name
* `--repo` -- The project's repo if different than the standard default
* `--module` -- The projects base module
* `--migration-path` -- The migration path
* `--whodoneit` -- Add current user tracking
* `--whodoneit-id` -- Use a simple id field instead of a relationship
* `--whodoneit-map` -- Store the current_user as a map
* `--whodoneit-id-type` -- Set the foreign key type
The following options are available to disable features:
* `--no-migrations` -- Don't generate the migration
* `--no-boilerplate` -- Don't generate anything
* `--no-models` -- Dont' generate models
"""
@shortdoc "Configure the Whatwasit Package"
import Macro, only: [camelize: 1, underscore: 1]
import Mix.Generator
import Mix.Ecto
import Whatwasit.Mix.Utils
@default_options ~w()
# the options that default to true, and can be disabled with --no-option
@default_booleans ~w(config migrations boilerplate models)
# all boolean_options
@boolean_options @default_booleans
@switches [repo: :string, migration_path: :string, model: :string, module: :string, whodoneit: :boolean, whodoneit_id: :string, whodoneit_map: :boolean, whodoneit_id_type: :string] ++ Enum.map(@boolean_options, &({String.to_atom(&1), :boolean}))
@switch_names Enum.map(@switches, &(elem(&1, 0)))
def run(args) do
{opts, parsed, unknown} = OptionParser.parse(args, switches: @switches)
verify_args!(parsed, unknown)
{bin_opts, opts} = parse_options(opts)
do_config(opts, bin_opts)
|> do_run
end
def do_run(config) do
config
|> gen_migration
|> gen_version_model
|> print_instructions
end
defp gen_version_model(%{models: true, whodoneit_map: true, boilerplate: true, binding: binding} = config) do
changeset_fields = "~w(item_type item_id object action whodoneit)a"
schema_fields = "field :whodoneit, :map\n"
binding = binding ++ [
schema_fields: schema_fields,
changeset_fields: changeset_fields
]
Mix.Phoenix.copy_from paths(),
"priv/templates/whatwasit.install/models/whatwasit", "", binding, [
{:eex, "version_map.ex", "web/models/whatwasit/version.ex"},
]
config
end
defp gen_version_model(%{models: true, boilerplate: true, binding: binding} = config) do
name_field = "field :whodoneit_name, :string\n"
changeset_fields = "item_type item_id object action"
whodoneit_changeset_fields = " whodoneit_id whodoneit_name"
whodoneit_id = config[:whodoneit_id]
whodoneit = config[:whodoneit]
{schema_fields, add_changeset_fields} = cond do
config[:whodoneit_id_type] ->
{name_field <> " belongs_to :whodoneit, #{config[:user_schema] |> elem(0)}, type: Ecto.UUID\n", whodoneit_changeset_fields}
whodoneit_id ->
{name_field <> " field :whodoneit_id, :#{whodoneit_id}\n", whodoneit_changeset_fields}
whodoneit ->
{name_field <> " belongs_to :whodoneit, #{config[:user_schema] |> elem(0)}\n", whodoneit_changeset_fields}
true ->
{"", ""}
end
binding = binding ++ [
schema_fields: schema_fields,
changeset_fields: "~w(#{changeset_fields}#{add_changeset_fields})a"
]
Mix.Phoenix.copy_from paths(),
"priv/templates/whatwasit.install/models/whatwasit", "", binding, [
{:eex, "version.ex", "web/models/whatwasit/version.ex"},
]
config
end
defp gen_version_model(config) do
config
end
defp gen_migration(%{migrations: true, boilerplate: true} = config) do
{_, table_name} = config[:user_schema]
whodoneit = cond do
config[:whodoneit_id_type] ->
"""
add :whodoneit_name, :string
add :whodoneit_id, references(:#{table_name}, on_delete: :nilify_all, type: :uuid)
"""
config[:whodoneit_map] ->
"""
add :whodoneit, :map
"""
config[:whodoneit_id] ->
"""
add :whodoneit_name, :string
add :whodoneit_id, :#{config[:whodoneit_id]}
"""
config[:whodoneit] ->
"""
add :whodoneit_name, :string
add :whodoneit_id, references(:#{table_name}, on_delete: :nilify_all)
"""
true -> ""
end
do_gen_migration config, "create_whatwasit_version", fn repo, _path, file, name ->
change = """
create table(:versions) do
add :item_type, :string, null: false
add :item_id, :integer, null: false
add :action, :string
add :object, :map, null: false
""" <> whodoneit <> """
timestamps
end
"""
assigns = [mod: Module.concat([repo, Migrations, camelize(name)]),
change: change]
create_file file, migration_template(assigns)
end
end
defp gen_migration(config), do: config
defp do_gen_migration(config, name, fun) do
timestamp = timestamp()
repo = config[:repo]
|> String.split(".")
|> Module.concat
ensure_repo(repo, [])
path = case config[:migration_path] do
path when is_binary(path) -> path
_ ->
Path.relative_to(migrations_path(repo), Mix.Project.app_path)
end
file = Path.join(path, "#{timestamp}_#{underscore(name)}.exs")
fun.(repo, path, file, name)
config
end
defp print_instructions(%{whodoneit_id: type} = config) when type != nil do
Mix.shell.info """
Add the following to your config/config.exs:
config :whatwasit,
repo: #{config[:repo]},
whodoneit_id: :#{type}
""" <> default_model_instructions(config)
config
end
defp print_instructions(%{whodoneit: true, base: base} = config) do
Mix.shell.info """
Add the following to your config/config.exs:
config :whatwasit,
repo: #{config[:repo]}
Update your models so the look like this:
defmodule #{base}.Post do
use #{base}.Web, :model
use Whatwasit # add this
schema "posts" do
field :title, :string
field :body, :string
timestamps
end
def changeset(model, params \\ %{}, opts \\ []) do
model
|> cast(params, ~w(title body))
|> validate_required(~w(title body)a)
|> prepare_version(opts) # add this
end
end
"""
config
end
defp print_instructions(config) do
Mix.shell.info """
Add the following to your config/config.exs:
config :whatwasit,
repo: #{config[:repo]}
""" <> default_model_instructions(config)
config
end
defp default_model_instructions(%{base: base}) do
"""
Update your models like this:
defmodule #{base}.Post do
use #{base}.Web, :model
use Whatwasit # add this
schema "posts" do
field :title, :string
field :body, :string
timestamps
end
def changeset(model, params \\ %{}) do
model
|> cast(params, ~w(title body))
|> validate_required(~w(title body)a)
|> prepare_version # add this
end
end
"""
end
defp timestamp do
{{y, m, d}, {hh, mm, ss}} = :calendar.universal_time()
"#{y}#{pad(m)}#{pad(d)}#{pad(hh)}#{pad(mm)}#{pad(ss)}"
end
embed_template :migration, """
defmodule <%= inspect @mod %> do
use Ecto.Migration
def change do
<%= @change %>
end
end
"""
#############
# Config
defp do_config(opts, bin_opts) do
binding = Mix.Project.config
|> Keyword.fetch!(:app)
|> Atom.to_string
|> Mix.Phoenix.inflect
# IO.puts "binding: #{inspect binding}"
base = opts[:module] || binding[:base]
opts = Keyword.put(opts, :base, base)
repo = (opts[:repo] || "#{base}.Repo")
binding = Keyword.put binding ,:base, base
user_schema = parse_model(opts[:model], base, opts)
whodoneit = if opts[:whodoneit_map] || opts[:whodoneit_id] || opts[:whodoneit_id_type], do: true, else: opts[:whodoneit]
case opts[:whodoneit_id_type] do
false -> :ok
nil -> :ok
"uuid" -> :ok
other ->
IO.puts "other: #{inspect other}"
Mix.raise """
--whodoneit-id-type only supports option uuid
"""
end
bin_opts
|> Enum.map(&({&1, true}))
|> Enum.into(%{})
|> Map.put(:base, base)
|> Map.put(:user_schema, user_schema)
|> Map.put(:repo, repo)
|> Map.put(:binding, binding)
|> Map.put(:migration_path, opts[:migration_path])
|> Map.put(:module, opts[:module])
|> Map.put(:whodoneit, whodoneit)
|> Map.put(:whodoneit_id, opts[:whodoneit_id])
|> Map.put(:whodoneit_map, opts[:whodoneit_map])
|> Map.put(:whodoneit_id_type, opts[:whodoneit_id_type])
|> do_default_config(opts)
end
defp parse_options(opts) do
{opts_bin, opts} = Enum.reduce opts, {[], []}, fn
opt, {acc_bin, acc} ->
{acc_bin, [opt | acc]}
end
opts_bin = Enum.uniq(opts_bin)
opts_names = Enum.map opts, &(elem(&1, 0))
with [] <- Enum.filter(opts_bin, &(not &1 in @switch_names)),
[] <- Enum.filter(opts_names, &(not &1 in @switch_names)) do
{opts_bin, opts}
else
list -> raise_option_errors(list)
end
end
################
# Utilities
defp pad(i) when i < 10, do: << ?0, ?0 + i >>
defp pad(i), do: to_string(i)
defp do_default_config(config, opts) do
list_to_atoms(@default_booleans)
|> Enum.reduce(config, fn opt, acc ->
Map.put acc, opt, Keyword.get(opts, opt, true)
end)
end
defp list_to_atoms(list), do: Enum.map(list, &(String.to_atom(&1)))
defp parse_model(model, _base, opts) when is_binary(model) do
case String.split(model, " ", trim: true) do
[model, table] ->
{prefix_model(model, opts), String.to_atom(table)}
[_] ->
Mix.raise """
The mix whatwasit.install --model option expects both singular and plural names. For example:
mix whatwasit.install --model="Account accounts"
"""
end
end
defp parse_model(_, base, _) do
{"#{base}.User", :users}
end
defp prefix_model(model, opts) do
module = opts[:module] || opts[:base]
if String.starts_with? model, module do
model
else
module <> "." <> model
end
end
defp paths do
[".", :whatwasit]
end
end