gapic-common 1.4.0 → 1.5.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.
@@ -0,0 +1,428 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2026 Google LLC
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ # rubocop:disable Metrics/ModuleLength
18
+
19
+ module Gapic
20
+ module Rest
21
+ module ResumableUpload
22
+ ##
23
+ # @private
24
+ # Configuration members shared by {StartUploadConfig} and {ResumeUploadConfig}, in the order both
25
+ # definitions splat them.
26
+ #
27
+ # The two config types are deliberately *flat*: {Core}, {Rules} and {Driver} read every member
28
+ # straight off `config`. Nesting the shared members inside a common object would turn every
29
+ # `config.upload_size` into `config.common.upload_size` at some thirty call sites for no behavioural
30
+ # gain, so they are spliced into each `Data.define` instead.
31
+ #
32
+ # * `stream` [IO] Binary input stream to upload. Required.
33
+ # * `upload_size` [Integer, nil] Total upload bytes if known upfront.
34
+ # * `content_type` [String, nil] MIME type of uploaded media.
35
+ # * `timeout` [Numeric, nil] Total upload timeout in seconds (zero or negative is treated as nil).
36
+ # * `control_plane_retry_policy` [Gapic::Common::RetryPolicy, Hash, nil] Retry policy for control
37
+ # commands (query, cancel).
38
+ # * `data_plane_retry_policy` [Gapic::Common::RetryPolicy, Hash, nil] Retry policy for data commands
39
+ # (upload, finalize).
40
+ # * `on_progress` [Proc, nil] Callback invoked as `->(progress)` with a {Progress} instance.
41
+ #
42
+ # Every retry policy member, here and in the per-run configs, follows the same convention: a
43
+ # {Gapic::Common::RetryPolicy} replaces the default policy outright, while a Hash overrides only the
44
+ # settings it names and leaves the remaining defaults — including retry codes and predicates — in place.
45
+ #
46
+ COMMON_MEMBERS = [
47
+ :stream,
48
+ :upload_size,
49
+ :content_type,
50
+ :timeout,
51
+ :control_plane_retry_policy,
52
+ :data_plane_retry_policy,
53
+ :on_progress
54
+ ].freeze
55
+
56
+ ##
57
+ # @private
58
+ # Header names a caller may not use in `initial_headers`, lowercased for comparison.
59
+ #
60
+ # These five headers are protocol machinery the driver owns: the protocol identifier, the command
61
+ # verb, the byte offset, and the content descriptors derived from `content_type` and `upload_size`.
62
+ # `x-goog-upload-offset` is included for completeness even though initiation never sets an offset:
63
+ # supplying an offset at initiation is meaningless and indicates a confused caller. Pass-through
64
+ # headers such as `X-Goog-Upload-Header-Content-Disposition` remain permitted.
65
+ #
66
+ # See `Driver#start_headers`, which builds the initiation headers this list protects.
67
+ #
68
+ # @return [Array<String>]
69
+ RESERVED_INITIAL_HEADERS = [
70
+ "x-goog-upload-protocol",
71
+ "x-goog-upload-command",
72
+ "x-goog-upload-offset",
73
+ "x-goog-upload-header-content-type",
74
+ "x-goog-upload-header-content-length"
75
+ ].freeze
76
+
77
+ ##
78
+ # @private
79
+ # Immutable configuration for a run that initiates a new upload session, i.e.
80
+ # {Gapic::ResumableUpload#start}.
81
+ #
82
+ # Carries {COMMON_MEMBERS} plus the members only an initiating run uses.
83
+ #
84
+ # @!attribute [r] initial_url
85
+ # @return [String] Initial endpoint URI for session initiation
86
+ # @!attribute [r] initial_body
87
+ # @return [String, nil] Request payload for session initiation
88
+ # @!attribute [r] initial_headers
89
+ # @return [Hash<String, String>] Additional headers for initiation, merged over the driver's
90
+ # own headers. Keys in {RESERVED_INITIAL_HEADERS} are rejected in any casing; use
91
+ # `content_type` and `upload_size` to shape those.
92
+ # @!attribute [r] chunk_size
93
+ # @return [Integer, nil] Requested chunk size in bytes, aligned to the granularity the server
94
+ # reports during initiation. A resumed run takes its chunk size from {ResumeUploadConfig}.
95
+ # @!attribute [r] start_retry_policy
96
+ # @return [Gapic::Common::RetryPolicy, Hash, nil] Retry policy for session initiation
97
+ #
98
+ StartUploadConfig = Data.define(
99
+ *COMMON_MEMBERS,
100
+ :initial_url,
101
+ :initial_body,
102
+ :initial_headers,
103
+ :chunk_size,
104
+ :start_retry_policy
105
+ ) do
106
+ ##
107
+ # @private
108
+ # Initializes a new upload configuration.
109
+ #
110
+ # @param initial_url [String] Initial endpoint URI for session initiation
111
+ # @param stream [IO] Binary input stream to upload
112
+ # @param initial_body [String, nil] Request payload for session initiation
113
+ # @param initial_headers [Hash<String, String>] Additional headers for initiation. Keys in
114
+ # {RESERVED_INITIAL_HEADERS} are rejected in any casing; use `content_type` and `upload_size`
115
+ # to shape those.
116
+ # @param upload_size [Integer, nil] Total upload bytes if known upfront
117
+ # @param chunk_size [Integer, nil] Requested chunk size in bytes
118
+ # @param content_type [String, nil] MIME type of uploaded media
119
+ # @param timeout [Numeric, nil] Total upload timeout in seconds (zero/negative values treated as nil)
120
+ # @param start_retry_policy [Gapic::Common::RetryPolicy, Hash, nil] Retry policy for session initiation
121
+ # @param control_plane_retry_policy [Gapic::Common::RetryPolicy, Hash, nil] Retry policy for control commands
122
+ # @param data_plane_retry_policy [Gapic::Common::RetryPolicy, Hash, nil] Retry policy for data commands
123
+ # @param on_progress [Proc, nil] Callback invoked as `->(progress)` with a {Progress} instance
124
+ # @raise [ArgumentError] If required arguments are missing or invalid
125
+ #
126
+ def initialize initial_url:,
127
+ stream:,
128
+ initial_body: nil,
129
+ initial_headers: {},
130
+ upload_size: nil,
131
+ chunk_size: nil,
132
+ content_type: nil,
133
+ timeout: nil,
134
+ start_retry_policy: nil,
135
+ control_plane_retry_policy: nil,
136
+ data_plane_retry_policy: nil,
137
+ on_progress: nil
138
+ raise ArgumentError, "initial_url is required" if initial_url.nil? || initial_url.to_s.strip.empty?
139
+ raise ArgumentError, "stream is required" if stream.nil?
140
+ reserved = (initial_headers || {}).keys.find do |key|
141
+ RESERVED_INITIAL_HEADERS.include? key.to_s.downcase
142
+ end
143
+ if reserved
144
+ raise ArgumentError,
145
+ "initial_headers must not set protocol header #{reserved.inspect}; " \
146
+ "use content_type and upload_size instead"
147
+ end
148
+
149
+ super(
150
+ initial_url: initial_url,
151
+ initial_body: initial_body,
152
+ initial_headers: initial_headers || {},
153
+ stream: stream,
154
+ upload_size: upload_size,
155
+ chunk_size: chunk_size,
156
+ content_type: content_type,
157
+ timeout: timeout,
158
+ start_retry_policy: start_retry_policy,
159
+ control_plane_retry_policy: control_plane_retry_policy,
160
+ data_plane_retry_policy: data_plane_retry_policy,
161
+ on_progress: on_progress
162
+ )
163
+ end
164
+ end
165
+
166
+ ##
167
+ # @private
168
+ # Immutable configuration for a run that resumes an existing upload session, i.e.
169
+ # {Gapic::ResumableUpload#resume}.
170
+ #
171
+ # Carries {COMMON_MEMBERS} plus the upload URL and chunk size the earlier run established. There is
172
+ # no `start_retry_policy` here: a resumed run issues no initiation request, so the member would
173
+ # always be dead.
174
+ #
175
+ # @!attribute [r] upload_url
176
+ # @return [String] Session upload URL returned by the upload backend
177
+ # @!attribute [r] chunk_size
178
+ # @return [Integer] Explicit chunk size in bytes (must be a positive integer). Server granularity is
179
+ # reported only during initiation, which a resumed run skips, so the size is carried forward from
180
+ # the earlier run rather than re-negotiated.
181
+ #
182
+ ResumeUploadConfig = Data.define(
183
+ *COMMON_MEMBERS,
184
+ :upload_url,
185
+ :chunk_size
186
+ ) do
187
+ ##
188
+ # @private
189
+ # Initializes a new upload resume configuration.
190
+ #
191
+ # @param upload_url [String] Session upload URL
192
+ # @param chunk_size [Integer] Explicit chunk size in bytes (must be a positive integer)
193
+ # @param stream [IO] Binary input stream to upload
194
+ # @param upload_size [Integer, nil] Total upload bytes if known upfront
195
+ # @param content_type [String, nil] MIME type of uploaded media
196
+ # @param timeout [Numeric, nil] Total upload timeout in seconds (zero/negative values treated as nil)
197
+ # @param control_plane_retry_policy [Gapic::Common::RetryPolicy, Hash, nil] Retry policy for control commands
198
+ # @param data_plane_retry_policy [Gapic::Common::RetryPolicy, Hash, nil] Retry policy for data commands
199
+ # @param on_progress [Proc, nil] Callback invoked as `->(progress)` with a {Progress} instance
200
+ # @raise [ArgumentError] If required arguments are missing or invalid
201
+ #
202
+ def initialize upload_url:,
203
+ chunk_size:,
204
+ stream:,
205
+ upload_size: nil,
206
+ content_type: nil,
207
+ timeout: nil,
208
+ control_plane_retry_policy: nil,
209
+ data_plane_retry_policy: nil,
210
+ on_progress: nil
211
+ raise ArgumentError, "upload_url is required" if upload_url.nil? || upload_url.to_s.strip.empty?
212
+ unless chunk_size.is_a?(Integer) && chunk_size.positive?
213
+ raise ArgumentError, "chunk_size must be a positive integer"
214
+ end
215
+ raise ArgumentError, "stream is required" if stream.nil?
216
+
217
+ super(
218
+ upload_url: upload_url,
219
+ chunk_size: chunk_size,
220
+ stream: stream,
221
+ upload_size: upload_size,
222
+ content_type: content_type,
223
+ timeout: timeout,
224
+ control_plane_retry_policy: control_plane_retry_policy,
225
+ data_plane_retry_policy: data_plane_retry_policy,
226
+ on_progress: on_progress
227
+ )
228
+ end
229
+ end
230
+
231
+ ##
232
+ # Immutable progress snapshot passed to the `on_progress` callback.
233
+ #
234
+ # The `on_progress` callback runs synchronously on the same thread as the upload protocol
235
+ # and must not block. Any exception raised inside the callback aborts the upload session
236
+ # and propagates out of {Gapic::ResumableUpload#start} or {Gapic::ResumableUpload#resume}.
237
+ #
238
+ # @!attribute [r] phase
239
+ # @return [Symbol] Current upload phase, one of {Progress::PHASES}
240
+ # @!attribute [r] bytes_uploaded
241
+ # @return [Integer] Cumulative bytes acknowledged by the server. Note that this is the
242
+ # server-confirmed offset and is not guaranteed to be monotonic — a server rewind during
243
+ # recovery can decrease this value.
244
+ # @!attribute [r] total_bytes
245
+ # @return [Integer, nil] Total upload size in bytes if known, or `nil`. Always set on the
246
+ # `:completed` phase — the total is known once the transfer finishes, even when `upload_size`
247
+ # was not supplied upfront.
248
+ #
249
+ Progress = Data.define(
250
+ :phase,
251
+ :bytes_uploaded,
252
+ :total_bytes
253
+ ) do
254
+ ##
255
+ # Initializes a new progress snapshot.
256
+ #
257
+ # @param phase [Symbol] Current upload phase, one of {Progress::PHASES}
258
+ # @param bytes_uploaded [Integer] Cumulative bytes acknowledged by the server
259
+ # @param total_bytes [Integer, nil] Total upload size in bytes if known, or nil
260
+ # @raise [ArgumentError] If the phase is not one of {Progress::PHASES}
261
+ #
262
+ def initialize phase:, bytes_uploaded:, total_bytes: nil
263
+ # Must use `self.class::` to access constants from the class scope
264
+ unless self.class::PHASES.include? phase
265
+ raise ArgumentError, "Invalid phase: #{phase.inspect}. Expected one of #{self.class::PHASES.inspect}"
266
+ end
267
+
268
+ super(
269
+ phase: phase,
270
+ bytes_uploaded: bytes_uploaded,
271
+ total_bytes: total_bytes
272
+ )
273
+ end
274
+ end
275
+
276
+ ##
277
+ # Allowed lifecycle phases for an upload session.
278
+ #
279
+ # A callback observes `:initiating`, `:uploading`, `:recovering`, `:finalizing` and `:completed`.
280
+ # `:cancelling` is reserved: cancellation is not exposed on {Gapic::ResumableUpload}, so no phase
281
+ # with that value is currently emitted.
282
+ #
283
+ # @return [Array<Symbol>]
284
+ Progress::PHASES = [:initiating, :uploading, :recovering, :finalizing, :cancelling, :completed].freeze
285
+
286
+ ##
287
+ # Immutable handle containing parameters necessary to resume an in-progress upload session.
288
+ # These parameters are provided by the server and can be persisted to resume the upload
289
+ # at a later time.
290
+ #
291
+ # @!attribute [r] upload_url
292
+ # @return [String] Upload session URL provided by the server
293
+ # @!attribute [r] chunk_size
294
+ # @return [Integer] Effective chunk size in bytes
295
+ #
296
+ ResumeHandle = Data.define(
297
+ :upload_url,
298
+ :chunk_size
299
+ ) do
300
+ ##
301
+ # Initializes a new resume handle.
302
+ #
303
+ # @param upload_url [String] Upload session URL provided by the server
304
+ # @param chunk_size [Integer] Effective chunk size in bytes
305
+ #
306
+ def initialize upload_url:, chunk_size:
307
+ super(
308
+ upload_url: upload_url,
309
+ chunk_size: chunk_size
310
+ )
311
+ end
312
+ end
313
+
314
+ ##
315
+ # @private
316
+ # Immutable state snapshot representing the current protocol progression.
317
+ #
318
+ # @!attribute [r] status
319
+ # @return [Symbol] Protocol lifecycle status, one of {Rules::STATUSES}
320
+ # @!attribute [r] upload_url
321
+ # @return [String, nil] Session upload URL returned by the upload backend
322
+ # @!attribute [r] offset
323
+ # @return [Integer] Contiguous bytes acknowledged by server
324
+ # @!attribute [r] chunk_size
325
+ # @return [Integer] Resolved effective chunk size in bytes
326
+ # @!attribute [r] chunk_granularity
327
+ # @return [Integer, nil] Alignment modulus returned by server
328
+ # @!attribute [r] in_flight_length
329
+ # @return [Integer] Byte length of in-flight chunk currently being transmitted
330
+ # @!attribute [r] last_error
331
+ # @return [StandardError, nil] Terminal exception if in an error or rejected status
332
+ # @!attribute [r] recovery_offset
333
+ # @return [Integer, nil] Server-confirmed offset when the open recovery episode began, or `nil` when no
334
+ # episode is open. A recovery episode is the run of consecutive recovery attempts during which the
335
+ # confirmed offset does not advance; every query in it after the first waits for the next backoff delay.
336
+ # See `design/resumable_upload/implementation-guide.md` section 6.2.1.
337
+ #
338
+ State = Data.define(
339
+ :status,
340
+ :upload_url,
341
+ :offset,
342
+ :chunk_size,
343
+ :chunk_granularity,
344
+ :in_flight_length,
345
+ :last_error,
346
+ :recovery_offset
347
+ ) do
348
+ ##
349
+ # @private
350
+ # Initializes a protocol state snapshot.
351
+ #
352
+ # @param status [Symbol] Protocol lifecycle status, one of {Rules::STATUSES}
353
+ # @param upload_url [String, nil] Session upload URL
354
+ # @param offset [Integer] Contiguous bytes acknowledged by server
355
+ # @param chunk_size [Integer] Resolved effective chunk size in bytes
356
+ # @param chunk_granularity [Integer, nil] Alignment modulus returned by server
357
+ # @param in_flight_length [Integer] Byte length of in-flight chunk
358
+ # @param last_error [StandardError, nil] Terminal exception
359
+ # @param recovery_offset [Integer, nil] Offset the open recovery episode began at; `nil` if none is open
360
+ #
361
+ def initialize status: :initializing,
362
+ upload_url: nil,
363
+ offset: 0,
364
+ chunk_size: Rules::DEFAULT_CHUNK_SIZE,
365
+ chunk_granularity: nil,
366
+ in_flight_length: 0,
367
+ last_error: nil,
368
+ recovery_offset: nil
369
+ super(
370
+ status: status,
371
+ upload_url: upload_url,
372
+ offset: offset,
373
+ chunk_size: chunk_size,
374
+ chunk_granularity: chunk_granularity,
375
+ in_flight_length: in_flight_length,
376
+ last_error: last_error,
377
+ recovery_offset: recovery_offset
378
+ )
379
+ end
380
+ end
381
+
382
+ ##
383
+ # @private
384
+ # Immutable decision snapshot emitted by Rules.decide.
385
+ #
386
+ # @!attribute [r] from_status
387
+ # @return [Symbol] The protocol status before the transition
388
+ # @!attribute [r] shape
389
+ # @return [Symbol] The canonical event shape
390
+ # @!attribute [r] recipe
391
+ # @return [Symbol] Selected transition recipe method name
392
+ # @!attribute [r] next_state
393
+ # @return [State] The new protocol state snapshot after transition
394
+ # @!attribute [r] instructions
395
+ # @return [Array<Object>] Emitted instructions for the Driver
396
+ #
397
+ Decision = Data.define(
398
+ :from_status,
399
+ :shape,
400
+ :recipe,
401
+ :next_state,
402
+ :instructions
403
+ ) do
404
+ ##
405
+ # @private
406
+ # Initializes a decision snapshot.
407
+ #
408
+ # @param from_status [Symbol] The protocol status before the transition
409
+ # @param shape [Symbol] The canonical event shape
410
+ # @param recipe [Symbol] Selected transition recipe method name
411
+ # @param next_state [State] Resulting protocol state snapshot
412
+ # @param instructions [Array<Object>] Emitted instructions for the Driver
413
+ #
414
+ def initialize from_status:, shape:, recipe:, next_state:, instructions: []
415
+ super(
416
+ from_status: from_status,
417
+ shape: shape,
418
+ recipe: recipe,
419
+ next_state: next_state,
420
+ instructions: instructions
421
+ )
422
+ end
423
+ end
424
+ end
425
+ end
426
+ end
427
+
428
+ # rubocop:enable Metrics/ModuleLength
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2026 Google LLC
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require "uri"
18
+
19
+ module Gapic
20
+ module Rest
21
+ module ResumableUpload
22
+ class Driver
23
+ ##
24
+ # @private
25
+ # Pure functions for redacting and abridging sensitive data and large payloads in logs.
26
+ #
27
+ module Abridge
28
+ module_function
29
+
30
+ ##
31
+ # @private
32
+ # Formats binary payload into truncated hex representation.
33
+ #
34
+ # @param data [Object, nil] Binary or string payload
35
+ # @return [String, nil] Truncated hex representation or nil
36
+ #
37
+ def bytes data
38
+ return nil if data.nil?
39
+
40
+ str = data.to_s
41
+ if str.bytesize >= 64
42
+ "#{str.byteslice(0, 32).unpack1('H*')}... <#{str.bytesize} bytes>"
43
+ else
44
+ str.unpack1 "H*"
45
+ end
46
+ end
47
+
48
+ ##
49
+ # @private
50
+ # Truncates error body to a safe log length.
51
+ #
52
+ # @param data [Object, nil] Error body payload
53
+ # @return [String, nil] UTF-8 scrubbed and truncated string
54
+ #
55
+ def error_body data
56
+ return nil if data.nil?
57
+
58
+ data.to_s.dup.force_encoding(Encoding::UTF_8).scrub[0, 512]
59
+ end
60
+
61
+ ##
62
+ # @private
63
+ # Redacts query parameter values in URLs for safe logging.
64
+ #
65
+ # @param url [Object, nil] URL string or URI
66
+ # @return [String, nil] URL with query values elided
67
+ #
68
+ def url url
69
+ return nil if url.nil?
70
+
71
+ uri = URI.parse url.to_s
72
+ if uri.query && !uri.query.empty?
73
+ elided = uri.query.split("&").map do |pair|
74
+ key, _val = pair.split "=", 2
75
+ "#{key}=<...>"
76
+ end.join "&"
77
+ uri.query = nil
78
+ return "#{uri}?#{elided}"
79
+ end
80
+ uri.to_s
81
+ rescue URI::InvalidURIError
82
+ url.to_s
83
+ end
84
+
85
+ ##
86
+ # @private
87
+ # Redacts non-protocol headers for safe logging.
88
+ #
89
+ # @param headers [Object] Headers hash
90
+ # @return [Hash<String, String>] Redacted headers
91
+ #
92
+ def headers headers
93
+ return {} unless headers.is_a? Hash
94
+
95
+ headers.each_with_object({}) do |(k, v), acc|
96
+ key_str = k.to_s
97
+ acc[key_str] = if key_str.downcase == "x-goog-upload-url"
98
+ url v
99
+ elsif key_str.downcase.start_with? "x-goog-upload-"
100
+ v
101
+ else
102
+ "<...>"
103
+ end
104
+ end
105
+ end
106
+
107
+ ##
108
+ # @private
109
+ # Converts a list of instructions into log-safe representation hashes.
110
+ #
111
+ # @param instructions [Array<Object>] List of instructions
112
+ # @return [Array<Hash>] Log-safe instruction summaries
113
+ #
114
+ def instructions instructions
115
+ instructions.map { |i| instruction i }
116
+ end
117
+
118
+ # rubocop:disable Metrics/MethodLength
119
+ ##
120
+ # @private
121
+ # Converts an instruction into a log-safe representation hash.
122
+ #
123
+ # @param instruction [Object] Instruction object
124
+ # @return [Hash] Log-safe instruction summary
125
+ #
126
+ def instruction instruction
127
+ case instruction
128
+ when Instruction::SendStart
129
+ { "type" => "SendStart", "url" => url(instruction.url) }
130
+ when Instruction::SendChunk
131
+ {
132
+ "type" => "SendChunk",
133
+ "url" => url(instruction.url),
134
+ "offset" => instruction.offset,
135
+ "length" => instruction.length,
136
+ "finalize" => instruction.finalize
137
+ }
138
+ when Instruction::SendFinalize
139
+ { "type" => "SendFinalize", "url" => url(instruction.url) }
140
+ when Instruction::SendQuery
141
+ { "type" => "SendQuery", "url" => url(instruction.url), "backoff" => instruction.backoff }
142
+ when Instruction::SendCancel
143
+ { "type" => "SendCancel", "url" => url(instruction.url) }
144
+ when Instruction::RealignBuffer
145
+ { "type" => "RealignBuffer", "serverOffset" => instruction.server_offset }
146
+ when Instruction::FillBuffer
147
+ { "type" => "FillBuffer", "targetBytesize" => instruction.target_bytesize }
148
+ when Instruction::NotifyProgress
149
+ {
150
+ "type" => "NotifyProgress",
151
+ "phase" => instruction.progress.phase.to_s,
152
+ "bytesUploaded" => instruction.progress.bytes_uploaded,
153
+ "totalBytes" => instruction.progress.total_bytes
154
+ }
155
+ when Instruction::TerminateSuccess
156
+ { "type" => "TerminateSuccess" }
157
+ when Instruction::TerminateFailure
158
+ { "type" => "TerminateFailure", "error" => instruction.error.to_s }
159
+ else
160
+ { "type" => instruction.class.name }
161
+ end
162
+ end
163
+ # rubocop:enable Metrics/MethodLength
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end