Skip to main content

lib/sf_voice_media/error.ex

defmodule SfVoiceMedia.Error do
  @moduledoc """
  structured error returned by every SDK function on failure.

  raised as an exception by `SfVoiceMedia.poll_task/3` on timeout or task failure;
  returned inside `{:error, %SfVoiceMedia.Error{}}` tuples by all other functions.

  fields:
  - `code`    — machine-readable error code string from the API (e.g. `"not_found"`)
  - `message` — human-readable description
  - `status`  — http status code, or `nil` when the error is client-side (e.g. poll timeout)
  """

  defexception [:code, :message, :status]

  @type t :: %__MODULE__{
          code: String.t(),
          message: String.t(),
          status: non_neg_integer() | nil
        }

  @impl true
  @doc """
  Format the exception into a human-readable string.

  Formats an `%SfVoiceMedia.Error{}` as `"[<code>] <message> (HTTP <status>)"`.
  """
  @spec message(t()) :: String.t()
  def message(%__MODULE__{code: code, message: msg, status: status}) do
    "[#{code}] #{msg} (HTTP #{status})"
  end

  # ── constructors ─────────────────────────────────────────────────────────────

  @doc """
  Builds an SfVoiceMedia.Error from the API error envelope.

  Parses a response body with the shape `%{"error" => %{"code" => code, "message" => message}}`
  and returns an `%SfVoiceMedia.Error{}` populated with `code`, `message`, and the given HTTP `status`.
  If the body does not match that shape, the function falls back to a generic HTTP error.
  `message: "request failed with status <status>"`, and `status` set to the given status.

  ## Parameters

    - status: HTTP status code returned by the request.
    - body: Parsed response body (map) or `nil`.

  """
  @spec from_response(non_neg_integer(), map() | nil) :: t()
  def from_response(status, %{"error" => %{"code" => code, "message" => msg}}) do
    %__MODULE__{code: code, message: msg, status: status}
  end

  def from_response(status, _body) do
    %__MODULE__{
      code: "http_error",
      message: "request failed with status #{status}",
      status: status
    }
  end

  @doc """
  Create a client-side timeout error for a polling task.

  The returned `%SfVoiceMedia.Error{}` has `code: "poll_timeout"`, a `message` that includes the `task_id` and `timeout_ms`, and `status: nil`.
  """
  @spec poll_timeout(String.t(), non_neg_integer()) :: t()
  def poll_timeout(task_id, timeout_ms) do
    %__MODULE__{
      code: "poll_timeout",
      message: "task #{task_id} did not complete within #{timeout_ms}ms",
      status: nil
    }
  end
end