gapic-common 0.10.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,15 +32,17 @@ module Gapic
32
32
  # @param credentials [Google::Auth::Credentials]
33
33
  # Credentials to send with calls in form of a googleauth credentials object.
34
34
  # (see the [googleauth docs](https://googleapis.dev/ruby/googleauth/latest/index.html))
35
+ # @param numeric_enums [Boolean] Whether to signal the server to JSON-encode enums as ints
35
36
  #
36
37
  # @yield [Faraday::Connection]
37
38
  #
38
- def initialize endpoint:, credentials:
39
+ def initialize endpoint:, credentials:, numeric_enums: false
39
40
  @endpoint = endpoint
40
41
  @endpoint = "https://#{endpoint}" unless /^https?:/.match? endpoint
41
42
  @endpoint = @endpoint.sub %r{/$}, ""
42
43
 
43
44
  @credentials = credentials
45
+ @numeric_enums = numeric_enums
44
46
 
45
47
  @connection = Faraday.new url: @endpoint do |conn|
46
48
  conn.headers = { "Content-Type" => "application/json" }
@@ -58,8 +60,8 @@ module Gapic
58
60
  #
59
61
  # @param uri [String] uri to send this request to
60
62
  # @param params [Hash] query string parameters for the request
61
- # @param options [::Gapic::CallOptions] gapic options to be applied to the REST call.
62
- # Currently only timeout and headers are supported.
63
+ # @param options [::Gapic::CallOptions,Hash] gapic options to be applied
64
+ # to the REST call. Currently only timeout and headers are supported.
63
65
  # @return [Faraday::Response]
64
66
  def make_get_request uri:, params: {}, options: {}
65
67
  make_http_request :get, uri: uri, body: nil, params: params, options: options
@@ -70,8 +72,8 @@ module Gapic
70
72
  #
71
73
  # @param uri [String] uri to send this request to
72
74
  # @param params [Hash] query string parameters for the request
73
- # @param options [::Gapic::CallOptions] gapic options to be applied to the REST call.
74
- # Currently only timeout and headers are supported.
75
+ # @param options [::Gapic::CallOptions,Hash] gapic options to be applied
76
+ # to the REST call. Currently only timeout and headers are supported.
75
77
  # @return [Faraday::Response]
76
78
  def make_delete_request uri:, params: {}, options: {}
77
79
  make_http_request :delete, uri: uri, body: nil, params: params, options: options
@@ -83,8 +85,8 @@ module Gapic
83
85
  # @param uri [String] uri to send this request to
84
86
  # @param body [String] a body to send with the request, nil for requests without a body
85
87
  # @param params [Hash] query string parameters for the request
86
- # @param options [::Gapic::CallOptions] gapic options to be applied to the REST call.
87
- # Currently only timeout and headers are supported.
88
+ # @param options [::Gapic::CallOptions,Hash] gapic options to be applied
89
+ # to the REST call. Currently only timeout and headers are supported.
88
90
  # @return [Faraday::Response]
89
91
  def make_patch_request uri:, body:, params: {}, options: {}
90
92
  make_http_request :patch, uri: uri, body: body, params: params, options: options
@@ -96,8 +98,8 @@ module Gapic
96
98
  # @param uri [String] uri to send this request to
97
99
  # @param body [String] a body to send with the request, nil for requests without a body
98
100
  # @param params [Hash] query string parameters for the request
99
- # @param options [::Gapic::CallOptions] gapic options to be applied to the REST call.
100
- # Currently only timeout and headers are supported.
101
+ # @param options [::Gapic::CallOptions,Hash] gapic options to be applied
102
+ # to the REST call. Currently only timeout and headers are supported.
101
103
  # @return [Faraday::Response]
102
104
  def make_post_request uri:, body: nil, params: {}, options: {}
103
105
  make_http_request :post, uri: uri, body: body, params: params, options: options
@@ -109,8 +111,8 @@ module Gapic
109
111
  # @param uri [String] uri to send this request to
110
112
  # @param body [String] a body to send with the request, nil for requests without a body
