defmodule Mix.Tasks.Phx.Gen.Notifier do
@shortdoc "Generates a notifier that delivers emails by default"
@moduledoc """
Generates a notifier that delivers emails by default.
$ mix phx.gen.notifier Accounts User welcome_user reset_password confirmation_instructions
This task expects a context module name, followed by a
notifier name and one or more message names. Messages
are the functions that will be created prefixed by "deliver",
so the message name should be "snake_case" without punctuation.
Additionally a context app can be specified with the flag
`--context-app`, which is useful if the notifier is being
generated in a different app under an umbrella.
$ mix phx.gen.notifier Accounts User welcome_user --context-app marketing
The app "marketing" must exist before the command is executed.
"""
use Mix.Task
@switches [
context: :boolean,
context_app: :string,
prefix: :string
]
@default_opts [context: true]
alias Mix.Phoenix.Context
@doc false
def run(args) do
if Mix.Project.umbrella?() do
Mix.raise(
"mix phx.gen.notifier must be invoked from within your *_web application root directory"
)
end
{context, notifier_module, messages} = build(args)
inflections = Mix.Phoenix.inflect(notifier_module)
binding = [
context: context,
inflections: inflections,
notifier_messages: messages
]
paths = Mix.Phoenix.generator_paths()
prompt_for_conflicts(context)
if "--no-compile" not in args do
Mix.Task.run("compile")
end
context
|> copy_new_files(binding, paths)
|> maybe_print_mailer_installation_instructions()
end
@doc false
def build(args, help \\ __MODULE__) do
{opts, parsed, _} = parse_opts(args)
[context_name, notifier_name | notifier_messages] = validate_args!(parsed, help)
notifier_module = inspect(Module.concat(context_name, "#{notifier_name}Notifier"))
context = Context.new(notifier_module, opts)
{context, notifier_module, notifier_messages}
end
defp parse_opts(args) do
{opts, parsed, invalid} = OptionParser.parse(args, switches: @switches)
merged_opts =
@default_opts
|> Keyword.merge(opts)
|> put_context_app(opts[:context_app])
{merged_opts, parsed, invalid}
end
defp put_context_app(opts, nil), do: opts
defp put_context_app(opts, string) do
Keyword.put(opts, :context_app, String.to_atom(string))
end
defp validate_args!([context, notifier | messages] = args, help) do
cond do
not Context.valid?(context) ->
help.raise_with_help(
"Expected the context, #{inspect(context)}, to be a valid module name"
)
not valid_notifier?(notifier) ->
help.raise_with_help(
"Expected the notifier, #{inspect(notifier)}, to be a valid module name"
)
context == Mix.Phoenix.base() ->
help.raise_with_help(
"Cannot generate context #{context} because it has the same name as the application"
)
notifier == Mix.Phoenix.base() ->
help.raise_with_help(
"Cannot generate notifier #{notifier} because it has the same name as the application"
)
Enum.any?(messages, &(!valid_message?(&1))) ->
help.raise_with_help(
"Cannot generate notifier #{inspect(notifier)} because one of the messages is invalid: #{Enum.map_join(messages, ", ", &inspect/1)}"
)
true ->
args
end
end
defp validate_args!(_, help) do
help.raise_with_help("Invalid arguments")
end
defp valid_notifier?(notifier) do
notifier =~ ~r/^[A-Z]\w*(\.[A-Z]\w*)*$/
end
defp valid_message?(message_name) do
message_name =~ ~r/^[a-z]+(\_[a-z0-9]+)*$/
end
@doc false
@spec raise_with_help(String.t()) :: no_return()
def raise_with_help(msg) do
Mix.raise("""
#{msg}
mix phx.gen.notifier expects a context module name, followed by a
notifier name and one or more message names. Messages are the
functions that will be created prefixed by "deliver", so the message
name should be "snake_case" without punctuation.
For example:
mix phx.gen.notifier Accounts User welcome reset_password
In this example the notifier will be called `UserNotifier` inside
the Accounts context. The functions `deliver_welcome/1` and
`reset_password/1` will be created inside this notifier.
""")
end
defp copy_new_files(%Context{} = context, binding, paths) do
files = files_to_be_generated(context)
Mix.Phoenix.copy_from(paths, "priv/templates/phx.gen.notifier", binding, files)
context
end
defp files_to_be_generated(%Context{} = context) do
[
{:eex, "notifier.ex", context.file},
{:eex, "notifier_test.exs", context.test_file}
]
end
defp prompt_for_conflicts(context) do
context
|> files_to_be_generated()
|> Mix.Phoenix.prompt_for_conflicts()
end
@doc false
@spec maybe_print_mailer_installation_instructions(%Context{}) :: %Context{}
def maybe_print_mailer_installation_instructions(%Context{} = context) do
mailer_module = Module.concat([context.base_module, "Mailer"])
unless Code.ensure_loaded?(mailer_module) do
Mix.shell().info("""
Unable to find the "#{inspect(mailer_module)}" module defined.
A mailer module like the following is expected to be defined
in your application in order to send emails.
defmodule #{inspect(mailer_module)} do
use Swoosh.Mailer, otp_app: #{inspect(context.context_app)}
end
It is also necessary to add "swoosh" as a dependency in your
"mix.exs" file:
def deps do
[{:swoosh, "~> 1.4"}]
end
Finally, an adapter needs to be set in your configuration:
import Config
config #{inspect(context.context_app)}, #{inspect(mailer_module)}, adapter: Swoosh.Adapters.Local
Check https://hexdocs.pm/swoosh for more details.
""")
end
context
end
end