Pagination

Usage on a top-level collection:

```
Stripe::Customer.list.auto_paging_each do |customer|
  puts customer
end
```

Usage on a subcollection:

``` ruby
customer.invoices.auto_paging_each do |invoice|
  puts invoice
end
```

We've also renamed `#all` to `#list` to prevent confusion ("all" implies
that all resources are being returned, and in Stripe's paginated API
this was not the case). An alias has been provided for backward API
compatibility.

Fixes #167.

Replaces #211 and #248.
This commit is contained in:
Brandur 2015-10-02 16:02:23 -07:00
parent 60b7617624
commit 42ea34b969
43 changed files with 270 additions and 74 deletions

View File

@ -2,7 +2,7 @@ module Stripe
class Account < APIResource
include Stripe::APIOperations::Create
include Stripe::APIOperations::Delete
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
include Stripe::APIOperations::Update
def url

View File

@ -1,17 +1,25 @@
module Stripe
module APIOperations
module List
module ClassMethods
def all(filters={}, opts={})
opts = Util.normalize_opts(opts)
response, opts = request(:get, url, filters, opts)
Util.convert_to_stripe_object(response, opts)
end
def list(filters={}, opts={})
opts = Util.normalize_opts(opts)
opts = @opts.merge(opts) if @opts
response, opts = request(:get, url, filters, opts)
obj = ListObject.construct_from(response, opts)
# set a limit so that we can fetch the same number when accessing the
# next and previous pages
obj.limit = filters[:limit]
obj
end
def self.included(base)
base.extend(ClassMethods)
end
# The original version of #list was given the somewhat unfortunate name of
# #all, and this alias allows us to maintain backward compatibility (the
# choice was somewhat misleading in the way that it only returned a single
# page rather than all objects).
alias :all :list
end
end
end

View File

@ -1,6 +1,6 @@
module Stripe
class ApplicationFee < APIResource
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
def self.url
'/v1/application_fees'

View File

@ -1,7 +1,7 @@
module Stripe
class ApplicationFeeRefund < APIResource
include Stripe::APIOperations::Update
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
def url
"#{ApplicationFee.url}/#{CGI.escape(fee)}/refunds/#{CGI.escape(id)}"

View File

@ -1,6 +1,6 @@
module Stripe
class BalanceTransaction < APIResource
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
def self.url
'/v1/balance/history'

View File

@ -2,7 +2,7 @@ module Stripe
class BankAccount < APIResource
include Stripe::APIOperations::Update
include Stripe::APIOperations::Delete
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
def url
if respond_to?(:customer)

View File

@ -3,7 +3,7 @@ module Stripe
include Stripe::APIOperations::Create
include Stripe::APIOperations::Update
include Stripe::APIOperations::Delete
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
def self.url
"/v1/bitcoin/receivers"

View File

@ -1,6 +1,6 @@
module Stripe
class BitcoinTransaction < APIResource
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
def self.url
"/v1/bitcoin/transactions"

View File

@ -2,7 +2,7 @@ module Stripe
class Card < APIResource
include Stripe::APIOperations::Update
include Stripe::APIOperations::Delete
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
def url
if respond_to?(:recipient)

View File

@ -1,6 +1,6 @@
module Stripe
class Charge < APIResource
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
include Stripe::APIOperations::Create
include Stripe::APIOperations::Update

View File

@ -3,6 +3,6 @@ module Stripe
include Stripe::APIOperations::Create
include Stripe::APIOperations::Update
include Stripe::APIOperations::Delete
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
end
end

View File

@ -3,7 +3,7 @@ module Stripe
include Stripe::APIOperations::Create
include Stripe::APIOperations::Delete
include Stripe::APIOperations::Update
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
def add_invoice_item(params, opts={})
opts = @opts.merge(Util.normalize_opts(opts))

View File

@ -1,6 +1,6 @@
module Stripe
class Dispute < APIResource
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
include Stripe::APIOperations::Create
include Stripe::APIOperations::Update

View File

@ -1,5 +1,5 @@
module Stripe
class Event < APIResource
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
end
end

View File

@ -1,7 +1,7 @@
module Stripe
class FileUpload < APIResource
include Stripe::APIOperations::Create
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
def self.url
"/v1/files"

View File

@ -1,6 +1,6 @@
module Stripe
class Invoice < APIResource
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
include Stripe::APIOperations::Update
include Stripe::APIOperations::Create

View File

@ -1,6 +1,6 @@
module Stripe
class InvoiceItem < APIResource
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
include Stripe::APIOperations::Create
include Stripe::APIOperations::Delete
include Stripe::APIOperations::Update

