Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support backwards pagination with list's #auto_paging_each #865

Merged
merged 2 commits into from
Oct 10, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
22 changes: 20 additions & 2 deletions lib/stripe/list_object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,18 @@ def auto_paging_each(&blk)

page = self
loop do
page.each(&blk)
page = page.next_page
# Backward iterating activates if we have an `ending_before` constraint
# and _just_ an `ending_before` constraint. If `starting_after` was
# also used, we iterate forwards normally.
if filters.include?(:ending_before) &&
ob-stripe marked this conversation as resolved.
Show resolved Hide resolved
!filters.include?(:starting_after)
page.reverse_each(&blk)
page = page.previous_page
else
page.each(&blk)
page = page.next_page
end

break if page.empty?
end
end
Expand Down Expand Up @@ -96,6 +106,8 @@ def next_page(params = {}, opts = {})
# This method will try to respect the limit of the current page. If none
# was given, the default limit will be fetched again.
def previous_page(params = {}, opts = {})
return self.class.empty_list(opts) unless has_more
ob-stripe marked this conversation as resolved.
Show resolved Hide resolved

first_id = data.first.id

params = filters.merge(ending_before: first_id).merge(params)
Expand All @@ -107,5 +119,11 @@ def resource_url
url ||
raise(ArgumentError, "List object does not contain a 'url' field.")
end

# Iterates through each resource in the page represented by the current
# `ListObject` in reverse.
def reverse_each(&blk)
data.reverse_each(&blk)
end
end
end
72 changes: 67 additions & 5 deletions test/stripe/list_object_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,80 @@ class ListObjectTest < Test::Unit::TestCase
assert_equal expected, list.each.to_a
end

should "provide #auto_paging_each" do
should "provide #reverse_each" do
arr = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
]
expected = Util.convert_to_stripe_object(arr.reverse, {})
list = Stripe::ListObject.construct_from(data: arr)
assert_equal expected, list.reverse_each.to_a
end

should "provide #auto_paging_each that supports forward pagination" do
ob-stripe marked this conversation as resolved.
Show resolved Hide resolved
arr = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
{ id: 4 },
{ id: 5 },
{ id: 6 },
]
expected = Util.convert_to_stripe_object(arr, {})

# Initial list object to page on. Notably, its last data element will be
# used as a cursor to fetch the next page.
list = TestListObject.construct_from(data: [{ id: 1 }],
has_more: true,
url: "/things")
list.filters = { limit: 3 }

# The test will start with the synthetic list object above, and use it as
# a starting point to fetch two more pages. The second page indicates
# that there are no more elements by setting `has_more` to `false`, and
# iteration stops.
stub_request(:get, "#{Stripe.api_base}/things")
.with(query: { starting_after: "1" })
.to_return(body: JSON.generate(data: [{ id: 2 }, { id: 3 }], has_more: false))
.with(query: { starting_after: "1", limit: "3" })
.to_return(body: JSON.generate(data: [{ id: 2 }, { id: 3 }, { id: 4 }], has_more: true, url: "/things"))
stub_request(:get, "#{Stripe.api_base}/things")
.with(query: { starting_after: "4", limit: "3" })
.to_return(body: JSON.generate(data: [{ id: 5 }, { id: 6 }], has_more: false, url: "/things"))

assert_equal expected, list.auto_paging_each.to_a
end

should "provide #auto_paging_each that supports backward pagination with `ending_before`" do
arr = [
{ id: 6 },
{ id: 5 },
{ id: 4 },
{ id: 3 },
{ id: 2 },
{ id: 1 },
]
ob-stripe marked this conversation as resolved.
Show resolved Hide resolved
expected = Util.convert_to_stripe_object(arr, {})

# Initial list object to page on. Notably, its first data element will be
# used as a cursor to fetch the next page.
list = TestListObject.construct_from(data: [{ id: 6 }],
has_more: true,
url: "/things")

# We also add an `ending_before` filter on the list to simulate backwards
# pagination.
list.filters = { ending_before: 7, limit: 3 }

# The test will start with the synthetic list object above, and use it as
# a starting point to fetch two more pages. The second page indicates
# that there are no more elements by setting `has_more` to `false`, and
# iteration stops.
stub_request(:get, "#{Stripe.api_base}/things")
.with(query: { ending_before: "6", limit: "3" })
.to_return(body: JSON.generate(data: [{ id: 3 }, { id: 4 }, { id: 5 }], has_more: true, url: "/things"))
stub_request(:get, "#{Stripe.api_base}/things")
.with(query: { ending_before: "3", limit: "3" })
.to_return(body: JSON.generate(data: [{ id: 1 }, { id: 2 }], has_more: false, url: "/things"))

assert_equal expected, list.auto_paging_each.to_a
end
Expand Down Expand Up @@ -114,21 +174,23 @@ class ListObjectTest < Test::Unit::TestCase

should "fetch a next page through #previous_page" do
list = TestListObject.construct_from(data: [{ id: 2 }],
has_more: true,
url: "/things")
stub_request(:get, "#{Stripe.api_base}/things")
.with(query: { ending_before: "2" })
.to_return(body: JSON.generate(data: [{ id: 1 }]))
.to_return(body: JSON.generate(data: [{ id: 1 }], has_more: false))
next_list = list.previous_page
refute next_list.empty?
end

should "fetch a next page through #previous_page and respect limit" do
list = TestListObject.construct_from(data: [{ id: 2 }],
has_more: true,
url: "/things")
list.filters = { expand: ["data.source"], limit: 3 }
stub_request(:get, "#{Stripe.api_base}/things")
.with(query: { "expand[]" => "data.source", "limit" => "3", "ending_before" => "2" })
.to_return(body: JSON.generate(data: [{ id: 1 }]))
.to_return(body: JSON.generate(data: [{ id: 1 }], has_more: false))
next_list = list.previous_page
assert_equal({ expand: ["data.source"], limit: 3 }, next_list.filters)
end
Expand Down