Skip to main content

lib/dust_ecto/error.ex

defmodule DustEcto.Error do
  @moduledoc """
  Single error struct returned from `DustEcto.Repo` when transport
  failures or server errors occur. Validation errors return
  `{:error, %Ecto.Changeset{}}` instead — that path is unchanged.

  ## Kinds

    * `:network` — Req call failed before reaching the server (connection
      refused, DNS, TLS, etc.). `retryable?` is `true`.
    * `:http` — server replied with a non-2xx, non-recognized status.
      `detail` carries the status + body. Usually retryable on 5xx,
      not on 4xx.
    * `:conflict` — `If-Match` precondition failed. `detail` carries
      `current_revision` and (for batch_write) `op_index` + `path`.
    * `:not_supported` — feature unavailable on the active transport
      (e.g. `subscribe` in HTTP mode). Not retryable.
    * `:nothing_to_write` — `Repo.insert`/`update` had no fields to
      send. Not retryable; usually a bug in the caller's changeset.
    * `:timeout` — sync write didn't get an ack within the configured
      window. The write may still eventually succeed; do not retry
      blindly.
    * `:unauthorized` — token rejected by the server.
    * `:invalid_params` — server rejected the request shape.
    * `:rate_limited` — server returned 429. `detail` may include
      `Retry-After`.
    * `:not_implemented` — server returned a whole-route 404 (the
      method/path isn't registered). Usually means the deployed Dust
      server is older than the SDK expects. Distinct from
      `:not_found`, which is an *entity* miss inside a working route.
  """

  @type kind ::
          :network
          | :http
          | :conflict
          | :not_supported
          | :nothing_to_write
          | :timeout
          | :unauthorized
          | :invalid_params
          | :rate_limited
          | :not_implemented

  @type t :: %__MODULE__{
          kind: kind(),
          detail: term(),
          retryable?: boolean()
        }

  defstruct [:kind, :detail, retryable?: false]

  @doc "Construct an error of the given kind."
  @spec new(kind(), term(), keyword()) :: t()
  def new(kind, detail \\ nil, opts \\ []) do
    %__MODULE__{
      kind: kind,
      detail: detail,
      retryable?: Keyword.get(opts, :retryable?, default_retryable?(kind))
    }
  end

  defp default_retryable?(:network), do: true
  defp default_retryable?(:rate_limited), do: true
  defp default_retryable?(:timeout), do: false
  defp default_retryable?(_), do: false
end