View File

@ -1,8 +1,21 @@
module Stripe
class ListObject < StripeObject
include Enumerable
include Stripe::APIOperations::List
include Stripe::APIOperations::Request
# This accessor allows a `ListObject` to inherit a limit that was given to
# a predecessor. This allows consistent limits as a user pages through
# resources.
attr_accessor :limit
# An empty list object. This is returned from +next+ when we know that
# there isn't a next page in order to replicate the behavior of the API
# when it attempts to return a page beyond the last.
def self.empty_list(opts={})
ListObject.construct_from({ :data => [] }, opts)
end
def [](k)
case k
when String, Symbol
@ -12,10 +25,32 @@ module Stripe
end
end
# Iterates through each resource in the page represented by the current
# `ListObject`.
#
# Note that this method makes no effort to fetch a new page when it gets to
# the end of the current page's resources. See also +auto_paging_each+.
def each(&blk)
self.data.each(&blk)
end
# Iterates through each resource in all pages, making additional fetches to
# the API as necessary.
#
# Note that this method will make as many API calls as necessary to fetch
# all resources. For more granular control, please see +each+ and
# +next_page+.
def auto_paging_each(&blk)
return enum_for(:auto_paging_each) unless block_given?
page = self
loop do
page.each(&blk)
page = page.next_page
break if page.empty?
end
end
# Returns true if the page object contains no elements.
def empty?
self.data.empty?
@ -32,9 +67,35 @@ module Stripe
Util.convert_to_stripe_object(response, opts)
end
def all(params={}, opts={})
response, opts = request(:get, url, params, opts)
Util.convert_to_stripe_object(response, opts)
# Fetches the next page in the resource list (if there is one).
#
# This method will try to respect the limit of the current page. If none
# was given, the default limit will be fetched again.
def next_page(params={}, opts={})
return self.class.empty_list(opts) if !has_more
last_id = data.last.id
params = {
:limit => limit, # may be nil
:starting_after => last_id,
}.merge(params)
list(params, opts)
end
# Fetches the previous page in the resource list (if there is one).
#
# 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={})
first_id = data.first.id
params = {
:ending_before => first_id,
:limit => limit, # may be nil
}.merge(params)
list(params, opts)
end
end
end

View File

@ -1,6 +1,6 @@
module Stripe
class Order < APIResource
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
include Stripe::APIOperations::Create
include Stripe::APIOperations::Update

View File

@ -2,7 +2,7 @@ module Stripe
class Plan < APIResource
include Stripe::APIOperations::Create
include Stripe::APIOperations::Delete
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
include Stripe::APIOperations::Update
end
end

View File

@ -1,6 +1,6 @@
module Stripe
class Product < APIResource
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
include Stripe::APIOperations::Create
include Stripe::APIOperations::Update

View File

@ -3,7 +3,7 @@ module Stripe
include Stripe::APIOperations::Create
include Stripe::APIOperations::Delete
include Stripe::APIOperations::Update
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
def transfers
Transfer.all({ :recipient => id }, @api_key)

View File

@ -1,7 +1,7 @@
module Stripe
class Refund < APIResource
include Stripe::APIOperations::Create
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
include Stripe::APIOperations::Update
end
end

View File

@ -1,7 +1,7 @@
module Stripe
class Reversal < APIResource
include Stripe::APIOperations::Update
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
def url
"#{Transfer.url}/#{CGI.escape(transfer)}/reversals/#{CGI.escape(id)}"

View File

@ -1,6 +1,6 @@
module Stripe
class SKU < APIResource
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
include Stripe::APIOperations::Create
include Stripe::APIOperations::Update

View File

@ -25,6 +25,13 @@ module Stripe
self.new(values[:id]).refresh_from(values, opts)
end
# Determines the equality of two Stripe objects. Stripe objects are
# considered to be equal if they have the same set of values and each one
# of those values is the same.
def ==(other)
@values == other.instance_variable_get(:@values)
end
def to_s(*args)
JSON.pretty_generate(@values)
end

View File

@ -1,6 +1,6 @@
module Stripe
class Transfer < APIResource
include Stripe::APIOperations::List
extend Stripe::APIOperations::List
include Stripe::APIOperations::Create
include Stripe::APIOperations::Update

View File

