philiprehberger-pagination

Tests Gem Version Last updated

Framework-agnostic pagination with cursor, offset, and keyset strategies

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem "philiprehberger-pagination"

Or install directly:

gem install philiprehberger-pagination

Usage

require "philiprehberger/pagination"

page = Philiprehberger::Pagination.paginate(users,
  strategy: :offset,
  per_page: 25,
  page: 2
)

page.items       # => [user_26, user_27, ...]
page.total       # => 100
page.has_next?   # => true
page.has_prev?   # => true

Offset Pagination

page = Philiprehberger::Pagination.paginate(items, strategy: :offset, per_page: 10, page: 3)
page.next_cursor  # => "4" (next page number)
page.prev_cursor  # => "2" (previous page number)
page.next_page    # => 4
page.prev_page    # => 2
page.page_range   # => 1..5

Cursor Pagination

page = Philiprehberger::Pagination.paginate(items, strategy: :cursor, per_page: 10)
next_page = Philiprehberger::Pagination.paginate(items,
  strategy: :cursor,
  per_page: 10,
  cursor: page.next_cursor
)

Walking Pages with next_params / prev_params

page = Philiprehberger::Pagination.paginate(items, strategy: :offset, per_page: 10, page: 2)

page.next_params # => { page: 3, per_page: 10 }
page.prev_params # => { page: 1, per_page: 10 }

next_page = Philiprehberger::Pagination.paginate(items, strategy: :offset, **page.next_params)

Keyset Pagination

# Items must be sorted; cursor is based on the last item's value
page = Philiprehberger::Pagination.paginate(sorted_items, strategy: :keyset, per_page: 10)

Page Metadata

page = Philiprehberger::Pagination.paginate(items, strategy: :offset, per_page: 10, page: 2)
page.
# => { current_page: 2, per_page: 10, total_pages: 5, total_count: 50, offset: 10 }
page.total_pages   # => 5
page.first_page?   # => false
page.last_page?    # => false

Hash & JSON Serialization

page = Philiprehberger::Pagination.paginate(items, strategy: :offset, per_page: 10, page: 2)
page.to_h
# => {
#   items: [...],
#   metadata: { current_page: 2, per_page: 10, total_pages: 5, total_count: 50, offset: 10 },
#   links: { next: "3", prev: "1" }
# }

page.to_json
# => '{"items":[...],"metadata":{...},"links":{...}}'

# Formatting options are forwarded to JSON.generate
page.to_json(indent: "  ", space: " ", object_nl: "\n")

Iteration

page = Philiprehberger::Pagination.paginate(items, strategy: :offset, per_page: 10)
page.size              # => 10
page.empty?            # => false
page.each { |item| puts item }
page.map { |item| item.name }  # Enumerable methods work directly on the page

Page Size Limits

page = Philiprehberger::Pagination.paginate(items,
  per_page: 25,
  max_per_page: 100,
  min_per_page: 1
)
# Raises InvalidPageSizeError if per_page is out of bounds

Cursor Encryption

page = Philiprehberger::Pagination.paginate(items,
  strategy: :cursor,
  per_page: 10,
  secret: "my-secret-key"
)
# Cursors are signed with HMAC-SHA256
# Tampered cursors raise InvalidCursorError

Iterating All Pages

each_page walks the collection one page at a time, threading cursors (or incrementing the page number for offset strategy) until exhausted. Returns an Enumerator if no block is given. Useful for export, ETL, and slow report generation that should pull every record without holding the entire result set in memory at once.

Philiprehberger::Pagination.each_page(records, strategy: :offset, per_page: 100) do |page|
  process(page.items)
end

# Cursor strategy works the same way
Philiprehberger::Pagination.each_page(records, strategy: :cursor, per_page: 100) do |page|
  process(page.items)
end

# Without a block, returns an Enumerator
total = Philiprehberger::Pagination.each_page(records, per_page: 100).sum { |p| p.items.length }

API

Pagination

Method Description
.paginate(collection, strategy:, per_page:, cursor:, page:, max_per_page:, min_per_page:, secret:) Paginate a collection
.each_page(collection, strategy: :offset, per_page:, **opts) Yield every Page of a collection in order; returns an Enumerator if no block is given

Page

Method Description
#items Items on the current page
#total Total number of items in the collection
#next_cursor Cursor for the next page
#prev_cursor Cursor for the previous page
#has_next? Whether there is a next page
#has_prev? Whether there is a previous page
#first_page? Whether this is the first page
#last_page? Whether this is the last page
#next_page Next page number (offset only)
#prev_page Previous page number (offset only)
#next_params Hash of kwargs to splat into Pagination.paginate for the next page (nil at end)
#prev_params Hash of kwargs to splat into Pagination.paginate for the previous page (nil at start)
#page_range Range of all page numbers, e.g. 1..5
#total_pages Ceiling division of total by per_page
#links Hash of navigation cursors
#metadata Hash with current_page, per_page, total_pages, total_count, offset
#to_h Hash with items, metadata, and links (JSON-ready)
#to_json(*args) JSON string of the page; extra args forwarded to JSON.generate
#size / #length / #count Number of items on this page
#empty? Whether the page has no items
#each Iterate over items (includes Enumerable)
#per_page Items per page
#current_page Current page number (offset only)
#offset Current offset (offset only)

Development

bundle install
bundle exec rspec
bundle exec rubocop

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT