lib/boruta/oauth/error.ex

defmodule Boruta.Oauth.Error do
  @moduledoc """
  Boruta OAuth errors

  > __Note__: Intended to follow [OAuth 2.0 errors](https://tools.ietf.org/html/rfc6749#section-5.2). Additionnal errors are provided as purpose.
  """

  alias Boruta.Oauth.CodeRequest
  alias Boruta.Oauth.Error
  alias Boruta.Oauth.HybridRequest
  alias Boruta.Oauth.TokenRequest

  @type t :: %__MODULE__{
          status: :internal_server_error | :bad_request | :unauthorized,
          error:
            :invalid_request
            | :invalid_client
            | :invalid_scope
            | :invalid_code
            | :invalid_resource_owner
            | :unknown_error,
          error_description: String.t(),
          format: :query | :fragment | nil,
          redirect_uri: String.t() | nil,
          state: String.t() | nil
        }

  @enforce_keys [:status, :error, :error_description]
  defstruct status: nil,
            error: nil,
            error_description: nil,
            format: nil,
            redirect_uri: nil,
            state: nil

  @doc """
  Returns the OAuth error augmented with the format according to request type.
  """
  @spec with_format(
          error :: Error.t(),
          request :: CodeRequest.t() | TokenRequest.t() | HybridRequest.t()
        ) :: Error.t()
  def with_format(%Error{error: :invalid_client} = error, _) do
    %{error | format: nil, redirect_uri: nil}
  end

  def with_format(%Error{} = error, %CodeRequest{redirect_uri: redirect_uri, state: state}) do
    %{error | format: :query, redirect_uri: redirect_uri, state: state}
  end

  def with_format(%Error{} = error, %HybridRequest{redirect_uri: redirect_uri, state: state}) do
    %{error | redirect_uri: redirect_uri, state: state}
  end

  def with_format(%Error{} = error, %TokenRequest{redirect_uri: redirect_uri, state: state}) do
    %{error | format: :fragment, redirect_uri: redirect_uri, state: state}
  end

  def with_format(error, _), do: error

  @doc """
  Returns the URL to be redirected according to error format.
  """
  @spec redirect_to_url(error :: t()) :: url :: String.t()
  def redirect_to_url(%__MODULE__{format: nil}), do: ""

  def redirect_to_url(%__MODULE__{} = error) do
    query_params = query_params(error)

    url(error, query_params)
  end

  defp query_params(%__MODULE__{
         error: error,
         error_description: error_description,
         state: state
       }) do
    %{error: error, error_description: error_description, state: state}
    |> Enum.filter(fn
      {_key, nil} -> false
      _ -> true
    end)
    |> URI.encode_query()
  end

  defp url(%Error{redirect_uri: redirect_uri, format: :query}, query_params),
    do: "#{redirect_uri}?#{query_params}"

  defp url(%Error{redirect_uri: redirect_uri, format: :fragment}, query_params),
    do: "#{redirect_uri}##{query_params}"
end