File: paged_enumerable.rb

package info (click to toggle)
ruby-gapic-common 1.3.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 392 kB
  • sloc: ruby: 2,081; makefile: 4
file content (259 lines) | stat: -rw-r--r-- 7,976 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

module Gapic
  ##
  # A class to provide the Enumerable interface to the response of a paginated method. PagedEnumerable assumes
  # response message holds a list of resources and the token to the next page.
  #
  # PagedEnumerable provides the enumerations over the resource data, and also provides the enumerations over the
  # pages themselves.
  #
  # @example normal iteration over resources.
  #   paged_enumerable.each { |resource| puts resource }
  #
  # @example per-page iteration.
  #   paged_enumerable.each_page { |page| puts page }
  #
  # @example Enumerable over pages.
  #   paged_enumerable.each_page do |page|
  #     page.each { |resource| puts resource }
  #   end
  #
  # @example more exact operations over pages.
  #   while some_condition()
  #     page = paged_enumerable.page
  #     do_something(page)
  #     break if paged_enumerable.next_page?
  #     paged_enumerable.next_page
  #   end
  #
  class PagedEnumerable
    include Enumerable

    ##
    # @attribute [r] page
    #   @return [Page] The current page object.
    attr_reader :page

    ##
    # @private
    # @param grpc_stub [Gapic::GRPC::Stub] The Gapic gRPC stub object.
    # @param method_name [Symbol] The RPC method name.
    # @param request [Object] The request object.
    # @param response [Object] The response object.
    # @param operation [::GRPC::ActiveCall::Operation] The RPC operation for the response.
    # @param options [Gapic::CallOptions] The options for making the RPC call.
    # @param format_resource [Proc] A Proc object to format the resource object. The Proc should accept response as an
    #   argument, and return a formatted resource object. Optional.
    #
    def initialize grpc_stub, method_name, request, response, operation, options, format_resource: nil
      @grpc_stub = grpc_stub
      @method_name = method_name
      @request = request
      @response = response
      @options = options
      @format_resource = format_resource
      @resource_field = nil # will be set in verify_response!

      verify_request!
      verify_response!

      @page = Page.new @response, @resource_field, operation, format_resource: @format_resource
    end

    ##
    # Iterate over the resources.
    #
    # @yield [Object] Gives the resource objects in the stream.
    #
    # @raise [RuntimeError] if it's not started yet.
    #
    def each &block
      return enum_for :each unless block_given?

      each_page do |page|
        page.each(&block)
      end
    end

    ##
    # Iterate over the pages.
    #
    # @yield [Page] Gives the pages in the stream.
    #
    # @raise if it's not started yet.
    #
    def each_page
      return enum_for :each_page unless block_given?

      loop do
        break if @page.nil?
        yield @page
        next_page!
      end
    end

    ##
    # True if it has the next page.
    #
    def next_page?
      @page.next_page_token?
    end

    ##
    # Update the response in the current page.
    #
    # @return [Page] the new page object.
    #
    def next_page!
      unless next_page?
        @page = nil
        return @page
      end

      next_request = @request.dup
      next_request.page_token = @page.next_page_token
      @grpc_stub.call_rpc @method_name, next_request, options: @options do |next_response, next_operation|
        @page = Page.new next_response, @resource_field, next_operation, format_resource: @format_resource
      end
      @page
    end
    alias next_page next_page!

    ##
    # The page token to be used for the next RPC call.
    #
    # @return [String]
    #
    def next_page_token
      @page.next_page_token
    end

    ##
    # The current response object, for the current page.
    #
    # @return [Object]
    #
    def response
      @page.response
    end

    private

    def verify_request!
      page_token = @request.class.descriptor.find do |f|
        f.name == "page_token" && f.type == :string
      end
      raise ArgumentError, "#{@request.class} must have a page_token field (String)" if page_token.nil?

      page_size = @request.class.descriptor.find do |f|
        f.name == "page_size" && [:int32, :int64].include?(f.type)
      end
      return unless page_size.nil?
      raise ArgumentError, "#{@request.class} must have a page_size field (Integer)"
    end

    def verify_response!
      next_page_token = @response.class.descriptor.find do |f|
        f.name == "next_page_token" && f.type == :string
      end
      raise ArgumentError, "#{@response.class} must have a next_page_token field (String)" if next_page_token.nil?

      # Find all repeated FieldDescriptors on the response Descriptor
      fields = @response.class.descriptor.select do |f|
        f.label == :repeated && f.type == :message
      end

      repeated_field = fields.first
      raise ArgumentError, "#{@response.class} must have one repeated field" if repeated_field.nil?

      min_repeated_field_number = fields.map(&:number).min
      if min_repeated_field_number != repeated_field.number
        raise ArgumentError, "#{@response.class} must have one primary repeated field by both position and number"
      end

      # We have the correct repeated field, save the field's name
      @resource_field = repeated_field.name
    end

    ##
    # A class to represent a page in a PagedEnumerable. This also implements Enumerable, so it can iterate over the
    # resource elements.
    #
    # @attribute [r] response
    #   @return [Object] the response object for the page.
    # @attribute [r] operation
    #   @return [::GRPC::ActiveCall::Operation] the RPC operation for the page.
    class Page
      include Enumerable
      attr_reader :response
      attr_reader :operation

      ##
      # @private
      # @param response [Object] The response object for the page.
      # @param resource_field [String] The name of the field in response which holds the resources.
      # @param operation [::GRPC::ActiveCall::Operation] the RPC operation for the page.
      # @param format_resource [Proc] A Proc object to format the resource object. The Proc should accept response as an
      #   argument, and return a formatted resource object. Optional.
      #
      def initialize response, resource_field, operation, format_resource: nil
        @response = response
        @resource_field = resource_field
        @operation = operation
        @format_resource = format_resource
      end

      ##
      # Iterate over the resources.
      #
      # @yield [Object] Gives the resource objects in the page.
      #
      def each
        return enum_for :each unless block_given?

        return if @response.nil?

        # We trust that the field exists and is an Enumerable
        @response[@resource_field].each do |resource|
          resource = @format_resource.call resource if @format_resource
          yield resource
        end
      end

      ##
      # The page token to be used for the next RPC call.
      #
      # @return [String]
      #
      def next_page_token
        return if @response.nil?

        @response.next_page_token
      end

      ##
      # Truthiness of next_page_token.
      #
      # @return [Boolean]
      #
      def next_page_token?
        return if @response.nil? # rubocop:disable Style/ReturnNilInPredicateMethodDefinition

        !@response.next_page_token.empty?
      end
    end
  end
end