Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7a832be
Spitball an ApiError hierarchy.
chanks Sep 15, 2025
53d4596
Add more error types.
chanks Sep 15, 2025
bc52ba8
Add error handling for http timeouts.
chanks Sep 15, 2025
44b2afa
Actually, introduce an ApiError that's a superclass of all our errors.
chanks Sep 15, 2025
37ca77b
Minor tweaks.
chanks Sep 15, 2025
4f38006
Initial implementation of ItemIterator and PageIterator.
chanks Sep 17, 2025
b06c0d6
Get basic iterator tests passing.
chanks Sep 17, 2025
6b01040
Test that iterators only call the API as many times as is needed.
chanks Sep 17, 2025
ed9fd57
Cleanup and expand tests.
chanks Sep 17, 2025
352cd2f
Add has_next_page? method to PageIterator.
chanks Sep 17, 2025
9053ad1
More tests.
chanks Sep 17, 2025
a27ade5
Support manual iteration for pages.
chanks Sep 17, 2025
49722a2
Support manual iteration for ItemIterator as well.
chanks Sep 17, 2025
f348ee1
Implement has_next? method for ItemIterator as well.
chanks Sep 17, 2025
a220cad
Add docstrings to the ItemIterator and PageIterator classes.
chanks Sep 17, 2025
b5805a5
Ensure that PageIterator handles fetching the first page, where there…
chanks Sep 17, 2025
9703d5e
Don't hard-code cursor field.
chanks Sep 18, 2025
586d60e
First pass at offset-based auto-pagination.
chanks Sep 18, 2025
65d6ffe
Split iterator class into two, one cursor-based and one offset-based.
chanks Sep 18, 2025
8bbb307
Merge common logic into an ItemIterator class.
chanks Sep 18, 2025
ba451de
Add proper step and has_next_page support to offset iterator, and tho…
chanks Sep 19, 2025
69eaad5
Cleanup.
chanks Sep 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/square.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
require_relative "square/internal/types/enum"
require_relative "square/internal/types/hash"
require_relative "square/internal/types/unknown"
require_relative "square/internal/iterators/item_iterator"
require_relative "square/internal/iterators/cursor_item_iterator"
require_relative "square/internal/iterators/cursor_page_iterator"
require_relative "square/internal/iterators/offset_item_iterator"
require_relative "square/internal/iterators/offset_page_iterator"

# API Types
require_relative "square/file_param"
Expand Down
37 changes: 22 additions & 15 deletions lib/square/cards/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,34 @@ def initialize(client:)
@client = client
end

# Retrieves a list of cards owned by the account making the request.
# A max of 25 cards will be returned.
# Retrieves an ItemIterator of cards owned by the account making the request.
#
# @return [Square::Types::ListCardsResponse]
# @return Square::Internal::ItemIterator
def list(request_options: {}, **params)
_query_param_names = %w[cursor customer_id include_disabled reference_id sort_order]
_query = params.slice(*_query_param_names)
params.except(*_query_param_names)

_request = Square::Internal::JSON::Request.new(
base_url: request_options[:base_url] || Square::Environment::SANDBOX,
method: "GET",
path: "v2/cards",
query: _query
)
_response = @client.send(_request)
if _response.code >= "200" && _response.code < "300"
return Square::Types::ListCardsResponse.load(_response.body)
end
Square::Internal::ItemIterator.new(item_field: :cards, initial_cursor: params[:cursor]) do |cursor|
_request = Square::Internal::JSON::Request.new(
base_url: request_options[:base_url] || Square::Environment::SANDBOX,
method: "GET",
path: "v2/cards",
query: _query.merge(cursor: cursor)
)

raise _response.body
begin
_response = @client.send(_request)
rescue Net::HTTPRequestTimeout
raise Square::Errors::TimeoutError
end
code = _response.code.to_i
if code.between?(200, 299)
Square::Types::ListCardsResponse.load(_response.body)
else
subclass = Square::Errors::ResponseError.subclass_for_code(code)
raise subclass.new(_response.body, code: code)
end
end
end

