Class: Dinie::Page

Inherits:
Object
  • Object
show all
Defined in:
lib/dinie/runtime/paginator.rb,
sig/dinie/runtime/paginator.rbs

Overview

Note:

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).

Examples:

auto-paginate every item

client.customers.list(limit: 50).each { |c| puts c.id }

page-by-page

client.customers.list.each_page { |page| process(page.data) }

just the first 10, lazily

top = client.customers.list.first(10)

Constant Summary collapse

NO_NEXT_PAGE_MESSAGE =

Message raised by #next_page when there is no next page to fetch.

Returns:

  • (String)
"No next page; guard with `#next_page?` (or check `#has_more`) first."
UNSPECIFIED =

Sentinel distinguishing first (one item) from first(n) (an array) — nil/0 are valid n.

Returns:

  • (Object)
Object.new

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data:, has_more:, fetch_page:) ⇒ Page

Returns a new instance of Page.

Parameters:

  • data (Array<Object>) —

    this page's items

  • has_more (Boolean) —

    whether more pages follow

  • fetch_page (#call) —

    ->(cursor) { envelope } for the next page

  • data: (Array[Elem])
  • has_more: (Boolean)
  • fetch_page: (^(String?) -> Hash[Symbol, untyped])


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.

Returns:

  • (Array<Object>)


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.

Returns:

  • (Boolean)


45
46
47
# File 'lib/dinie/runtime/paginator.rb', line 45

def has_more
  @has_more
end

Class Method Details

.build(envelope, fetch_page) ⇒ void

This method returns an undefined value.

Wrap a fetched { data:, has_more: } envelope 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) ⇒ void

This method returns an undefined value.

Build the FIRST page by invoking fetch_page with no cursor.



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

A list endpoint is paginated iff its envelope carries has_more.

Parameters:

  • envelope (Object)

Returns:

  • (Boolean)


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 ⇒ self #each ⇒ Enumerator[Elem, self] Also known as: auto_paging_each

Auto-paginate: yield every item of this page, then every following page. No block → Enumerator.

Overloads:

  • #each ⇒ self

    Returns:

    • (self)
  • #each ⇒ Enumerator[Elem, self]

    Returns:

    • (Enumerator[Elem, self])

Yields:

Yield Parameters:

  • item (Elem)

Yield Returns:

  • (void)


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 ⇒ self #each_page ⇒ Enumerator[Page[Elem], self]

Yield page-by-page (manual control). No block → Enumerator over the pages.

Overloads:

  • #each_page ⇒ self

    Returns:

    • (self)
  • #each_page ⇒ Enumerator[Page[Elem], self]

    Returns:

    • (Enumerator[Page[Elem], self])

Yields:

Yield Parameters:

Yield Returns:

  • (void)


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), fetched lazily. (count defaults to a private sentinel, so the signature is loose; returns Elem? or Array[Elem].)

Parameters:

  • count (Object) (defaults to: UNSPECIFIED)

Returns:

  • (Object)


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 ⇒ Page[Elem]

Fetch the next page (cursor = id of this page's last item). Guard with next_page?.

Returns:



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.

Returns:

  • (Boolean)


97
98
99
# File 'lib/dinie/runtime/paginator.rb', line 97

def next_page?
  @has_more && !@data.empty?
end