@ -39,10 +39,10 @@ module Stripe
should "using a nil api key should raise an exception" do
assert_raises TypeError do
Stripe::Customer.all({}, nil)
Stripe::Customer.list({}, nil)
end
assert_raises TypeError do
Stripe::Customer.all({}, { :api_key => nil })
Stripe::Customer.list({}, { :api_key => nil })
end
end
@ -231,17 +231,17 @@ module Stripe
should "urlencode values in GET params" do
response = make_response(make_charge_array)
@mock.expects(:get).with("#{Stripe.api_base}/v1/charges?customer=test%20customer", nil, nil).returns(response)
charges = Stripe::Charge.all(:customer => 'test customer').data
charges = Stripe::Charge.list(:customer => 'test customer').data
assert charges.kind_of? Array
end
should "construct URL properly with base query parameters" do
response = make_response(make_invoice_customer_array)
@mock.expects(:get).with("#{Stripe.api_base}/v1/invoices?customer=test_customer", nil, nil).returns(response)
invoices = Stripe::Invoice.all(:customer => 'test_customer')
invoices = Stripe::Invoice.list(:customer => 'test_customer')
@mock.expects(:get).with("#{Stripe.api_base}/v1/invoices?customer=test_customer&paid=true", nil, nil).returns(response)
invoices.all(:paid => true)
invoices.list(:paid => true)
end
should "a 400 should give an InvalidRequestError with http status, body, and JSON body" do
@ -311,7 +311,7 @@ module Stripe
(url =~ %r{^#{Stripe.api_base}/v1/charges?} &&
query.keys.sort == ['offset', 'sad'])
end.returns(make_response({ :count => 1, :data => [make_charge] }))
Stripe::Charge.all(:count => nil, :offset => 5, :sad => false)
Stripe::Charge.list(:count => nil, :offset => 5, :sad => false)
@mock.expects(:post).with do |url, api_key, params|
url == "#{Stripe.api_base}/v1/charges" &&
@ -335,8 +335,9 @@ module Stripe
should "making a GET request with parameters should have a query string and no body" do
params = { :limit => 1 }
@mock.expects(:get).once.with("#{Stripe.api_base}/v1/charges?limit=1", nil, nil).returns(make_response([make_charge]))
Stripe::Charge.all(params)
@mock.expects(:get).once.with("#{Stripe.api_base}/v1/charges?limit=1", nil, nil).
returns(make_response({ :data => [make_charge] }))
Stripe::Charge.list(params)
end
should "making a POST request with parameters should have a body and no query string" do
@ -407,7 +408,7 @@ module Stripe
should "loading all of an APIResource should return an array of recursively instantiated objects" do
@mock.expects(:get).once.returns(make_response(make_charge_array))
c = Stripe::Charge.all.data
c = Stripe::Charge.list.data
assert c.kind_of? Array
assert c[0].kind_of? Stripe::Charge
assert c[0].card.kind_of?(Stripe::StripeObject) && c[0].card.object == 'card'

View File

@ -4,7 +4,7 @@ module Stripe
class ApplicationFeeTest < Test::Unit::TestCase
should "application fees should be listable" do
@mock.expects(:get).once.returns(make_response(make_application_fee_array))
fees = Stripe::ApplicationFee.all
fees = Stripe::ApplicationFee.list
assert fees.data.kind_of? Array
fees.each do |fee|
assert fee.kind_of?(Stripe::ApplicationFee)

View File

@ -16,7 +16,7 @@ module Stripe
should "all should list bitcoin receivers" do
@mock.expects(:get).once.returns(make_response(make_bitcoin_receiver_array))
receivers = Stripe::BitcoinReceiver.all
receivers = Stripe::BitcoinReceiver.list
assert_equal 3, receivers.data.length
assert receivers.data.kind_of? Array
receivers.each do |receiver|
@ -31,7 +31,7 @@ module Stripe
@mock.expects(:get).with("#{Stripe.api_base}/v1/bitcoin/receivers/btcrcv_test_receiver", nil, nil).once.returns(make_response(make_bitcoin_receiver))
receiver = Stripe::BitcoinReceiver.retrieve('btcrcv_test_receiver')
@mock.expects(:get).with("#{Stripe.api_base}/v1/bitcoin/receivers/btcrcv_test_receiver/transactions", nil, nil).once.returns(make_response(make_bitcoin_transaction_array))
transactions = receiver.transactions.all
transactions = receiver.transactions.list
assert_equal(3, transactions.data.length)
end

View File

@ -18,7 +18,7 @@ module Stripe
with("#{Stripe.api_base}/v1/bitcoin/transactions", nil, nil).
once.
returns(make_response(make_bitcoin_transaction_array))
transactions = Stripe::BitcoinTransaction.all
transactions = Stripe::BitcoinTransaction.list
assert_equal 3, transactions.data.length
assert transactions.data.kind_of? Array
transactions.each do |transaction|

View File

@ -4,7 +4,7 @@ module Stripe
class ChargeTest < Test::Unit::TestCase
should "charges should be listable" do
@mock.expects(:get).once.returns(make_response(make_charge_array))
c = Stripe::Charge.all
c = Stripe::Charge.list
assert c.data.kind_of? Array
c.each do |charge|
assert charge.kind_of?(Stripe::Charge)

View File

@ -12,7 +12,7 @@ module Stripe
should "customer cards should be listable" do
c = customer
@mock.expects(:get).once.returns(make_response(make_customer_card_array(customer.id)))
cards = c.sources.all(:object => "card").data
cards = c.sources.list(:object => "card").data
assert cards.kind_of? Array
assert cards[0].kind_of? Stripe::Card
end

View File

@ -4,7 +4,7 @@ module Stripe
class CustomerTest < Test::Unit::TestCase
should "customers should be listable" do
@mock.expects(:get).once.returns(make_response(make_customer_array))
c = Stripe::Customer.all.data
c = Stripe::Customer.list.data
assert c.kind_of? Array
assert c[0].kind_of? Stripe::Customer
end

View File

@ -10,7 +10,7 @@ module Stripe
should "disputes should be listable" do
@mock.expects(:get).once.returns(make_response(make_dispute_array))
d = Stripe::Dispute.all
d = Stripe::Dispute.list
assert d.data.kind_of? Array
d.each do |dispute|
assert dispute.kind_of?(Stripe::Dispute)

View File

@ -31,7 +31,7 @@ module Stripe
with("#{Stripe.uploads_base}/v1/files", nil, nil).
returns(make_response(make_file_array))
c = Stripe::FileUpload.all.data
c = Stripe::FileUpload.list.data
assert c.kind_of? Array
assert c[0].kind_of? Stripe::FileUpload
end

View File

@ -2,6 +2,123 @@ require File.expand_path('../../test_helper', __FILE__)
module Stripe
class ListObjectTest < Test::Unit::TestCase
should "provide .empty_list" do
list = Stripe::ListObject.empty_list
assert list.empty?
end
should "provide #count via enumerable" do
list = Stripe::ListObject.construct_from(make_charge_array)
assert_equal 3, list.count
end
should "provide #each" do
arr = [
{ :id => 1 },
{ :id => 2 },
{ :id => 3 },
]
expected = Util.convert_to_stripe_object(arr, {})
list = Stripe::ListObject.construct_from({ :data => arr })
assert_equal expected, list.each.to_a
end
should "provide #auto_paging_each" do
arr = [
{ :id => 1 },
{ :id => 2 },
{ :id => 3 },
]
expected = Util.convert_to_stripe_object(arr, {})
list = TestListObject.construct_from({ :data => [{ :id => 1 }], :has_more => true })
@mock.expects(:get).once.with("#{Stripe.api_base}/things?starting_after=1", nil, nil).
returns(make_response({ :data => [{ :id => 2 }, { :id => 3}], :has_more => false }))
assert_equal expected, list.auto_paging_each.to_a
end
should "provide #auto_paging_each that responds to a block" do
arr = [
{ :id => 1 },
{ :id => 2 },
{ :id => 3 },
]
expected = Util.convert_to_stripe_object(arr, {})
list = TestListObject.construct_from({ :data => [{ :id => 1 }], :has_more => true })
@mock.expects(:get).once.with("#{Stripe.api_base}/things?starting_after=1", nil, nil).
returns(make_response({ :data => [{ :id => 2 }, { :id => 3}], :has_more => false }))
actual = []
list.auto_paging_each do |obj|
actual << obj
end
assert_equal expected, actual
end
should "provide #empty?" do
list = Stripe::ListObject.construct_from({ :data => [] })
assert list.empty?
list = Stripe::ListObject.construct_from({ :data => [{}] })
refute list.empty?
end
#
# next_page
#
should "fetch a next page through #next_page" do
list = TestListObject.construct_from({ :data => [{ :id => 1 }], :has_more => true })
@mock.expects(:get).once.with("#{Stripe.api_base}/things?starting_after=1", nil, nil).
returns(make_response({ :data => [{ :id => 2 }], :has_more => false }))
next_list = list.next_page
refute next_list.empty?
end
should "fetch a next page through #next_page and respect limit" do
list = TestListObject.construct_from({ :data => [{ :id => 1 }], :has_more => true })
list.limit = 3
@mock.expects(:get).once.with("#{Stripe.api_base}/things?limit=3&starting_after=1", nil, nil).
returns(make_response({ :data => [{ :id => 2 }], :has_more => false }))
next_list = list.next_page
assert_equal 3, next_list.limit
end
should "fetch an empty page through #next_page" do
list = TestListObject.construct_from({ :data => [{ :id => 1 }], :has_more => false })
next_list = list.next_page
assert_equal Stripe::ListObject.empty_list, next_list
end
#
# previous_page
#
should "fetch a next page through #previous_page" do
list = TestListObject.construct_from({ :data => [{ :id => 2 }] })
@mock.expects(:get).once.with("#{Stripe.api_base}/things?ending_before=2", nil, nil).
returns(make_response({ :data => [{ :id => 1 }] }))
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 }] })
list.limit = 3
@mock.expects(:get).once.with("#{Stripe.api_base}/things?ending_before=2&limit=3", nil, nil).
returns(make_response({ :data => [{ :id => 1 }] }))
next_list = list.previous_page
assert_equal 3, next_list.limit
end
#
# backward compatibility
#
# note that the name #all is deprecated, as is using it fetch the next page
# in a list
should "be able to retrieve full lists given a listobject" do
@mock.expects(:get).twice.returns(make_response(make_charge_array))
c = Stripe::Charge.all
@ -12,19 +129,12 @@ module Stripe
assert_equal('/v1/charges', all.url)
assert all.data.kind_of?(Array)
end
should "provide #empty?" do
object = Stripe::ListObject.construct_from({ :data => [] })
assert object.empty?
object = Stripe::ListObject.construct_from({ :data => [{}] })
refute object.empty?
end
should "provide enumerable functionality" do
@mock.expects(:get).once.returns(make_response(make_charge_array))
c = Stripe::Charge.all
assert c.kind_of?(Stripe::ListObject)
assert_equal 3, c.count
end
end
end
# A helper class with a URL that allows us to try out pagination.
class TestListObject < Stripe::ListObject
def url
"/things"
end
end

