Skip to content

Commit 6485a06

Browse files
fix: correct pagination heuristic for Compute (#1188)
1 parent 965aa53 commit 6485a06

File tree

10 files changed

+588
-26
lines changed

10 files changed

+588
-26
lines changed

gapic-generator-ads/test/test_helper.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ def method_presenter api_name, service_name, method_name
6868
refute_nil service
6969
method = service.methods.find { |s| s.name == method_name }
7070
refute_nil method
71+
72+
yield api_obj, service, method if block_given?
73+
7174
gem_presenter = Gapic::Presenters::GemPresenter.new api_obj
7275
service_presenter = Gapic::Presenters::ServicePresenter.new gem_presenter, api_obj, service
7376
Gapic::Presenters::MethodPresenter.new service_presenter, api_obj, method

gapic-generator-cloud/test/test_helper.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ def method_presenter api_name, service_name, method_name
109109
refute_nil service
110110
method = service.methods.find { |s| s.name == method_name }
111111
refute_nil method
112+
113+
yield api_obj, service, method if block_given?
114+
112115
gem_presenter = Gapic::Presenters::GemPresenter.new api_obj
113116
service_presenter = Gapic::Presenters::ServicePresenter.new gem_presenter, api_obj, service
114117
Gapic::Presenters::MethodPresenter.new service_presenter, api_obj, method

gapic-generator/lib/gapic/presenters/method/compute_pagination_info.rb

Lines changed: 159 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,80 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616

17+
require "gapic/model/model_error"
18+
1719
module Gapic
1820
module Presenters
1921
module Method
2022
##
21-
# Pagination info determined from the proto method
23+
# Compute-specific pagination info determined from the proto method
24+
#
25+
# This implements the Compute-specific pagination heuristic
26+
#
27+
# The following steps are followed for this heuristic:
28+
# 1. The method should not be server-streamed
29+
# 2. The request should have a page token and page size fields
30+
# 3. The response should have a next page token and contain a valid results field
31+
#
32+
# Now determining the valid results field is its own complicated sub-heuristic that should be run last.
33+
# This sub-heuristic cannot end in "not paginated". It should either determine the results field or throw an error
34+
#
35+
# The following steps are followed for this sub-heuristic:
36+
# 0. Check the exception dictionary. If the method is there as a key, use the value as the results field.
37+
# 1. If there is exactly one map field, that field is the results field.
38+
#
39+
# NB: at this point the response contains either 0 or 2 map fields
40+
#
41+
# 2. If there are no repeated fields there is no results field and we shall throw an error
42+
# 3. If there is exactly one repeated field, that field is the results field.
43+
# 4. If there are exactly 2 repeated fields, one is of message type, and the other is of type
44+
# "String", the field of message type is the results field.
45+
#
46+
# At this point there are either 2 repeated fields in wrong configuration, or 3 or more repeated
47+
# fields. The method should have been in the exception dictionary (see step 0).
48+
# Since the method is NOT in the exception dictionary we should throw an error to prevent
49+
# accidentally releasing a Compute library with incorrect pagination.
2250
#
2351
class ComputePaginationInfo
2452
include Gapic::Helpers::NamespaceHelper
53+
54+
# @private Field name for Pagination Request page token
55+
PAGE_TOKEN_NAME = "page_token"
56+
private_constant :PAGE_TOKEN_NAME
57+
# @private Field type for Pagination Request page token
58+
PAGE_TOKEN_TYPE = :TYPE_STRING
59+
private_constant :PAGE_TOKEN_TYPE
60+
61+
# @private Field names for Pagination Request page size
62+
PAGE_SIZE_NAMES = ["page_size", "max_results"].freeze
63+
private_constant :PAGE_SIZE_NAMES
64+
# @private Field types for Pagination Request page size
65+
PAGE_SIZE_TYPES = [:TYPE_UINT32, :TYPE_INT32].freeze
66+
private_constant :PAGE_SIZE_TYPES
67+
68+
# @private Field name for Pagination Response next page token
69+
NEXT_PAGE_TOKEN_NAME = "next_page_token"
70+
private_constant :NEXT_PAGE_TOKEN_NAME
71+
# @private Field type for Pagination Response next page token
72+
NEXT_PAGE_TOKEN_TYPE = :TYPE_STRING
73+
private_constant :NEXT_PAGE_TOKEN_TYPE
74+
75+
# @private A dictionary of special response messages of paginated methods and their repeated fields
76+
# Curently contains only UsableSubnetworksAggregatedList which is a paginated field with 3 repeated fields,
77+
# 2 of which are message-typed fields
78+
REPEATED_FIELD_SPECIAL_DICTIONARY = {
79+
"google.cloud.compute.v1.UsableSubnetworksAggregatedList" => "items",
80+
"google.cloud.compute.v1beta.UsableSubnetworksAggregatedList" => "items"
81+
}.freeze
82+
private_constant :REPEATED_FIELD_SPECIAL_DICTIONARY
83+
2584
##
2685
# @param proto_method [Gapic::Schema::Method] the method to derive pagination info from
2786
# @param api [Gapic::Schema::Api]
2887
#
2988
def initialize proto_method, api
3089
@api = api
90+
@method_full_name = proto_method.full_name
3191
@request = proto_method.input
3292
@response = proto_method.output
3393
@server_streaming = proto_method.server_streaming
@@ -38,7 +98,10 @@ def initialize proto_method, api
3898
#
3999
# @return [Boolean]
40100
def paged?
41-
!server_streaming? && paged_request? && paged_response?
101+
paged_candidate = !server_streaming? && paged_request? && paged_response_candidate?
102+
103+
# `paged_response?` can raise and should be evaluated last
104+
paged_candidate && paged_response?
42105
end
43106

44107
##
@@ -111,7 +174,7 @@ def paged_request?
111174
def request_page_token_field
112175
# Has a String page_token field which specifies the actual (next) page to retrieve.
113176
@request_page_token_field ||= @request.fields.find do |f|
114-
f.name == "page_token" && f.type == :TYPE_STRING
177+
f.name == PAGE_TOKEN_NAME && f.type == PAGE_TOKEN_TYPE
115178
end
116179
end
117180

@@ -123,14 +186,8 @@ def request_page_token_field
123186
def request_page_size_field
124187
@request_page_size_field ||=
125188
begin
126-
page_size_names = ["page_size", "max_results"]
127-
128-
# Has the int32 page_size or int32 max_results field
129-
# which defines the maximum number of paginated resources to return in the response.
130-
page_size_types = [:TYPE_UINT32, :TYPE_INT32]
131-
132189
field = @request.fields.find do |f|
133-
page_size_names.include?(f.name) && page_size_types.include?(f.type)
190+
PAGE_SIZE_NAMES.include?(f.name) && PAGE_SIZE_TYPES.include?(f.type)
134191
end
135192

136193
field
@@ -143,48 +200,126 @@ def request_page_size_field
143200
#
144201
# @return [Boolean]
145202
def paged_response?
146-
# Has the string next_page_token field to be used in the next request as page_token to retrieve the next page.
147-
# Has only one repeated or map<string, ?> field containing a list of paginated resources.
148-
!response_next_page_token_field.nil? && !response_results_field.nil?
203+
# Has the string next_page_token field to be used in the next request as
204+
# page_token to retrieve the next page.
205+
# Passes the heuristic for paginated response
206+
# Order is important here, since paginated response heuristic can raise and should be evaluated last
207+
paged_response_candidate? && !response_results_field.nil?
208+
end
209+
210+
##
211+
# Whether the response message for the REGAPIC rpc satisfies the criteria
212+
# to be a candidate for pagination. This is intentinally split from evaluating
213+
# the paged response heuristic since that heuristic can raise.
214+
#
215+
# @return [Boolean]
216+
def paged_response_candidate?
217+
# Has the string next_page_token field to be used in the next request as
218+
# page_token to retrieve the next page.
219+
!response_next_page_token_field.nil?
149220
end
150221

151222
##
152223
# The field in the response that holds a next page_token
153224
#
154225
# @return [Gapic::Schema::Field, nil]
155226
def response_next_page_token_field
156-
# Has the string next_page_token field to be used in the next request as page_token to retrieve the next page.
227+
# Has the string next_page_token field to be used in the next request as
228+
# page_token to retrieve the next page.
157229
@response_next_page_token_field ||= @response.fields.find do |f|
158-
f.name == "next_page_token" && f.type == :TYPE_STRING
230+
f.name == NEXT_PAGE_TOKEN_NAME && f.type == NEXT_PAGE_TOKEN_TYPE
159231
end
160232
end
161233

234+
235+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
236+
# The heuristic in `response_results_field` would be more confusing if spread across several methods
237+
162238
##
163239
# The field in the response that holds the results
164-
# For Regapic can be either a vanilla repeated field or a map
240+
# For Compute Regapic can be either a vanilla repeated field or a map
165241
#
166242
# @return [Gapic::Schema::Field, nil]
167243
def response_results_field
244+
# This sub-heuristic cannot end in "not paginated".
245+
# It should either determine the results field or throw an error.
168246
@response_results_field ||= begin
169247
map_fields = @response.fields.find_all(&:map?)
170248
repeated_fields = @response.fields.find_all do |f|
171249
!f.map? && f.label == :LABEL_REPEATED
172250
end
173251

174-
if map_fields.count == 1
175-
# If the response message has only one map<string, ?> field
176-
# treat it as the one with paginated resources (i.e. ignore the repeated fields if any).
252+
# The following steps are followed for this sub-heuristic:
253+
# 0. Check the exception dictionary. If the method is there as key, use the value as the results field.
254+
if REPEATED_FIELD_SPECIAL_DICTIONARY.key? @response.full_name
255+
field_name = REPEATED_FIELD_SPECIAL_DICTIONARY[@response.full_name]
256+
field = map_fields.find do |f|
257+
f.name == field_name
258+
end || repeated_fields.find do |f|
259+
f.name == field_name
260+
end
261+
262+
unless field
263+
error_text = "A field of name \"#{field_name}\" is included in the special dictionary for " \
264+
"the response type \"#{@response.full_name}\". However this field is not a map " \
265+
"or repeated field. Failing, as the generator cannot continue."
266+
267+
raise ::Gapic::Model::ModelError, error_text
268+
end
269+
270+
field
271+
elsif map_fields.count == 1
272+
# 1. If there is exactly one map field, that field is the results field.
177273
map_fields.first
178-
elsif repeated_fields.count == 1 && map_fields.empty?
179-
# If the response message contains only one repeated field,
180-
# treat that field as the one containing the paginated resources.
274+
# NB: at this point the response contains either 0 or 2 map fields.
275+
elsif repeated_fields.count.zero?
276+
# 2. If there are no repeated fields there is no results field and we shall throw an error
277+
error_text = "A response message \"#{@response.full_name}\" has a next page token field and " \
278+
"is being evaluated as a candidate for pagination. However it has " \
279+
"#{map_fields.count} (!= 1) map fields and no repeated fields. " \
280+
"Failing, as the generator should not continue."
281+
282+
raise ::Gapic::Model::ModelError, error_text
283+
elsif repeated_fields.count == 1
284+
# 3. If there is exactly one repeated field, that field is the results field.
181285
repeated_fields.first
286+
elsif repeated_fields.count == 2
287+
# 4. If there are exactly 2 repeated fields, one is of message type, and the other is of type
288+
# "String", the field of message type is the results field.
289+
pagination_field = repeated_fields.find(&:message?)
290+
string_field = repeated_fields.find { |f| f.type == :TYPE_STRING }
291+
292+
unless pagination_field && string_field
293+
# At this point if there are 2 repeated fields of different configuration, or 3 or more repeated
294+
# fields the method should have been in the exception dictionary (see step 0).
295+
error_text = "A response message \"#{@response.full_name}\" has a next page token " \
296+
"field and is being evaluated as a candidate for pagination. However it should have " \
297+
"a configuration of exactly 2 repeated fields, one is of message type, and the other " \
298+
"of type \"String\". Failing, as the generator cannot continue. \n" \
299+
"As a developer, please evaluate the message that fails this heuristic and either " \
300+
"change the heuristic or add the message to the special dictionary."
301+
302+
raise ::Gapic::Model::ModelError, error_text
303+
end
304+
305+
pagination_field
306+
else
307+
# At this point there are 3 or more repeated fields, and the method should have been in the
308+
# exception dictionary (see step 0).
309+
error_text = "A response message \"#{@response.full_name}\" has a next page token " \
310+
"field and is being evaluated as a candidate for pagination. However it has " \
311+
"#{repeated_fields.count} (>= 3) repeated fields, and not in the special dictionary " \
312+
"for exceptions. Failing, as the generator cannot continue. \n" \
313+
"As a developer, please evaluate the message that fails this heuristic and either " \
314+
"change the heuristic or add the message to the special dictionary."
315+
316+
raise ::Gapic::Model::ModelError, error_text
182317
end
183-
# If the response message contains more than one repeated field or does not have repeated fields at all
184-
# but has more than one map<string, ?> field, do not generate any paginated methods for such rpc.
185318
end
186319
end
187320

321+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
322+
188323
# @private
189324
FIELD_TYPE_MAPPING = {
190325
TYPE_DOUBLE: "::Float",

0 commit comments

Comments
 (0)