lib/swoosh/email/recipient.ex

defprotocol Swoosh.Email.Recipient do
  @moduledoc """
  Recipient Protocol controls how data is formatted into an email recipient

  ## Deriving

  The protocol allows leveraging the Elixir's `@derive` feature to simplify protocol implementation
  in trivial cases. Accepted options are:

    * `:name` (optional)
    * `:address` (required)

  ## Example

      defmodule MyUser do
        @derive {Swoosh.Email.Recipient, name: :name, address: :email}
        defstruct [:name, :email, :other_props]
      end

  or with optional name...

      defmodule MySubscriber do
        @derive {Swoosh.Email.Recipient, address: :email}
        defstruct [:email, :preferences]
      end

  full implementation without deriving...

      defmodule MyUser do
        defstruct [:name, :email, :other_props]
      end

      defimpl Swoosh.Email.Recipient, for: MyUser do
        def format(%MyUser{name: name, email: address} = value) do
          {name, address}
        end
      end
  """

  @type t :: term
  @fallback_to_any true

  @doc """
  Formats `value` into a Swoosh recipient, a 2-tuple with recipient name and recipient address
  """
  @spec format(t) :: Swoosh.Email.mailbox()
  def format(value)
end

defimpl Swoosh.Email.Recipient, for: Any do
  defmacro __deriving__(module, struct, opts) do
    name_field = Keyword.get(opts, :name)
    address_field = Keyword.fetch!(opts, :address)
    keys = Map.keys(struct)

    fields =
      [{:name, name_field}, {:address, address_field}]
      |> Enum.reject(fn {_, field} -> is_nil(field) end)
      |> Enum.map(fn {var, field} ->
        unless field in keys do
          raise ArgumentError, "#{inspect(field)} does not exist in #{inspect(struct)}"
        end

        {field, {var, [generated: true], __MODULE__}}
      end)

    quote do
      defimpl Swoosh.Email.Recipient, for: unquote(module) do
        def format(%{unquote_splicing(fields)}) do
          {unquote(if(name_field, do: Macro.var(:name, __MODULE__), else: "")), address}
        end
      end
    end
  end

  def format(data) do
    raise Protocol.UndefinedError,
      protocol: @protocol,
      value: data,
      description: """
      Swoosh.Email.Recipient needs to be implemented for #{inspect(data)}

      Default implementations of Recipient include
      * a string representing an email address like `foo.bar@example.com`
      * or a two-element tuple `{name, address}`, where name and address are strings.
        - name is allowed to be nil in this case
      """
  end
end

defimpl Swoosh.Email.Recipient, for: Tuple do
  def format({name, address}) when name in [nil, ""] and is_binary(address) and address != "" do
    {"", address}
  end

  def format({name, address}) when is_binary(name) and is_binary(address) and address != "" do
    {name, address}
  end

  def format(tuple) do
    raise ArgumentError, """
    Unexpected tuple format, #{inspect(tuple)} cannot be formatted into a Recipeint.

    The expected format is {name :: String.t() | nil, address :: String.t()}, where address cannot be empty.
    """
  end
end

defimpl Swoosh.Email.Recipient, for: BitString do
  def format("") do
    raise ArgumentError, """
    Cannot format empty string into a Recipeint.
    """
  end

  def format(address) when is_binary(address) do
    {"", address}
  end
end