gapic-common 0.10.0 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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