1414# See the License for the specific language governing permissions and
1515# limitations under the License.
1616
17+ require "gapic/model/model_error"
18+
1719module 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