Class: Dinie::Page
- Inherits:
-
Object
- Object
- Dinie::Page
- Defined in:
- lib/dinie/runtime/paginator.rb
Overview
Items must respond to ‘#id` — the next page’s ‘starting_after` cursor is the `id` of the last item on the current page. Every list item in the frozen surface satisfies this.
‘Page` — one page of a cursor-paginated list, and the auto-pagination engine over the pages that follow it (architecture §11, RB8). Ports `sdk-js` `src/runtime/paginator.ts` and mirrors the mechanics of OpenAI Ruby’s ‘base_page.rb`. Part of the **public surface** (`Dinie::Page`).
Dinie has exactly ONE cursor scheme (inherited contract): ‘starting_after` (the `id` of the last item of the previous page) + `has_more` (the ONLY end-of-list signal — never `data.length == limit`, since the server may return fewer items with `has_more: true`).
A resource’s ‘list*` does the first request eagerly and returns a `Page` carrying the first page’s ‘data`, its `has_more`, and a `fetch_page` closure that knows how to fetch the next page (it maps a cursor to the `starting_after` query param and calls the transport). The `Page` owns when to call the closure and what cursor to pass; the resource owns how the closure reaches the network — so this class stays pure and is tested with an in-memory fake (no `HttpClient` coupling).
── Why NOT ‘include Enumerable` (RB8) ──`Enumerable` would graft on methods that materialize EVERY page eagerly (`to_a`, `count`, `sort`, `min`…). A paginator must stay lazy, so we deliberately define only `#each` (+ its `#auto_paging_each` alias) and `#each_page`; calling either without a block returns an `Enumerator`, which gives `.map` / `.first` / `.lazy` on demand without eager-loading.
── Iteration modes ──
* `#each` / `#auto_paging_each` — yield EVERY item of EVERY following page (auto-paginate).
* `#each_page` — yield page-by-page (manual control via `#next_page?` / `#next_page`).
* `#first(n)` — the first `n` items across pages, fetched lazily (stops as soon as it has `n`).
Constant Summary collapse
- NO_NEXT_PAGE_MESSAGE =
Message raised by #next_page when there is no next page to fetch.
"No next page; guard with `#next_page?` (or check `#has_more`) first."
Instance Attribute Summary collapse
-
#data ⇒ Array<Object>
readonly
Items on this page — already deserialized into typed POROs by the resource’s ‘fetch_page`.
-
#has_more ⇒ Boolean
readonly
Whether the API reports more pages after this one — the ONLY end-of-list signal.
Class Method Summary collapse
-
.build(envelope, fetch_page) ⇒ Dinie::Page
private
Wrap a fetched ‘{ data:, has_more: }` envelope (already item-deserialized) in a `Page`.
-
.from_fetch(fetch_page) ⇒ Dinie::Page
Build the FIRST page by invoking ‘fetch_page` with no cursor (so the closure uses the caller’s ‘starting_after`, if any).
-
.paginated?(envelope) ⇒ Boolean
The deterministic discriminator (architecture §7.5/§11): a list endpoint is paginated iff its envelope carries ‘has_more`.
Instance Method Summary collapse
-
#each {|item| ... } ⇒ self, Enumerator
(also: #auto_paging_each)
Auto-paginate: yield every item of this page, then every item of each following page, until ‘has_more` is false.
-
#each_page {|page| ... } ⇒ self, Enumerator
Yield page-by-page (manual control), following the cursor until ‘has_more` is false.
-
#first(count = UNSPECIFIED) ⇒ Object+
The first ‘count` items across pages (or the very first item when `count` is omitted), fetched lazily — only as many pages as needed are requested.
-
#initialize(data:, has_more:, fetch_page:) ⇒ Page
constructor
A new instance of Page.
-
#next_page ⇒ Dinie::Page
Fetch the next page.
-
#next_page? ⇒ Boolean
Whether a next page can be fetched.
Constructor Details
#initialize(data:, has_more:, fetch_page:) ⇒ Page
Returns a new instance of Page.
85 86 87 88 89 90 |
# File 'lib/dinie/runtime/paginator.rb', line 85 def initialize(data:, has_more:, fetch_page:) @data = data @has_more = has_more @fetch_page = fetch_page freeze end |
Instance Attribute Details
#data ⇒ Array<Object> (readonly)
Items on this page — already deserialized into typed POROs by the resource’s ‘fetch_page`.
42 43 44 |
# File 'lib/dinie/runtime/paginator.rb', line 42 def data @data end |
#has_more ⇒ Boolean (readonly)
Whether the API reports more pages after this one — the ONLY end-of-list signal.
45 46 47 |
# File 'lib/dinie/runtime/paginator.rb', line 45 def has_more @has_more end |
Class Method Details
.build(envelope, fetch_page) ⇒ Dinie::Page
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Wrap a fetched ‘{ data:, has_more: }` envelope (already item-deserialized) in a `Page`.
67 68 69 |
# File 'lib/dinie/runtime/paginator.rb', line 67 def build(envelope, fetch_page) new(data: envelope[:data] || [], has_more: envelope[:has_more] == true, fetch_page: fetch_page) end |
.from_fetch(fetch_page) ⇒ Dinie::Page
Build the FIRST page by invoking ‘fetch_page` with no cursor (so the closure uses the caller’s ‘starting_after`, if any). This is the entry point a resource’s ‘list*` calls.
57 58 59 |
# File 'lib/dinie/runtime/paginator.rb', line 57 def from_fetch(fetch_page) build(fetch_page.call(nil), fetch_page) end |
.paginated?(envelope) ⇒ Boolean
The deterministic discriminator (architecture §7.5/§11): a list endpoint is paginated iff its envelope carries ‘has_more`. `/banks` lacks it, so the platform resource (story 010) returns a flat `Array<Bank>` instead of a `Page`. Exposed here so that rule has one home.
77 78 79 |
# File 'lib/dinie/runtime/paginator.rb', line 77 def paginated?(envelope) envelope.is_a?(Hash) && envelope.key?(:has_more) end |
Instance Method Details
#each {|item| ... } ⇒ self, Enumerator Also known as: auto_paging_each
Auto-paginate: yield every item of this page, then every item of each following page, until ‘has_more` is false. Without a block, returns an `Enumerator` (`.map` / `.first` / `.lazy`).
117 118 119 120 121 122 123 124 125 126 127 128 |
# File 'lib/dinie/runtime/paginator.rb', line 117 def each(&block) return enum_for(:each) unless block_given? page = self loop do page.data.each(&block) break unless page.next_page? page = page.next_page end self end |
#each_page {|page| ... } ⇒ self, Enumerator
Yield page-by-page (manual control), following the cursor until ‘has_more` is false. Without a block, returns an `Enumerator` over the pages.
136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/dinie/runtime/paginator.rb', line 136 def each_page return enum_for(:each_page) unless block_given? page = self loop do yield page break unless page.next_page? page = page.next_page end self end |
#first(count = UNSPECIFIED) ⇒ Object+
The first ‘count` items across pages (or the very first item when `count` is omitted), fetched lazily — only as many pages as needed are requested.
154 155 156 157 158 |
# File 'lib/dinie/runtime/paginator.rb', line 154 def first(count = UNSPECIFIED) return auto_paging_each.first if count.equal?(UNSPECIFIED) auto_paging_each.first(count) end |
#next_page ⇒ Dinie::Page
Fetch the next page. The cursor is the ‘id` of the LAST item on this page (mapped to `starting_after` by the closure). Guard with #next_page? before calling.
106 107 108 109 110 |
# File 'lib/dinie/runtime/paginator.rb', line 106 def next_page raise Dinie::Error, NO_NEXT_PAGE_MESSAGE unless next_page? self.class.build(@fetch_page.call(@data.last.id), @fetch_page) end |
#next_page? ⇒ Boolean
Whether a next page can be fetched. Driven by ‘has_more`; the `!data.empty?` guard is a safety net — an empty page has no last item (so no cursor), so we stop rather than loop forever on a malformed `{ data: [], has_more: true }`.
97 98 99 |
# File 'lib/dinie/runtime/paginator.rb', line 97 def next_page? @has_more && !@data.empty? end |