View File

@ -4,7 +4,7 @@ module Stripe
class OrderTest < Test::Unit::TestCase
should "orders should be listable" do
@mock.expects(:get).once.returns(make_response(make_order_array))
orders = Stripe::Order.all
orders = Stripe::Order.list
assert orders.data.kind_of?(Array)
orders.each do |order|
assert order.kind_of?(Stripe::Order)

View File

@ -4,7 +4,7 @@ module Stripe
class ProductTest < Test::Unit::TestCase
should "products should be listable" do
@mock.expects(:get).once.returns(make_response(make_product_array))
products = Stripe::Product.all
products = Stripe::Product.list
assert products.data.kind_of?(Array)
products.each do |product|
assert product.kind_of?(Stripe::Product)

View File

@ -12,7 +12,7 @@ module Stripe
should "recipient cards should be listable" do
c = recipient
@mock.expects(:get).once.returns(make_response(make_recipient_card_array(recipient.id)))
cards = c.cards.all.data
cards = c.cards.list.data
assert cards.kind_of? Array
assert cards[0].kind_of? Stripe::Card
end

View File

@ -7,7 +7,7 @@ module Stripe
with("#{Stripe.api_base}/v1/refunds", nil, nil).
once.returns(make_response(make_refund_array))
refunds = Stripe::Refund.all
refunds = Stripe::Refund.list
assert refunds.first.kind_of?(Stripe::Refund)
end

View File

@ -5,7 +5,7 @@ module Stripe
should "SKUs should be listable" do
@mock.expects(:get).once.
returns(make_response(make_sku_array("test_product")))
skus = Stripe::SKU.all
skus = Stripe::SKU.list
assert skus.data.kind_of? Array
skus.each do |sku|
assert sku.kind_of?(Stripe::SKU)

View File

@ -2,7 +2,16 @@ require File.expand_path('../../test_helper', __FILE__)
module Stripe
class StripeObjectTest < Test::Unit::TestCase
should "implement #respond_to correctly" do
should "implement #==" do
obj1 = Stripe::StripeObject.construct_from({ :id => 1, :foo => "bar" })
obj2 = Stripe::StripeObject.construct_from({ :id => 1, :foo => "bar" })
obj3 = Stripe::StripeObject.construct_from({ :id => 1, :foo => "rab" })
assert obj1 == obj2
refute obj1 == obj3
end
should "implement #respond_to" do
obj = Stripe::StripeObject.construct_from({ :id => 1, :foo => 'bar' })
assert obj.respond_to?(:id)
assert obj.respond_to?(:foo)