lib/csrf_plus/exception.ex

defmodule CsrfPlus.Exception do
  @moduledoc """
  CsrfPlus exceptions.
  """

  defexception [:message]

  defmodule HeaderException do
    defexception [:message]
  end

  defmodule MismatchException do
    defexception [:message]
  end

  defmodule SessionException do
    defexception [:message]
  end

  defmodule StoreException do
    defexception [:message]
  end

  @doc """
  Creates a new CsrfPlus exception based on the given params during the `Kernel.raise/2` call.

  Passing no param will create a `CsrfPlus.Exception`.

  If a string is passed it will be used as the exception message.

  If it's an exception module name (an atom) an exception will be created from it with the default message set in the
  `exceptions/0` function.

  When a tuple is passed in the format `{which, type}` an exception with name `which` will be created having the message set in the
  `exception/1` function with the message in `type`.

  ## Examples

       iex> raise CsrfPlus.Exception
       ** (CsrfPlus.Exception) invalid token

       iex> raise CsrfPlus.Exception, "custom message"
       ** (CsrfPlus.Exception) custom message

       iex> raise CsrfPlus.Exception, CsrfPlus.Exception.StoreException
       ** (CsrfPlus.Exception.StoreException) no store is set

       iex> raise CsrfPlus.Exception, {CsrfPlus.Exception.StoreException, :token_not_found}
       ** (CsrfPlus.Exception.StoreException) token was not found

  """

  @impl Exception
  def exception([]) do
    map_exception(__MODULE__)
  end

  @impl Exception
  def exception({which, type}) when is_atom(type) do
    map_exception(which, type)
  end

  @impl Exception
  def exception(which) when is_atom(which) do
    map_exception(which)
  end

  @impl Exception
  def exception(message) when is_binary(message) do
    map_exception_message(__MODULE__, message)
  end

  @doc """
  Checks if the given exception is a CsrfPlus exception.
  Returns a boolean true if the given exception is a CsrfPlus.
  """
  def csrf_plus_exception?(%{__exception__: true, __struct__: exception}) do
    csrf_plus_exception?(exception)
  end

  def csrf_plus_exception?(exception) when is_atom(exception) do
    Map.has_key?(exceptions(), exception)
  end

  @doc "Retrieve all the CsrfPlus exceptions with their corresponding names and messages"
  def exceptions do
    %{
      "#{__MODULE__}.HeaderException":
        {HeaderException, [default: "missing token in the requrest header"]},
      "#{__MODULE__}.MismatchException": {MismatchException, [default: "tokens mismatch"]},
      "#{__MODULE__}.SessionException":
        {SessionException,
         [default: "missing token in the session", missing_id: "missing id in the session"]},
      "#{__MODULE__}.StoreException":
        {StoreException,
         [
           default: "no store is set",
           token_not_found: "token was not found",
           token_expired: "token has expired"
         ]},
      "#{__MODULE__}": {__MODULE__, [default: "invalid token"]}
    }
  end

  defp map_exception(which, type \\ nil) when is_atom(type) do
    {exception, message} = get_exception(which, type)

    map_exception_message(exception, message)
  end

  defp map_exception_message(exception, message) do
    exception
    |> Map.from_struct()
    |> Map.put(:message, message)
    |> Map.put(:__struct__, exception)
  end

  defp get_exception(which, type) do
    {exception, messages} = Map.get(exceptions(), which)

    message =
      if type != nil && Keyword.has_key?(messages, type) do
        Keyword.get(messages, type)
      else
        Keyword.get(messages, :default, "unknown exception")
      end

    {exception, message}
  end
end