lib/kevo/api/error.ex

defmodule Kevo.ApiError do
  @moduledoc """
  Represents a failed response from Kevo's HTTP/2 API.
  """

  defexception [
    # :network_error, :unexpected_status, :unexpected_body
    :reason,
    # map of request parameters
    :request,
    # response received
    :response,
    # some expected value, status or header
    :expected,
    # root error given
    :caused_by,
    # interstitial step if applicable
    :step
  ]

  @type t :: %Kevo.ApiError{
          reason: :network_error | :unexpected_status | :unexpected_body,
          request: map(),
          response: any(),
          expected: integer() | list({String.t(), String.t()}),
          caused_by: any(),
          step: {function :: String.t(), arity :: integer()}
        }

  # network error
  def message(%__MODULE__{reason: :network_error} = err) do
    step_prefix(err.step) <> "network error: #{inspect(err.caused_by)}"
  end

  # unexpected status
  def message(%__MODULE__{reason: :unexpected_status} = err) do
    {:response, _, status, _} = err.response

    step_prefix(err.step) <> "expected response status #{err.expected}, got: #{status}"
  end

  # unexpected body
  def message(%__MODULE__{reason: :unexpected_body} = err) do
    step_prefix(err.step) <> "invalid json response: #{Jason.DecodeError.message(err.caused_by)}"
  end

  # missing header
  def message(%__MODULE__{reason: :missing_header} = err) do
    {:response, _, _, headers} = err.response

    headers = Enum.map_join(headers, ", ", fn {header, _} -> header end)

    step_prefix(err.step) <> "expected header `#{err.expected}` not present in: [#{headers}]"
  end

  def from_network(request, error, step \\ nil) do
    {:error,
     %__MODULE__{
       reason: :network_error,
       request: request,
       caused_by: error,
       step: step
     }}
  end

  def from_body(request, %Jason.DecodeError{} = error, step \\ nil) do
    {:error,
     %__MODULE__{
       reason: :unexpected_body,
       request: request,
       caused_by: error,
       step: step
     }}
  end

  def from_status(request, response, expected, step \\ nil) do
    {:error,
     %__MODULE__{
       reason: :unexpected_status,
       request: request,
       response: response,
       expected: expected,
       step: step
     }}
  end

  def from_headers(request, response, expected, step \\ nil) do
    {:error,
     %__MODULE__{
       reason: :missing_header,
       request: request,
       response: response,
       expected: expected,
       step: step
     }}
  end

  defp step_prefix(nil), do: ""
  defp step_prefix({name, arity}), do: "#{name}/#{arity}: "
end