GhEx.REST (gh_ex v0.3.2)

Copy Markdown View Source

Generic REST access. Any GitHub REST path is reachable through get/3, post/3, patch/3, put/3, and delete/3.

Every call returns {:ok, body, meta} on a 2xx or {:error, reason} otherwise, where reason is a GhEx.Error for an API error response or a Req exception for a transport failure. meta is a GhEx.REST.Meta struct carrying the status, response headers, parsed pagination links, and rate-limit snapshot.

Options

Per-call options are passed through to Req, so the useful ones are:

  • :params - query string parameters, e.g. params: [state: "open", per_page: 100]
  • :json - a body to JSON-encode for post/patch/put

Examples

GhEx.REST.get(client, "/repos/elixir-lang/elixir")
GhEx.REST.get(client, "/repos/o/r/issues", params: [state: "open"])
GhEx.REST.post(client, "/repos/o/r/issues", json: %{title: "Bug", body: "..."})

Conditional requests

Each successful response's meta carries the :etag and :last_modified headers. Store one and send it back to make the next request conditional:

{:ok, body, meta} = GhEx.REST.get(client, "/repos/o/r/issues")

case GhEx.REST.get(client, "/repos/o/r/issues",
       headers: [{"if-none-match", meta.etag}]) do
  {:ok, :not_modified, meta} -> # unchanged; reuse the cached body
  {:ok, body, meta} -> # changed; meta.etag is the new validator
end

A 304 Not Modified comes back as {:ok, :not_modified, meta}, distinct from both a normal {:ok, body, meta} and an {:error, _}. GitHub does not charge a 304 against the primary rate limit, so polling with the stored validator is cheaper than refetching. Storing the body is the caller's job; this is opt-in ergonomics, not a response cache.

Telemetry

Every REST call (and each stream/3 page) is wrapped in :telemetry.span/3, emitting:

  • [:gh_ex, :request, :start] - measurements %{system_time, monotonic_time}, metadata %{method, path}.
  • [:gh_ex, :request, :stop] - measurements %{duration, monotonic_time}, metadata %{method, path, result} plus, on success, status and the rate_limit snapshot, or, on a normalized API/transport failure, error (the {:error, reason} is reported here, not as :exception).
  • [:gh_ex, :request, :exception] - emitted only if the call raises unexpectedly; measurements %{duration, monotonic_time}, metadata %{method, path, kind, reason, stacktrace}.

Read rate_limit.remaining off the :stop metadata to wire rate-limit headroom into a metrics pipeline.

Summary

Functions

Runs a request and returns the raw Req.Response, without normalizing it.

Auto-paginates a list endpoint into a lazy Stream of individual items.

Types

meta()

@type meta() :: GhEx.REST.Meta.t()

result()

@type result() :: {:ok, term(), meta()} | {:error, GhEx.Error.t() | Exception.t()}

Functions

delete(client, path, opts \\ [])

@spec delete(GhEx.Client.t(), String.t(), keyword()) :: result()

Issues a DELETE.

get(client, path, opts \\ [])

@spec get(GhEx.Client.t(), String.t(), keyword()) :: result()

Issues a GET.

patch(client, path, opts \\ [])

@spec patch(GhEx.Client.t(), String.t(), keyword()) :: result()

Issues a PATCH.

post(client, path, opts \\ [])

@spec post(GhEx.Client.t(), String.t(), keyword()) :: result()

Issues a POST.

put(client, path, opts \\ [])

@spec put(GhEx.Client.t(), String.t(), keyword()) :: result()

Issues a PUT.

raw(client, method, path, opts \\ [])

@spec raw(GhEx.Client.t(), atom(), String.t(), keyword()) ::
  {:ok, Req.Response.t()} | {:error, term()}

Runs a request and returns the raw Req.Response, without normalizing it.

Unlike the verb functions, a non-2xx response comes back as {:ok, %Req.Response{}} (inspect resp.status yourself) rather than {:error, _}. Useful when a 404 means "absent" rather than an error, or when you need the raw status, headers, or body, or custom decoding. Auth resolution can still fail, returning {:error, reason}. Compose with GhEx.Pagination.links/1 and GhEx.RateLimit.from_response/1 for links or a rate-limit snapshot.

stream(client, path, opts \\ [])

@spec stream(GhEx.Client.t(), String.t(), keyword()) :: Enumerable.t()

Auto-paginates a list endpoint into a lazy Stream of individual items.

Follows the Link: rel="next" header until GitHub stops handing one back. The first page's :params are applied once; subsequent pages use the exact next URL GitHub returns, so cursors are never double-applied. A failed page raises GhEx.Error.

A next URL whose origin (scheme, host, port) differs from the client's rest_url is refused with GhEx.Error rather than followed, so the bearer is never re-applied to an unauthorized host. A non-default GitHub Enterprise rest_url paginates normally, since its pages stay on the same origin.

per_page defaults to 100 for the first page when the caller does not set it, so a stream pages at GitHub's maximum rather than its default of 30, cutting request count and rate-limit burn for large collections. Subsequent pages follow the next URL verbatim, which already carries the page size.

Options

  • :items - the key under which an object-wrapped response holds its array, for the endpoints that return %{"total_count" => _, "items" => [...]} rather than a bare array (Search "items", Actions runs "workflow_runs", Checks "check_runs", and so on). When set, each page's items are taken from body[items] and flattened. Omit it for plain-array endpoints, which paginate unchanged. Any other option is forwarded to Req for the first page.

    client |> GhEx.REST.stream("/repos/elixir-lang/elixir/issues", params: [state: "all", per_page: 100]) |> Stream.map(& &1["number"]) |> Enum.take(250)