Packages

The official Elixir SDK for the Nomba One subscription-billing API — recurring billing for Nigeria over card, direct debit, and bank transfer, with dunning that recovers and a ledger that never loses a kobo.

Current section

Files

Jump to
nombaone lib nombaone page.ex
Raw

lib/nombaone/page.ex

defmodule Nombaone.Page do
@moduledoc """
One page of a list result — the items on this page plus everything needed to
keep going.
A `Page` is `Enumerable`: iterating it (with `Enum`, `Stream`, or a `for`
comprehension) walks **every** item across **every** following page, threading
cursors for you.
{:ok, page} = Nombaone.Customers.list(client, %{limit: 50})
# Just this page:
page.data
page.pagination.has_more
# Every customer, cursors handled for you (pages fetched lazily):
for customer <- page do
IO.puts(customer.email)
end
A page fetch that fails mid-iteration raises the `Nombaone` error (streams
cannot carry an `{:error, _}`). Use `next_page/1` when you want to handle that
as a tuple instead.
"""
alias Nombaone.{Client, HTTP, Pagination}
@enforce_keys [:data, :pagination, :request_id]
defstruct [:data, :pagination, :request_id, :__client__, :__spec__, :__caster__]
@type t :: %__MODULE__{
data: [term()],
pagination: Pagination.t(),
request_id: String.t(),
__client__: Client.t(),
__spec__: map(),
__caster__: term()
}
@doc false
@spec new(Client.t(), HTTP.spec(), term(), HTTP.result()) :: t()
def new(client, spec, caster, result) do
items = if is_list(result.data), do: result.data, else: []
%__MODULE__{
data: Enum.map(items, &cast_item(caster, &1)),
pagination: to_pagination(result.pagination, items),
request_id: result.request_id,
__client__: client,
__spec__: spec,
__caster__: caster
}
end
@doc "Whether another page exists beyond this one."
@spec has_next_page?(t()) :: boolean()
def has_next_page?(%__MODULE__{pagination: pagination}) do
pagination.has_more and not is_nil(pagination.next_cursor)
end
@doc """
Fetch the next page — same filters, next cursor. Returns `{:ok, page}` or
`{:error, error}`. Raises `ArgumentError` if there is no next page (check
`has_next_page?/1` first).
"""
@spec next_page(t()) :: {:ok, t()} | {:error, Nombaone.Error.t()}
def next_page(%__MODULE__{pagination: %{next_cursor: nil}}) do
raise ArgumentError,
"no next page available — check has_next_page?/1 before calling next_page/1"
end
def next_page(%__MODULE__{} = page) do
spec = put_cursor(page.__spec__, page.pagination.next_cursor)
case HTTP.request(page.__client__, spec) do
{:ok, result} -> {:ok, new(page.__client__, spec, page.__caster__, result)}
{:error, error} -> {:error, error}
end
end
@doc """
A lazy `Stream` over every item across this and all following pages. Iterating
the page directly does the same thing; this is the explicit form.
"""
@spec stream(t()) :: Enumerable.t()
def stream(%__MODULE__{} = first_page) do
Stream.resource(
fn -> {:emit, first_page} end,
&step/1,
fn _acc -> :ok end
)
end
defp step({:emit, page}), do: {page.data, {:advance, page}}
defp step({:advance, page}) do
if has_next_page?(page) do
case next_page(page) do
{:ok, next} -> {[], {:emit, next}}
{:error, error} -> raise error
end
else
{:halt, :done}
end
end
defp step(:done), do: {:halt, :done}
defp cast_item(nil, item), do: item
defp cast_item(fun, item) when is_function(fun, 1), do: fun.(item)
defp cast_item(module, item) when is_atom(module), do: module.cast(item)
defp to_pagination(%{has_more: has_more} = pagination, _items) do
%Pagination{
limit: Map.get(pagination, :limit, 0),
has_more: has_more,
next_cursor: Map.get(pagination, :next_cursor)
}
end
# A list response that somehow carried no pagination block — treat it as a
# single, terminal page.
defp to_pagination(_pagination, items) do
%Pagination{limit: length(items), has_more: false, next_cursor: nil}
end
defp put_cursor(spec, cursor) do
query = spec |> Map.get(:query) |> Kernel.||(%{}) |> Map.put(:cursor, cursor)
Map.put(spec, :query, query)
end
end
defimpl Enumerable, for: Nombaone.Page do
def reduce(page, acc, fun), do: Enumerable.reduce(Nombaone.Page.stream(page), acc, fun)
# No total count is available without walking every page, so defer to reduce.
def count(_page), do: {:error, __MODULE__}
def member?(_page, _element), do: {:error, __MODULE__}
def slice(_page), do: {:error, __MODULE__}
end