# Adds a card on file to an existing merchant.
Expand Down
6 changes: 6 additions & 0 deletions lib/square/errors/api_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module Square
module Errors
class ApiError < StandardError
end
end
end
15 changes: 15 additions & 0 deletions lib/square/errors/client_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Square
module Errors
class ClientError < ResponseError
end

class UnauthorizedError < ClientError
end

class ForbiddenError < ClientError
end

class NotFoundError < ClientError
end
end
end
6 changes: 6 additions & 0 deletions lib/square/errors/redirect_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module Square
module Errors
class RedirectError < ResponseError
end
end
end
38 changes: 38 additions & 0 deletions lib/square/errors/response_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

module Square
module Errors
class ResponseError < ApiError
attr_reader :code

def initialize(msg, code:)
@code = code
super(msg)
end

# Returns the most appropriate error class for the given code.
#
# @return [Class]
def self.subclass_for_code(code)
case code
when 300..399
RedirectError
when 401
UnauthorizedError
when 403
ForbiddenError
when 404
NotFoundError
when 400..499
ClientError
when 503
ServiceUnavailableError
when 500..599
ServerError
else
ResponseError
end
end
end
end
end
9 changes: 9 additions & 0 deletions lib/square/errors/server_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Square
module Errors
class ServerError < ResponseError
end

class ServiceUnavailableError < ApiError
end
end
end
6 changes: 6 additions & 0 deletions lib/square/errors/timeout_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module Square
module Errors
class TimeoutError < ApiError
end
end
end
23 changes: 23 additions & 0 deletions lib/square/internal/iterators/cursor_item_iterator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module Square
module Internal
class CursorItemIterator < ItemIterator
# Instantiates a CursorItemIterator, an Enumerable class which wraps calls to a cursor-based paginated API and yields individual items from it.
#
# @param initial_cursor [String] The initial cursor to use when iterating, if any.
# @param cursor_field [Symbol] The field in API responses to extract the next cursor from.
# @param item_field [Symbol] The field in API responses to extract the items to iterate over.
def initialize(initial_cursor:, cursor_field:, item_field:, &block)
@item_field = item_field
@page_iterator = CursorPageIterator.new(initial_cursor:, cursor_field:, &block)
@page = nil
end

# Returns the CursorPageIterator mediating access to the underlying API.
#
# @return [Square::Internal::CursorPageIterator]
def pages
@page_iterator
end
end
end
end
47 changes: 47 additions & 0 deletions lib/square/internal/iterators/cursor_page_iterator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
module Square
module Internal
class CursorPageIterator
include Enumerable

# Instantiates a CursorPageIterator, an Enumerable class which wraps calls to a paginated API and yields pages of items.
#
# @param initial_cursor [String] The initial cursor to use when iterating.
# @param cursor_field [String] The name of the field in API responses to extract the next cursor from.
# @return [Square::Internal::CursorPageIterator]
def initialize(initial_cursor:, cursor_field:, &block)
@need_initial_load = initial_cursor.nil?
@cursor = initial_cursor
@cursor_field = cursor_field
@get_next_page = block
end

# Iterates over each page returned by the API.
#
# @param block [Proc] The block which each retrieved page is yielded to.
# @return [nil]
def each(&block)
while page = get_next do
block.call(page)
end
end

# Whether another page will be available from the API.
#
# @return [Boolean]
def has_next?
@need_initial_load || !@cursor.nil?
end

# Retrieves the next page from the API.
#
# @return [Boolean]
def get_next
return if !@need_initial_load && @cursor.nil?
@need_initial_load = false
next_page = @get_next_page.call(@cursor)
@cursor = next_page.send(@cursor_field)
next_page
end
end
end
end
58 changes: 58 additions & 0 deletions lib/square/internal/iterators/item_iterator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
module Square
module Internal
class ItemIterator
include Enumerable

# Iterates over each item returned by the API.
#
# @param block [Proc] The block which each retrieved item is yielded to.
# @return [nil]
def each(&block)
while item = get_next do
block.call(item)
end
end

