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

Add support for Search API #992

Merged
merged 6 commits into from
Mar 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions lib/stripe.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
require "stripe/api_operations/nested_resource"
require "stripe/api_operations/request"
require "stripe/api_operations/save"
require "stripe/api_operations/search"

# API resource support classes
require "stripe/errors"
Expand All @@ -35,6 +36,7 @@
require "stripe/stripe_object"
require "stripe/stripe_response"
require "stripe/list_object"
require "stripe/search_result_object"
require "stripe/error_object"
require "stripe/api_resource"
require "stripe/singleton_api_resource"
Expand Down
19 changes: 19 additions & 0 deletions lib/stripe/api_operations/search.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module Stripe
module APIOperations
module Search
def _search(search_url, filters = {}, opts = {})
opts = Util.normalize_opts(opts)

resp, opts = execute_resource_request(:get, search_url, filters, opts)
obj = SearchResultObject.construct_from(resp.data, opts)

# set filters so that we can fetch the same limit and query
# when accessing the next page
obj.filters = filters.dup
obj
end
end
end
end
1 change: 1 addition & 0 deletions lib/stripe/object_types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ def self.object_names_to_classes
{
# data structures
ListObject::OBJECT_NAME => ListObject,
SearchResultObject::OBJECT_NAME => SearchResultObject,
pakrym-stripe marked this conversation as resolved.
Show resolved Hide resolved

# business objects
Account::OBJECT_NAME => Account,
Expand Down
86 changes: 86 additions & 0 deletions lib/stripe/search_result_object.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# frozen_string_literal: true

module Stripe
class SearchResultObject < StripeObject
include Enumerable
include Stripe::APIOperations::Search
include Stripe::APIOperations::Request

OBJECT_NAME = "search_result"

# This accessor allows a `SearchResultObject` to inherit various filters
# that were given to a predecessor. This allows for things like consistent
# limits, expansions, and predicates as a user pages through resources.
attr_accessor :filters

# An empty search result 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_search_result(opts = {})
SearchResultObject.construct_from({ data: [] }, opts)
end

def initialize(*args)
super
self.filters = {}
end

def [](key)
case key
when String, Symbol
super
else
raise ArgumentError,
"You tried to access the #{key.inspect} index, but " \
"SearchResultObject types only support String keys. " \
"(HINT: Search calls return an object with a 'data' (which is " \
"the data array). You likely want to call #data[#{key.inspect}])"
end
end

# Iterates through each resource in the page represented by the current
# `SearchListObject`.
#
# 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)
data.each(&blk)
end

# Returns true if the page object contains no elements.
def empty?
data.empty?
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_search_result_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_search_result_page

break if page.empty?
end
end

# 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_search_result_page(params = {}, opts = {})
return self.class.empty_search_result(opts) unless has_more

params = filters.merge(page: next_page).merge(params)

_search(url, params, opts)
end
end
end
132 changes: 132 additions & 0 deletions test/stripe/search_result_object_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# frozen_string_literal: true

require ::File.expand_path("../test_helper", __dir__)

module Stripe
class SearchResultObjectTest < Test::Unit::TestCase
should "provide .empty_list" do
list = Stripe::SearchResultObject.empty_search_result
assert list.empty?
end

should "provide #count via enumerable" do
list = Stripe::SearchResultObject.construct_from(data: [{ object: "charge" }])
assert_equal 1, list.count
end

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

should "provide #auto_paging_each that supports forward pagination" do
arr = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
{ id: 4 },
{ id: 5 },
{ id: 6 },
]

list = TestSearchResultObject.construct_from({ data: [{ id: 1 }],
has_more: true,
next_page: "next_page_token_1",
url: "/things", })
list.filters = { limit: 3 }

# The test will start with the synthetic search result object above, and uses the
# 'next_page' token 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: { limit: 3, page: "next_page_token_1" })
.to_return(body: JSON.generate(data: [{ id: 2 }, { id: 3 }, { id: 4 }], has_more: true, url: "/things", next_page: "next_page_token_2"))
stub_request(:get, "#{Stripe.api_base}/things")
.with(query: { limit: 3, page: "next_page_token_2" })
.to_return(body: JSON.generate(data: [{ id: 5 }, { id: 6 }], has_more: false, url: "/things", next_page: nil))

assert_equal arr, list.auto_paging_each.to_a.map(&:to_hash)
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 = TestSearchResultObject.construct_from(data: [{ id: 1 }],
has_more: true,
next_page: "next_page_token_1",
url: "/things")

stub_request(:get, "#{Stripe.api_base}/things")
.with(query: { page: "next_page_token_1" })
.to_return(body: JSON.generate(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::SearchResultObject.construct_from(data: [])
assert list.empty?
list = Stripe::SearchResultObject.construct_from(data: [{}])
refute list.empty?
end

#
# next_page
#

should "fetch a next page through #next_page" do
list = TestSearchResultObject.construct_from(data: [{ id: 1 }],
has_more: true,
next_page: "next_page_token_1",
url: "/things")
stub_request(:get, "#{Stripe.api_base}/things")
.with(query: { page: "next_page_token_1" })
.to_return(body: JSON.generate(data: [{ id: 2 }], has_more: false))
next_list = list.next_search_result_page
refute next_list.empty?
assert_equal [{ id: 2 }], next_list.auto_paging_each.to_a.map(&:to_hash)
end

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

should "fetch an empty page through #next_page" do
list = TestSearchResultObject.construct_from(data: [{ id: 1 }],
has_more: false,
url: "/things")
next_list = list.next_search_result_page
assert_equal Stripe::SearchResultObject.empty_search_result, next_list
end
end
end

# A helper class with a URL that allows us to try out pagination.
class TestSearchResultObject < Stripe::SearchResultObject
end