111
113
  # @param params [Hash] query string parameters for the request
112
- # @param options [::Gapic::CallOptions] gapic options to be applied to the REST call.
113
- # Currently only timeout and headers are supported.
114
+ # @param options [::Gapic::CallOptions,Hash] gapic options to be applied
115
+ # to the REST call. Currently only timeout and headers are supported.
114
116
  # @return [Faraday::Response]
115
117
  def make_put_request uri:, body: nil, params: {}, options: {}
116
118
  make_http_request :put, uri: uri, body: body, params: params, options: options
@@ -123,17 +125,84 @@ module Gapic
123
125
  # @param uri [String] uri to send this request to
124
126
  # @param body [String, nil] a body to send with the request, nil for requests without a body
125
127
  # @param params [Hash] query string parameters for the request
126
- # @param options [::Gapic::CallOptions] gapic options to be applied to the REST call.
127
- # Currently only timeout and headers are supported.
128
+ # @param options [::Gapic::CallOptions,Hash] gapic options to be applied to the REST call.
129
+ # @param is_server_streaming [Boolean] flag if method is streaming
130
+ # @yieldparam chunk [String] The chunk of data received during server streaming.
128
131
  # @return [Faraday::Response]
129
- def make_http_request verb, uri:, body:, params:, options:
132
+ def make_http_request verb, uri:, body:, params:, options:, is_server_streaming: false, &block
133
+ # Converts hash and nil to an options object
134
+ options = ::Gapic::CallOptions.new(**options.to_h) unless options.is_a? ::Gapic::CallOptions
135
+ deadline = calculate_deadline options
136
+ retried_exception = nil
137
+ next_timeout = get_timeout deadline
138
+
139
+ begin
140
+ base_make_http_request(verb,
141
+ uri: uri,
142
+ body: body,
143
+ params: params,
144
+ metadata: options.metadata,
145
+ timeout: next_timeout,
146
+ is_server_streaming: is_server_streaming,
147
+ &block)
148
+ rescue ::Faraday::Error => e
149
+ next_timeout = get_timeout deadline
150
+
151
+ if next_timeout&.positive? && options.retry_policy.call(e)
152
+ retried_exception = e
153
+ retry
154
+ end
155
+
156
+ unless next_timeout&.positive?
157
+ raise Gapic::GRPC::DeadlineExceededError.new e.message, root_cause: retried_exception
158
+ end
159
+
160
+ raise e
161
+ end
162
+ end
163
+
164
+ private
165
+
166
+ ##
167
+ # @private
168
+ # Sends a http request via Faraday
169
+ # @param verb [Symbol] http verb
170
+ # @param uri [String] uri to send this request to
171
+ # @param body [String, nil] a body to send with the request, nil for requests without a body
172
+ # @param params [Hash] query string parameters for the request
173
+ # @param metadata [Hash] additional headers for the request
174
+ # @param is_server_streaming [Boolean] flag if method is streaming
175
+ # @yieldparam chunk [String] The chunk of data received during server streaming.
176
+ # @return [Faraday::Response]
177
+ def base_make_http_request verb, uri:, body:, params:, metadata:, timeout:, is_server_streaming: false
178
+ if @numeric_enums && (!params.key?("$alt") || params["$alt"] == "json")
179
+ params = params.merge({ "$alt" => "json;enum-encoding=int" })
180
+ end
181
+
130
182
  @connection.send verb, uri do |req|
131
183
  req.params = params if params.any?
132
184
  req.body = body unless body.nil?
133
- req.headers = req.headers.merge options.metadata
134
- req.options.timeout = options.timeout if options.timeout&.positive?
185
+ req.headers = req.headers.merge metadata
186
+ req.options.timeout = timeout if timeout&.positive?
187
+ if is_server_streaming
188
+ req.options.on_data = proc do |chunk, _overall_received_bytes|
189
+ yield chunk
190
+ end
191
+ end
135
192
  end
136
193
  end
