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