Skip to main content

lib/kagi/error.ex

defmodule Kagi.Error do
  @moduledoc """
  Structured error returned by every Kagi operation.

  `Kagi.Error` is both a struct and an exception. Non-bang functions return it
  in `{:error, error}`; bang functions raise it.

  The `:reason` atom is stable for pattern matching; the `:message` string is
  diagnostic text and may change between releases.

  ## Reasons

    * `:missing_session_token` - no valid session token is configured.
    * `:invalid_option` - an option failed client-side validation.
    * `:request_failed` - the HTTP request failed before receiving a response.
    * `:unauthorized` - Kagi returned HTTP 401 or 403.
    * `:rate_limited` - Kagi returned HTTP 429.
    * `:http_error` - Kagi returned an unexpected non-2xx status.
    * `:blocked` - Kagi returned a CAPTCHA or challenge page.
    * `:parse_error` - a response could not be parsed.
  """

  @typedoc "Stable, machine-readable reason returned in `%Kagi.Error{}`."
  @type reason ::
          :missing_session_token
          | :invalid_option
          | :request_failed
          | :unauthorized
          | :rate_limited
          | :http_error
          | :blocked
          | :parse_error

  @typedoc "A `Kagi.Error` value."
  @type t :: %__MODULE__{reason: reason(), message: String.t()}

  defexception [:reason, :message]

  @doc """
  Builds a `Kagi.Error` with a stable reason and diagnostic message.

  Application code usually receives these values from `Kagi` calls rather than
  constructing them directly.

  ## Examples

      Kagi.Error.new(:invalid_option, "limit must be a non-negative integer")
      #=> %Kagi.Error{reason: :invalid_option, message: "limit must be a non-negative integer"}
  """
  @spec new(reason(), String.t()) :: t()
  def new(reason, message) when is_atom(reason) and is_binary(message) do
    %__MODULE__{reason: reason, message: message}
  end
end