194
+
195
+ def calculate_deadline options
196
+ return if options.timeout.nil?
197
+ return if options.timeout.negative?
198
+
199
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) + options.timeout
200
+ end
201
+
202
+ def get_timeout deadline
203
+ return if deadline.nil?
204
+ deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
205
+ end
137
206
  end
138
207
  end
139
208
  end
@@ -19,16 +19,30 @@ module Gapic
19
19
  module Rest
20
20
  # Gapic REST exception class
21
21
  class Error < ::Gapic::Common::Error
22
- # @return [Integer] the http status code for the error
22
+ # @return [Integer, nil] the http status code for the error
23
23
  attr_reader :status_code
24
+ # @return [Object, nil] the text representation of status as parsed from the response body
25
+ attr_reader :status
26
+ # @return [Object, nil] the details as parsed from the response body
27
+ attr_reader :details
28
+ # @return [Object, nil] the headers of the REST error
29
+ attr_reader :headers
30
+ # The Cloud error wrapper expect to see a `header` property
31
+ alias header headers
24
32
 
25
33
  ##
26
34
  # @param message [String, nil] error message
27
35
  # @param status_code [Integer, nil] HTTP status code of this error
36
+ # @param status [String, nil] The text representation of status as parsed from the response body
37
+ # @param details [Object, nil] Details data of this error
38
+ # @param headers [Object, nil] Http headers data of this error
28
39
  #
29
- def initialize message, status_code
30
- @status_code = status_code
40
+ def initialize message, status_code, status: nil, details: nil, headers: nil
31
41
  super message
42
+ @status_code = status_code
43
+ @status = status
44
+ @details = details
45
+ @headers = headers
32
46
  end
33
47
 
34
48
  class << self
@@ -37,36 +51,85 @@ module Gapic
37
51
  # it tries to parse and set a detailed message and an error code from
38
52
  # from the Google Cloud's response body
39
53
  #
54
+ # @param err [Faraday::Error] the Faraday error to wrap
55
+ #
56
+ # @return [ Gapic::Rest::Error]
40
57
  def wrap_faraday_error err
41
58
  message = err.message
42
59
  status_code = err.response_status
60
+ status = nil
61
+ details = nil
62
+ headers = err.response_headers
43
63
 
44
64
  if err.response_body
45
- msg, code = try_parse_from_body err.response_body
65
+ msg, code, status, details = try_parse_from_body err.response_body
46
66
  message = "An error has occurred when making a REST request: #{msg}" unless msg.nil?
47
67
  status_code = code unless code.nil?
48
68
  end
49
69
 
50
- Gapic::Rest::Error.new message, status_code
70
+ Gapic::Rest::Error.new message, status_code, status: status, details: details, headers: headers
51
71
  end
52
72
 
53
73
  private
54
74
 
55
75
  ##
76
+ # @private
56
77
  # Tries to get the error information from the JSON bodies
57
78
  #
58
79
  # @param body_str [String]
59
- # @return [Array(String, String)]
80
+ # @return [Array(String, String, String, String)]
60
81
  def try_parse_from_body body_str
61
82
  body = JSON.parse body_str
62
- return [nil, nil] unless body && body["error"].is_a?(Hash)
63
83
 
64
- message = body["error"]["message"]
65
- code = body["error"]["code"]
84
+ unless body.is_a?(::Hash) && body&.key?("error") && body["error"].is_a?(::Hash)
85
+ return [nil, nil, nil, nil]
86
+ end
87
+ error = body["error"]
88
+
89
+ message = error["message"] if error.key? "message"
90
+ code = error["code"] if error.key? "code"
91
+ status = error["status"] if error.key? "status"
92
+
93
+ details = parse_details error["details"] if error.key? "details"
66
94
 
67
- [message, code]
95
+ [message, code, status, details]
68
96
  rescue JSON::ParserError
