From c8380a08c6948384d5182c2f01aaff2998552ccf Mon Sep 17 00:00:00 2001 From: Susindaran Elangovan Date: Mon, 2 Aug 2021 16:47:35 -0700 Subject: [PATCH 1/5] Add support for search-api --- lib/stripe.rb | 2 + lib/stripe/api_operations/search.rb | 19 ++++ lib/stripe/object_types.rb | 1 + lib/stripe/search_result_object.rb | 86 +++++++++++++++ test/stripe/search_result_object_test.rb | 132 +++++++++++++++++++++++ 5 files changed, 240 insertions(+) create mode 100644 lib/stripe/api_operations/search.rb create mode 100644 lib/stripe/search_result_object.rb create mode 100644 test/stripe/search_result_object_test.rb diff --git a/lib/stripe.rb b/lib/stripe.rb index 1d8a45a12..3f4ddc23e 100644 --- a/lib/stripe.rb +++ b/lib/stripe.rb @@ -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" @@ -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" diff --git a/lib/stripe/api_operations/search.rb b/lib/stripe/api_operations/search.rb new file mode 100644 index 000000000..0d9ec839f --- /dev/null +++ b/lib/stripe/api_operations/search.rb @@ -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, expansions, and + # predicates when accessing the next and previous pages + obj.filters = filters.dup + obj + end + end + end +end diff --git a/lib/stripe/object_types.rb b/lib/stripe/object_types.rb index 612c34584..e671816a7 100644 --- a/lib/stripe/object_types.rb +++ b/lib/stripe/object_types.rb @@ -9,6 +9,7 @@ def self.object_names_to_classes { # data structures ListObject::OBJECT_NAME => ListObject, + SearchResultObject::OBJECT_NAME => SearchResultObject, # business objects Account::OBJECT_NAME => Account, diff --git a/lib/stripe/search_result_object.rb b/lib/stripe/search_result_object.rb new file mode 100644 index 000000000..347ccb79f --- /dev/null +++ b/lib/stripe/search_result_object.rb @@ -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(next_page: next_page).merge(params) + + _search(url, params, opts) + end + end +end diff --git a/test/stripe/search_result_object_test.rb b/test/stripe/search_result_object_test.rb new file mode 100644 index 000000000..e6bc3316e --- /dev/null +++ b/test/stripe/search_result_object_test.rb @@ -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 list 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, next_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, next_page: "next_page_token_2" }) + .to_return(body: JSON.generate(data: [{ id: 5 }, { id: 6 }], has_more: false, url: "/things")) + + 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: { next_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: { next_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, next_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, next_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 From 723618070cd63464dd14f253c7f66012c20fc0c7 Mon Sep 17 00:00:00 2001 From: Pavel Krymets Date: Wed, 16 Mar 2022 14:53:35 -0700 Subject: [PATCH 2/5] adjust property names --- lib/stripe/search_result_object.rb | 2 +- test/stripe/search_result_object_test.rb | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/stripe/search_result_object.rb b/lib/stripe/search_result_object.rb index 347ccb79f..81e10ca0f 100644 --- a/lib/stripe/search_result_object.rb +++ b/lib/stripe/search_result_object.rb @@ -78,7 +78,7 @@ def auto_paging_each(&blk) def next_search_result_page(params = {}, opts = {}) return self.class.empty_search_result(opts) unless has_more - params = filters.merge(next_page: next_page).merge(params) + params = filters.merge(page: next_page).merge(params) _search(url, params, opts) end diff --git a/test/stripe/search_result_object_test.rb b/test/stripe/search_result_object_test.rb index e6bc3316e..c0f934655 100644 --- a/test/stripe/search_result_object_test.rb +++ b/test/stripe/search_result_object_test.rb @@ -46,11 +46,11 @@ class SearchResultObjectTest < Test::Unit::TestCase # 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, next_page: "next_page_token_1" }) + .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, next_page: "next_page_token_2" }) - .to_return(body: JSON.generate(data: [{ id: 5 }, { id: 6 }], has_more: false, url: "/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 @@ -69,7 +69,7 @@ class SearchResultObjectTest < Test::Unit::TestCase url: "/things") stub_request(:get, "#{Stripe.api_base}/things") - .with(query: { next_page: "next_page_token_1" }) + .with(query: { page: "next_page_token_1" }) .to_return(body: JSON.generate(data: [{ id: 2 }, { id: 3 }], has_more: false)) actual = [] @@ -97,7 +97,7 @@ class SearchResultObjectTest < Test::Unit::TestCase next_page: "next_page_token_1", url: "/things") stub_request(:get, "#{Stripe.api_base}/things") - .with(query: { next_page: "next_page_token_1" }) + .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? @@ -111,10 +111,10 @@ class SearchResultObjectTest < Test::Unit::TestCase url: "/things") list.filters = { limit: 3 } stub_request(:get, "#{Stripe.api_base}/things") - .with(query: { "limit": 3, next_page: "next_page_token_1" }) + .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, next_page: "next_page_token_1" }, next_list.filters) + assert_equal({ limit: 3, page: "next_page_token_1" }, next_list.filters) end should "fetch an empty page through #next_page" do From 2a2d05ce8a4ea725aaa510d52d71480ac4c23776 Mon Sep 17 00:00:00 2001 From: Pavel Krymets Date: Wed, 16 Mar 2022 15:18:53 -0700 Subject: [PATCH 3/5] fb --- lib/stripe/api_operations/search.rb | 4 ++-- lib/stripe/object_types.rb | 1 - test/stripe/search_result_object_test.rb | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/stripe/api_operations/search.rb b/lib/stripe/api_operations/search.rb index 0d9ec839f..11c0fdbd1 100644 --- a/lib/stripe/api_operations/search.rb +++ b/lib/stripe/api_operations/search.rb @@ -9,8 +9,8 @@ def _search(search_url, filters = {}, 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, expansions, and - # predicates when accessing the next and previous pages + # set filters so that we can fetch the same limit and query when accessing + # the next page obj.filters = filters.dup obj end diff --git a/lib/stripe/object_types.rb b/lib/stripe/object_types.rb index cc836fa25..b7b9112ca 100644 --- a/lib/stripe/object_types.rb +++ b/lib/stripe/object_types.rb @@ -9,7 +9,6 @@ def self.object_names_to_classes { # data structures ListObject::OBJECT_NAME => ListObject, - SearchResultObject::OBJECT_NAME => SearchResultObject, # business objects Account::OBJECT_NAME => Account, diff --git a/test/stripe/search_result_object_test.rb b/test/stripe/search_result_object_test.rb index c0f934655..706fcbd79 100644 --- a/test/stripe/search_result_object_test.rb +++ b/test/stripe/search_result_object_test.rb @@ -41,7 +41,7 @@ class SearchResultObjectTest < Test::Unit::TestCase url: "/things", }) list.filters = { limit: 3 } - # The test will start with the synthetic list object above, and uses the + # 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. From 26dbae9d85032e340aab8eeaae8a5a05aba4eb4d Mon Sep 17 00:00:00 2001 From: Pavel Krymets Date: Wed, 16 Mar 2022 15:22:45 -0700 Subject: [PATCH 4/5] lint --- lib/stripe/api_operations/search.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/stripe/api_operations/search.rb b/lib/stripe/api_operations/search.rb index 11c0fdbd1..b13b47ac0 100644 --- a/lib/stripe/api_operations/search.rb +++ b/lib/stripe/api_operations/search.rb @@ -9,8 +9,8 @@ def _search(search_url, filters = {}, 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 + # set filters so that we can fetch the same limit and query + # when accessing the next page obj.filters = filters.dup obj end From cdec18021ad9e4fff37f48fa75b5aae3e9d6614a Mon Sep 17 00:00:00 2001 From: Pavel Krymets Date: Thu, 17 Mar 2022 07:36:20 -0700 Subject: [PATCH 5/5] objecttypes --- lib/stripe/object_types.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/stripe/object_types.rb b/lib/stripe/object_types.rb index b7b9112ca..cc836fa25 100644 --- a/lib/stripe/object_types.rb +++ b/lib/stripe/object_types.rb @@ -9,6 +9,7 @@ def self.object_names_to_classes { # data structures ListObject::OBJECT_NAME => ListObject, + SearchResultObject::OBJECT_NAME => SearchResultObject, # business objects Account::OBJECT_NAME => Account,