lib/github/error.ex

defmodule GitHub.Error do
  @moduledoc """
  Exception struct used for communicating errors from the client

  > #### Note {:.info}
  >
  > Functions in this module is unlikely to be used directly by applications. Instead, they are
  > useful for plugins. See `GitHub.Plugin` for more information.

  This error covers errors generated by the client (for example, HTTP connection errors) as well as
  errors returned from the GitHub API (for example, Not Found errors).

  ## Fields

    * `code` (integer): Status code of the API response, if a response was received.

    * `message` (string): Human-readable message describing the error. Defaults to a generic
      `"Unknown Error"`.

    * `operation` (`t:Operation.t/0`): Operation at the time of the error.

    * `reason` (atom): Easily-matched atom for common errors, like `:not_found`. Defaults to a
      generic `:error`. See **Error Reasons** below for a list of possible values.

    * `source` (term): Cause of the error. This could be an operation, an API error response, or
      something else.

    * `stacktrace` (`t:Exception.stacktrace/0`): Stacktrace from the time of the error. Defaults
      to a stack that terminates with the calling function, but can be overridden (for example,
      if a `__STACKTRACE__` is available in a `rescue` block).

    * `step` (plugin): Plugin active at the time of the error (expressed as a tuple containing the
      module and function).

  Users of the library can match on the information in the `code` and `source` fields to extract
  additional information.

  ## Error Reasons

  Although plugins may use any atom for the `reason` field, the following have predetermined
  meanings:

    * `:invalid_auth`: The credential (`:auth` option) provided is invalid.

    * `:invalid_version`: The version (`:version` option) provided is invalid.

    * `:not_found`: A resource or route was not found. Note that GitHub may return this kind of
      response when authentication is required to see a resource.

    * `:oauth_restricted`: The OAuth credentials are valid, but the requested resource is owned
      by an organization that requires admin approval for OAuth apps.

    * `:rate_limited`: The client has exceeded a primary or secondary rate limit. Secondary rate
      limits have a distinct `message` field with further information.

    * `:requires_auth`: The requested endpoint requires an authenticated user, and no auth
      credentials were given.

    * `:unauthorized`: Valid authentication credentials were given, but the current user does not
      have permission to perform this action.

  """

  alias GitHub.Operation

  @typedoc "GitHub API client error"
  @type t :: %__MODULE__{
          code: integer | nil,
          message: String.t(),
          operation: Operation.t(),
          reason: atom,
          source: term,
          stacktrace: Exception.stacktrace(),
          step: {module, atom}
        }

  @derive {Inspect, except: [:operation, :stacktrace]}
  defexception [:code, :message, :operation, :reason, :source, :stacktrace, :step]

  @doc """
  Create a new error struct with the given fields

  The current stacktrace is automatically filled in to the resulting error. Callers should specify
  the status `code` (if available), a `message`, the original `operation`, the `source` of the
  error, and which `step` or plugin is currently active.
  """
  @spec new(keyword) :: t
  def new(opts) do
    {:current_stacktrace, stack} = Process.info(self(), :current_stacktrace)
    # Drop `Process.info/2` and `new/1`.
    stacktrace = Enum.drop(stack, 2)

    fields =
      Keyword.merge(
        [
          message: "Unknown Error",
          reason: :error,
          stacktrace: stacktrace
        ],
        opts
      )

    struct!(%__MODULE__{}, fields)
  end
end