69
- [nil, nil]
97
+ [nil, nil, nil, nil]
98
+ end
99
+
100
+ ##
101
+ # @private
102
+ # Parses the details data, trying to extract the Protobuf.Any objects
103
+ # from it, if it's an array of hashes. Otherwise returns it as is.
104
+ #
105
+ # @param details [Object, nil] the details object
106
+ #
107
+ # @return [Object, nil]
108
+ def parse_details details
109
+ # For rest errors details will contain json representations of `Protobuf.Any`
110
+ # decoded into hashes. If it's not an array, of its elements are not hashes,
111
+ # it's some other case
112
+ return details unless details.is_a? ::Array
113
+
114
+ details.map do |detail_instance|
115
+ next detail_instance unless detail_instance.is_a? ::Hash
116
+ # Next, parse detail_instance into a Proto message.
117
+ # There are three possible issues for the JSON->Any->message parsing
118
+ # - json decoding fails
119
+ # - the json belongs to a proto message type we don't know about
120
+ # - any unpacking fails
121
+ # If we hit any of these three issues we'll just return the original hash
122
+ begin
123
+ any = ::Google::Protobuf::Any.decode_json detail_instance.to_json
124
+ klass = ::Google::Protobuf::DescriptorPool.generated_pool.lookup(any.type_name)&.msgclass
125
+ next detail_instance if klass.nil?
126
+ unpack = any.unpack klass
127
+ next detail_instance if unpack.nil?
128
+ unpack
129
+ rescue ::Google::Protobuf::ParseError
130
+ detail_instance
131
+ end
132
+ end.compact
70
133
  end
71
134
  end
72
135
  end
@@ -18,22 +18,43 @@ module Gapic
18
18
  # Registers the middleware with Faraday
19
19
  module FaradayMiddleware
20
20
  ##
21
+ # @private
21
22
  # Request middleware that constructs the Authorization HTTP header
22
23
  # using ::Google::Auth::Credentials
23
24
  #
24
25
  class GoogleAuthorization < Faraday::Middleware
25
26
  ##
27
+ # @private
26
28
  # @param app [#call]
27
- # @param credentials [::Google::Auth::Credentials]
29
+ # @param credentials [Google::Auth::Credentials, Signet::OAuth2::Client, Symbol, Proc]
30
+ # Provides the means for authenticating requests made by
31
+ # the client. This parameter can be many types:
32
+ # * A `Google::Auth::Credentials` uses a the properties of its represented keyfile for authenticating requests
33
+ # made by this client.
34
+ # * A `Signet::OAuth2::Client` object used to apply the OAuth credentials.
35
+ # * A `Proc` will be used as an updater_proc for the auth token.
36
+ # * A `Symbol` is treated as a signal that authentication is not required.
37
+ #
28
38
  def initialize app, credentials
29
- @credentials = credentials
39
+ @updater_proc = case credentials
40
+ when Symbol
41
+ credentials
42
+ else
43
+ updater_proc = credentials.updater_proc if credentials.respond_to? :updater_proc
44
+ updater_proc ||= credentials if credentials.is_a? Proc
45
+ raise ArgumentError, "invalid credentials (#{credentials.class})" if updater_proc.nil?
46
+ updater_proc
47
+ end
30
48
  super app
31
49
  end
32
50
 
51
+ # @private
33
52
  # @param env [Faraday::Env]
34
53
  def call env
35
- auth_hash = @credentials.client.apply({})
36
- env.request_headers["Authorization"] = auth_hash[:authorization]
54
+ unless @updater_proc.is_a? Symbol
55
+ auth_hash = @updater_proc.call({})
56
+ env.request_headers["Authorization"] = auth_hash[:authorization]
57
+ end
37
58
 
38
59
  @app.call env
39
60
  end
@@ -15,6 +15,7 @@
15
15
  module Gapic
16
16
  module Rest
17
17
  class GrpcTranscoder
18
+ ##
18
19
  # @private
19
20
  # A single binding for GRPC-REST transcoding of a request
20
21
  # It includes a uri template with bound field parameters, a HTTP method type,
@@ -25,7 +26,8 @@ module Gapic
25
26
  # @attribute [r] template
26
27
  # @return [String] The URI template for the request.
27
28
  # @attribute [r] field_bindings