# Whether another item will be available from the API.
#
# @return [Boolean]
def has_next?
load_next_page if @page.nil?
return false if @page.nil?

return true if any_items_in_cached_page
load_next_page
return false if @page.nil?
any_items_in_cached_page
end

# Retrieves the next item from the API.
#
# @return [Boolean]
def get_next
item = next_item_from_cached_page
return item if item
load_next_page
next_item_from_cached_page
end

private

def next_item_from_cached_page
return if !@page
@page.send(@item_field)&.shift
end

def any_items_in_cached_page
return false if !@page
items = @page.send(@item_field)
return false if items.nil?
!items.empty?
end

def load_next_page
@page = @page_iterator.get_next
end
end
end
end
24 changes: 24 additions & 0 deletions lib/square/internal/iterators/offset_item_iterator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module Square
module Internal
class OffsetItemIterator < ItemIterator
# Instantiates an OffsetItemIterator, an Enumerable class which wraps calls to an offset-based paginated API and yields the individual items from it.
#
# @param initial_page [Integer] The initial cursor to use when iterating.
# @param item_field [Symbol] The name of the field in API responses to extract the items to iterate over.
# @param has_next_field [Symbol] The name of the field in API responses containing a boolean of whether another page exists.
# @param step [Boolean] If true, treats the page number as a true offset (i.e. increments the page number by the number of items returned from each call rather than just 1)
def initialize(initial_page:, item_field:, has_next_field:, step:, &block)
@item_field = item_field
@page_iterator = OffsetPageIterator.new(initial_page:, item_field:, has_next_field:, step:, &block)
@page = nil
end

# Returns the OffsetPageIterator that is mediating access to the underlying API.
#
# @return [Square::Internal::OffsetPageIterator]
def pages
@page_iterator
end
end
end
end
83 changes: 83 additions & 0 deletions lib/square/internal/iterators/offset_page_iterator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
module Square
module Internal
class OffsetPageIterator
include Enumerable

# Instantiates an OffsetPageIterator, an Enumerable class which wraps calls to an offset-based paginated API and yields pages of items from it.
#
# @param initial_page [Integer] The initial page to use when iterating, if any.
# @param item_field [Symbol] The field to pull the list of items to iterate over.
# @param has_next_field [Symbol] The field to pull the boolean of whether a next page exists from, if any.
# @param step [Boolean] If true, treats the page number as a true offset (i.e. increments the page number by the number of items returned from each call rather than just 1)
# @return [Square::Internal::OffsetPageIterator]
def initialize(initial_page:, item_field:, has_next_field:, step:, &block)
@page_number = initial_page || (step ? 0 : 1)
@item_field = item_field
@get_next_page = block

@next_page = nil
@has_next_field = has_next_field
@has_next_page = nil
@step = step
end

# Iterates over each page returned by the API.
#
# @param block [Proc] The block which each retrieved page is yielded to.
# @return [nil]
def each(&block)
while page = get_next do
block.call(page)
end
end

# Whether another page will be available from the API.
#
# @return [Boolean]
def has_next?
return @has_next_page unless @has_next_page.nil?
return true if @next_page

next_page = @get_next_page.call(@page_number)
next_page_items = next_page&.send(@item_field)
if next_page_items.nil? || next_page_items.empty?
@has_next_page = false
else
@next_page = next_page
true
end
end

# Retrieves the next page from the API.
#
# @return [Boolean]
def get_next
return nil if @page_number.nil?

# We sometimes preload the next page so that has_next? will be accurate even when we don't have has_next_field.
if @next_page
this_page = @next_page
@next_page = nil
else
this_page = @get_next_page.call(@page_number)
end

if @has_next_field
@has_next_page = this_page&.send(@has_next_field)
end

items = this_page.send(@item_field)
if items.nil? || items.empty?
@page_number = nil
return nil
elsif @step
@page_number += items.length
else
@page_number += 1
end

this_page
end
end
end
end
Loading