28
- # @return [Array<FieldBinding>] The field bindings for the URI template variables.
29
+ # @return [Array<Gapic::Rest::GrpcTranscoder::HttpBinding::FieldBinding>]
30
+ # The field bindings for the URI template variables.
29
31
  # @attribute [r] body
30
32
  # @return [String] The body template for the request.
31
33
  class HttpBinding
@@ -41,6 +43,57 @@ module Gapic
41
43
  @body = body
42
44
  end
43
45
 
46
+ ##
47
+ # @private
48
+ # Creates a new HttpBinding.
49
+ #
50
+ # @param uri_method [Symbol] The rest verb for the binding.
51
+ # @param uri_template [String] The string with uri template for the binding.
52
+ # This string will be expanded with the parameters from variable bindings.
53
+ # @param matches [Array<Array>] Variable bindings in an array. Every element
54
+ # of the array is an [Array] triplet, where:
55
+ # - the first element is a [String] field path (e.g. `foo.bar`) in the request
56
+ # to bind to
57
+ # - the second element is a [Regexp] to match the field value
58
+ # - the third element is a [Boolean] whether the slashes in the field value
59
+ # should be preserved (as opposed to escaped) when expanding the uri template.
60
+ # @param body [String, Nil] The body template, e.g. `*` or a field path.
61
+ #
62
+ # @return [Gapic::Rest::GrpcTranscoder::HttpBinding] The new binding.
63
+ def self.create_with_validation uri_method:, uri_template:, matches: [], body: nil
64
+ template = uri_template
65
+
66
+ matches.each do |name, _regex, _preserve_slashes|
67
+ unless uri_template =~ /({#{Regexp.quote name}})/
68
+ err_msg = "Binding configuration is incorrect: missing parameter in the URI template.\n" \
69
+ "Parameter `#{name}` is specified for matching but there is no corresponding parameter " \
70
+ "`{#{name}}` in the URI template."
71
+ raise ::Gapic::Common::Error, err_msg
72
+ end
73
+
74
+ template = template.gsub "{#{name}}", ""
75
+ end
76
+
77
+ if template =~ /{([a-zA-Z_.]+)}/
78
+ err_name = Regexp.last_match[1]
79
+ err_msg = "Binding configuration is incorrect: missing match configuration.\n" \
80
+ "Parameter `{#{err_name}}` is specified in the URI template but there is no " \
81
+ "corresponding match configuration for `#{err_name}`."
82
+ raise ::Gapic::Common::Error, err_msg
83
+ end
84
+
85
+ if body&.include? "."
86
+ raise ::Gapic::Common::Error,
87
+ "Provided body template `#{body}` points to a field in a sub-message. This is not supported."
88
+ end
89
+
90
+ field_bindings = matches.map do |name, regex, preserve_slashes|
91
+ HttpBinding::FieldBinding.new name, regex, preserve_slashes
92
+ end
93
+
94
+ HttpBinding.new uri_method, uri_template, field_bindings, body
95
+ end
96
+
44
97
  # A single binding for a field of a request message.
45
98
  # @attribute [r] field_path
46
99
  # @return [String] The path of the bound field, e.g. `foo.bar`.
@@ -44,36 +44,11 @@ module Gapic
44
44
  #
45
45
  # @return [Gapic::Rest::GrpcTranscoder] The updated transcoder.
46
46
  def with_bindings uri_method:, uri_template:, matches: [], body: nil
47
- template = uri_template
48
-
49
- matches.each do |name, _regex, _preserve_slashes|
50
- unless uri_template =~ /({#{Regexp.quote name}})/
51
- err_msg = "Binding configuration is incorrect: missing parameter in the URI template.\n" \
52
- "Parameter `#{name}` is specified for matching but there is no corresponding parameter" \
53
- " `{#{name}}` in the URI template."
54
- raise ::Gapic::Common::Error, err_msg
55
- end
56
-
57
- template = template.gsub "{#{name}}", ""
58
- end
59
-
60
- if template =~ /{([a-zA-Z_.]+)}/
61
- err_name = Regexp.last_match[1]
62
- err_msg = "Binding configuration is incorrect: missing match configuration.\n" \
63
- "Parameter `{#{err_name}}` is specified in the URI template but there is no" \
64
- " corresponding match configuration for `#{err_name}`."
65
- raise ::Gapic::Common::Error, err_msg
66
- end
67
-
68
- if body&.include? "."
69
- raise ::Gapic::Common::Error,
70
- "Provided body template `#{body}` points to a field in a sub-message. This is not supported."
71
- end
72
-
73
- field_bindings = matches.map do |name, regex, preserve_slashes|
74
- HttpBinding::FieldBinding.new name, regex, preserve_slashes
75
- end
76
- GrpcTranscoder.new @bindings + [HttpBinding.new(uri_method, uri_template, field_bindings, body)]
47
+ binding = HttpBinding.create_with_validation(uri_method: uri_method,
48
+ uri_template: uri_template,
49
+ matches: matches,
50
+ body: body)
51
+ GrpcTranscoder.new @bindings + [binding]
77
52
  end
78
53
 
79
54
  ##
@@ -108,12 +83,9 @@ module Gapic
108
83
  uri_values = bind_uri_values! http_binding, request_hash
109
84
  next if uri_values.any? { |_, value| value.nil? }
110
85
 
111
- if http_binding.body && http_binding.body != "*"
112
- # Note that the body template can only point to a top-level field,
113
- # so there is no need to split the path.
114
- body_binding_camel = camel_name_for http_binding.body
115
- next unless request_hash.key? body_binding_camel
116
- end
86
+ # Note that the body template can only point to a top-level field,
87
+ # so there is no need to split the path.
88
+ next if http_binding.body && http_binding.body != "*" && !(request.respond_to? http_binding.body.to_sym)
117
89
 
118
90
  method = http_binding.method
119
91
  uri = expand_template http_binding.template, uri_values
@@ -145,11 +117,7 @@ module Gapic
145
117
  field_value = extract_scalar_value! request_hash, field_path_camel, field_binding.regex
146
118
 
147
119
  if field_value
148
- field_value = if field_binding.preserve_slashes
149
- field_value.split("/").map { |segment| percent_escape(segment) }.join("/")
150
- else
151
- percent_escape field_value
152
- end
120
+ field_value = field_value.split("/").map { |segment| percent_escape(segment) }.join("/")
153
121
  end
154
122
 
155
123
  [field_binding.field_path, field_value]
@@ -186,9 +154,15 @@ module Gapic
186
154
  #
187
155
  # The `request_hash_without_uri` at this point was mutated to delete these fields.
188
156
  #
189
- # Note that the body template can only point to a top-level field
190
- request_hash_without_uri.delete camel_name_for body_template
191
- body = request.send(body_template.to_sym).to_json(emit_defaults: true)
157
+ # Note 1: body template can only point to a top-level field.
158
+ # Note 2: The field that body template points to can be null, in which case
159
+ # an empty string should be sent. E.g. `Compute.Projects.SetUsageExportBucket`.
160
+ request_body_field = request.send body_template.to_sym if request.respond_to? body_template.to_sym
161
+ if request_body_field
162
+ request_hash_without_uri.delete camel_name_for body_template
163
+ body = request_body_field.to_json emit_defaults: true
164
+ end
165
+
192
166
  query_params = build_query_params request_hash_without_uri
193
167
  else
194
168
  query_params = build_query_params request_hash_without_uri
@@ -0,0 +1,101 @@
1
+ # Copyright 2022 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "json"
16
+
17
+ module Gapic
18
+ module Rest
19
+ ##
20
+ # A class to provide the Enumerable interface to the response of a REST server-streaming dmethod.
21
+ #
22
+ # ServerStream provides the enumerations over the individual response messages within the stream.
23
+ #
24
+ # @example normal iteration over resources.
25
+ # server_stream.each { |response| puts response }
26
+ #
27
+ class ServerStream
28
+ include Enumerable
29
+
30
+ ##
31
+ # Initializes ServerStream object.
32
+ #
33
+ # @param message_klass [Class]
34
+ # @param json_enumerator [Enumerator<String>]
35
+ def initialize message_klass, json_enumerator
36
+ @json_enumerator = json_enumerator
37
+ @obj = ""
38
+ @message_klass = message_klass
39
+ @ready_objs = [] # List of strings
40
+ end
41
+
42
+ ##
43
+ # Iterate over JSON objects in the streamed response.
44
+ #
45
+ # @yield [Object] Gives one complete Message object.
46
+ #
47
+ # @return [Enumerator] if no block is provided
48
+ #
49
+ def each
50
+ return enum_for :each unless block_given?
51
+
52
+ loop do
53
+ while @ready_objs.length.zero?
54
+ begin
55
+ chunk = @json_enumerator.next
56
+ next unless chunk
57
+ next_json! chunk
58
+ rescue StopIteration
59
+ dangling_content = @obj.strip
60
+ error_expl = "Dangling content left after iterating through the stream. " \
61
+ "This means that not all content was received or parsed correctly. " \
62
+ "It is likely a result of server or network error."
63
+ error_text = "#{error_expl}\n Content left unparsed: #{dangling_content}"
64
+
65
+ raise Gapic::Common::Error, error_text unless dangling_content.empty?
66
+ return
67
+ end
68
+ end
69
+ yield @message_klass.decode_json @ready_objs.shift, ignore_unknown_fields: true
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ ##
76
+ # Builds the next JSON object of the server stream from chunk.
77
+ #
78
+ # @param chunk [String] Contains (partial) JSON object
79
+ #
80
+ def next_json! chunk
81
+ chunk.chars.each do |char|
82
+ # Invariant: @obj is always either a part of a single JSON object or the entire JSON object.
83
+ # Hence, it's safe to strip whitespace, commans and array brackets. These characters
84
+ # are only added before @obj is a complete JSON object and essentially can be flushed.
85
+ next if @obj.empty? && char != "{"
86
+ @obj += char
87
+ next unless char == "}"
88
+ begin
89
+ # Two choices here: append a Ruby object into
90
+ # ready_objs or a string. Going with the latter here.
91
+ JSON.parse @obj
92
+ @ready_objs.append @obj
93
+ @obj = ""
94
+ rescue JSON::ParserError
95
+ next
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,72 @@
1
+ # Copyright 2022 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ module Gapic
17
+ module Rest
18
+ ##
19
+ # @private
20
+ # A class to provide the Enumerable interface to an incoming stream of data.
21
+ #
22
+ # ThreadedEnumerator provides the enumerations over the individual chunks of data received from the server.
23
+ #
24
+ # @example normal iteration over resources.
25
+ # chunk = threaded_enumerator.next
26
+ #
27
+ # @attribute [r] in_q
28
+ # @return [Queue] Input queue.
29
+ # @attribute [r] out_q
30
+ # @return [Queue] Output queue.
31
+ class ThreadedEnumerator
32
+ attr_reader :in_q
33
+ attr_reader :out_q
34
+
35
+ # Spawns a new thread and does appropriate clean-up
36
+ # in case thread fails. Propagates exception back
37
+ # to main thread.
38
+ #
39
+ # @yieldparam in_q[Queue] input queue
40
+ # @yieldparam out_q[Queue] output queue
41
+ def initialize
42
+ @in_q = Queue.new
43
+ @out_q = Queue.new
44
+
45
+ Thread.new do
46
+ yield @in_q, @out_q
47
+ @out_q.enq nil
48
+ rescue StandardError => e
49
+ @out_q.push e
50
+ end
51
+ end
52
+
53
+ def next
54
+ @in_q.enq :next
55
+ chunk = @out_q.deq
56
+
57
+ if chunk.is_a? StandardError
58
+ @out_q.close
59
+ @in_q.close
60
+ raise chunk
61
+ end
62
+
63
+ if chunk.nil?
64
+ @out_q.close
65
+ @in_q.close
66
+ raise StopIteration
67
+ end
68
+ chunk
69
+ end
70
+ end
71
+ end
72
+ end