gapic-common 1.3.0 → 1.6.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e4338a183319704aa9fb613e73d34b4092a94ad56f3136c2d4a0490a3218770f
4
- data.tar.gz: 3a0d1456f8c2bc573245e5c46afae6b2c83be393085bf6bebc5b28c06368d3f7
3
+ metadata.gz: b913235ab6a4508add68839ed5249a5593fe35543c12d7c5a0fd10da958d52d4
4
+ data.tar.gz: 25da91337dde92ebab37ce20f20d6795c3eb15f70480e488fb5a76e0c3e771ab
5
5
  SHA512:
6
- metadata.gz: 696026e2b9ca26c060c2b65a336f790ec193938a7740d8bdce3008c6bfae40cd80290077b717174554d7e6a1530e470f2523a5859b210c2bf9bd583e7dd72041
7
- data.tar.gz: 42979948486f1e55f990bdbcc0ee2a697ffbf3c450b4013a7ae293e0960291533bfc4356fcfb8cfdeca5eae94d97a7ff522e2b5667cd1e9611de93cf39af560e
6
+ metadata.gz: '093cefa2ecd695bc76f1a9bc0b90934f946e8953576b110abb834e60efa6b849e684e39d86d0c02ca560fb52b221261e348c5bf4ff7caf1c2b45256c7ea3ff21'
7
+ data.tar.gz: 12e1f11af015c7ee7df9dc3049123b998e3a3aa0d6fb7cf39cb5804620a9bd1883870b05526d8d1418d7162c2f883b1fdafa2f1731ef8ea216a53d8e884f3979
data/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # Release History
2
2
 
3
+ ### 1.6.1 (2026-10-29)
4
+
5
+ #### Documentation
6
+
7
+ * link to the published Ruby version support policy ([#91](https://github.com/googleapis/ruby-core-libraries/issues/91))
8
+
9
+ ### 1.6.0 (2026-10-22)
10
+
11
+ #### Features
12
+
13
+ * require grpc >= 1.84.1 ([#90](https://github.com/googleapis/ruby-core-libraries/issues/90))
14
+ #### Bug Fixes
15
+
16
+ * ignore inherited Service constants in normalize_service ([#71](https://github.com/googleapis/ruby-core-libraries/issues/71)) ([47c7e98](https://github.com/googleapis/ruby-core-libraries/commit/47c7e9854904caa59955aab31a6fdacd0af200b8)), refs [#70](https://github.com/googleapis/ruby-core-libraries/issues/70)
17
+
18
+ ### 1.5.1 (2026-09-30)
19
+
20
+ #### Bug Fixes
21
+
22
+ * tighten path template matching in the REST transcoder ([#83](https://github.com/googleapis/ruby-core-libraries/issues/83))
23
+
24
+ ### 1.5.0 (2026-09-29)
25
+
26
+ #### Features
27
+
28
+ * Resumable Media Upload functionality implementation ([#72](https://github.com/googleapis/ruby-core-libraries/issues/72))
29
+ #### Documentation
30
+
31
+ * remove stale preview notice and disclaimer from READMEs ([#77](https://github.com/googleapis/ruby-core-libraries/issues/77))
32
+
33
+ ### 1.4.1 (2026-09-28)
34
+
35
+ #### Bug Fixes
36
+
37
+ * validate path parameters and prevent traversal/injection in REST transcoder ([#67](https://github.com/googleapis/ruby-core-libraries/issues/67))
38
+
39
+ ### 1.4.0 (2026-09-23)
40
+
41
+ #### Features
42
+
43
+ * add custom predicate matching to RetryPolicy ([#59](https://github.com/googleapis/ruby-core-libraries/issues/59))
44
+ #### Bug Fixes
45
+
46
+ * require grpc >= 1.83 for post-quantum key exchange ([#73](https://github.com/googleapis/ruby-core-libraries/issues/73))
47
+ * only use emit_default for proto messages ([#75](https://github.com/googleapis/ruby-core-libraries/issues/75))
48
+
3
49
  ### 1.3.0 (2026-03-05)
4
50
 
5
51
  #### Features
data/README.md CHANGED
@@ -15,14 +15,41 @@ convenient and idiomatic API surface to callers.
15
15
 
16
16
  ## Supported Ruby Versions
17
17
 
18
- This library is supported on Ruby 3.0+.
18
+ This library is supported on Ruby 3.2+.
19
19
 
20
- Google provides official support for Ruby versions that are actively supported
21
- by Ruby Core—that is, Ruby versions that are either in normal maintenance or in
22
- security maintenance, and not end of life. Older versions of Ruby _may_ still
23
- work, but are unsupported and not recommended. See
24
- https://www.ruby-lang.org/en/downloads/branches/ for details about the Ruby
25
- support schedule.
20
+ Google provides official support for Ruby versions as described in the
21
+ [Supported Ruby versions](https://docs.cloud.google.com/ruby/getting-started/supported-ruby-versions)
22
+ policy. Older versions of Ruby _may_ still work, but are unsupported and not
23
+ recommended.
24
+
25
+ ## Post-Quantum Key Exchange
26
+
27
+ Clients built on this library negotiate the hybrid post-quantum key exchange
28
+ group `X25519MLKEM768` — classical X25519 paired with ML-KEM-768 ([NIST FIPS
29
+ 203][]) — over TLS 1.3. No application code changes are required, because the
30
+ handshake belongs to the transport layer. The two transports source that
31
+ support differently:
32
+
33
+ | Transport | Cryptographic provider | Requirement | Enforced by this gem? |
34
+ | --- | --- | --- | --- |
35
+ | gRPC | BoringSSL, vendored inside the `grpc` gem | `grpc >= 1.83` | Yes |
36
+ | REST | Host system OpenSSL, via `Net::HTTP` and Faraday | OpenSSL `>= 3.5` | No |
37
+
38
+ gRPC clients get post-quantum key exchange automatically: `gapic-common`
39
+ depends on `grpc >= 1.83`, the first release to offer `X25519MLKEM768` in the
40
+ TLS ClientHello by default.
41
+
42
+ The REST requirement cannot be expressed as a gem dependency. Ruby's `openssl`
43
+ is a default gem bound to whatever `libssl` the host provides, and ML-KEM first
44
+ ships in OpenSSL 3.5. On an older host, REST connections negotiate classical
45
+ X25519 instead — a safe fallback rather than an error, but not post-quantum. To
46
+ check the OpenSSL your Ruby is linked against:
47
+
48
+ ```sh
49
+ ruby -ropenssl -e 'puts OpenSSL::OPENSSL_LIBRARY_VERSION'
50
+ ```
51
+
52
+ [NIST FIPS 203]: https://csrc.nist.gov/pubs/fips/203/final
26
53
 
27
54
  ## Contributing
28
55
 
@@ -32,9 +59,6 @@ See the {file:CONTRIBUTING.md CONTRIBUTING} documentation for more information o
32
59
 
33
60
  ## Versioning
34
61
 
35
- This library is currently a **preview** with no guarantees of stability or support. Please get
36
- involved and let us know if you find it useful and we'll work towards a stable version.
37
-
38
- ## Disclaimer
62
+ This library follows [Semantic Versioning](http://semver.org/).
39
63
 
40
- This is not an official Google product.
64
+ This library is considered to be stable and will not have backwards-incompatible changes introduced in subsequent minor releases.
@@ -19,6 +19,12 @@ module Gapic
19
19
  ##
20
20
  # Gapic Common retry policy base class.
21
21
  #
22
+ # A policy distinguishes "set to this value" from "never set". Every setting is stored as `nil`
23
+ # until someone supplies it, and the reader for each substitutes the corresponding `DEFAULT_`
24
+ # constant on the way out, so a reader can never say which of the two happened. That distinction is
25
+ # what lets {#apply_defaults} fill in gaps without overwriting a caller's choices, and {#overrides}
26
+ # report what a caller actually asked for.
27
+ #
22
28
  class RetryPolicy
23
29
  # @return [Numeric] Default initial delay in seconds.
24
30
  DEFAULT_INITIAL_DELAY = 1
@@ -45,9 +51,17 @@ module Gapic
45
51
  # @param retry_codes [Array<String|Integer>] List of retry codes.
46
52
  # @param timeout [Numeric] Timeout threshold value in seconds.
47
53
  # @param jitter [Numeric] Random jitter added to the delay in seconds.
54
+ # @param retry_predicate [Proc, nil] The predicate to evaluate whether to retry on a given error. Optional.
55
+ # If the predicate is specified, it is run first. If it returns nil, the decision on
56
+ # whether to retry is made on the basis of retry_codes. Otherwise, the truthiness of
57
+ # the return value determines whether to retry.
48
58
  #
49
- def initialize initial_delay: nil, max_delay: nil, multiplier: nil, retry_codes: nil, timeout: nil, jitter: nil
59
+ def initialize initial_delay: nil, max_delay: nil, multiplier: nil, retry_codes: nil, timeout: nil,
60
+ jitter: nil, retry_predicate: nil
50
61
  raise ArgumentError, "jitter cannot be negative" if jitter&.negative?
62
+ if retry_predicate && !retry_predicate.respond_to?(:call)
63
+ raise ArgumentError, "retry_predicate must respond to :call"
64
+ end
51
65
 
52
66
  # Instance values are set as `nil` to determine whether values are overriden from default.
53
67
  @initial_delay = initial_delay
@@ -56,6 +70,7 @@ module Gapic
56
70
  @retry_codes = convert_codes retry_codes
57
71
  @timeout = timeout
58
72
  @jitter = jitter
73
+ @retry_predicate = retry_predicate
59
74
  start!
60
75
  end
61
76
 
@@ -89,6 +104,47 @@ module Gapic
89
104
  @jitter || DEFAULT_JITTER
90
105
  end
91
106
 
107
+ ##
108
+ # The predicate to evaluate whether to retry on a given error. Optional.
109
+ #
110
+ # When a predicate is specified:
111
+ # 1. The predicate is executed first on the error.
112
+ # 2. If it returns nil, the decision on whether to retry is made on the basis of retry_codes.
113
+ # 3. Otherwise, the truthiness of the return value determines whether to retry.
114
+ #
115
+ # @return [Proc, nil]
116
+ def retry_predicate
117
+ @retry_predicate
118
+ end
119
+
120
+ ##
121
+ # @private
122
+ # The settings this policy actually carries, as keyword arguments for {RetryPolicy.initialize}.
123
+ #
124
+ # A key is present only if that setting was explicitly supplied; a key that was never set is
125
+ # absent rather than `nil`. This is what the readers cannot tell you — {#max_delay} returns
126
+ # {DEFAULT_MAX_DELAY} whether the caller chose that number or said nothing — so it is the only
127
+ # safe way to carry one policy's settings onto another without dragging defaults along, and
128
+ # without mistaking a deliberate choice that happens to equal a default for silence.
129
+ #
130
+ # An empty `retry_codes` list counts as unset. It retries nothing, which is exactly what an
131
+ # unset list does, so there is nothing for it to carry.
132
+ #
133
+ # @return [Hash{Symbol=>Object}] Explicitly set settings only
134
+ def overrides
135
+ # Assigns nil, and so omits the key, when the list is absent or empty.
136
+ retry_codes = @retry_codes unless @retry_codes.nil? || @retry_codes.empty?
137
+ {
138
+ initial_delay: @initial_delay,
139
+ max_delay: @max_delay,
140
+ multiplier: @multiplier,
141
+ retry_codes: retry_codes,
142
+ timeout: @timeout,
143
+ jitter: @jitter,
144
+ retry_predicate: @retry_predicate
145
+ }.compact
146
+ end
147
+
92
148
  ##
93
149
  # Returns a duplicate in a non-executing state, i.e. with the deadline
94
150
  # and current delay reset.
@@ -101,7 +157,8 @@ module Gapic
101
157
  multiplier: @multiplier,
102
158
  retry_codes: @retry_codes,
103
159
  timeout: @timeout,
104
- jitter: @jitter
160
+ jitter: @jitter,
161
+ retry_predicate: @retry_predicate
105
162
  end
106
163
 
107
164
  ##
@@ -176,6 +233,13 @@ module Gapic
176
233
  # @return [Boolean] Whether this error should be retried.
177
234
  #
178
235
  def retry_error? error
236
+ # Run predicate first. If it returns nil, decision is made on the basis of retry_codes.
237
+ # Otherwise return truthiness.
238
+ if @retry_predicate.respond_to? :call
239
+ result = @retry_predicate.call error
240
+ return !!result unless result.nil?
241
+ end
242
+
179
243
  (defined?(::GRPC) && error.is_a?(::GRPC::BadStatus) && retry_codes.include?(error.code)) ||
180
244
  (error.respond_to?(:response_status) &&
181
245
  retry_codes.include?(ErrorCodes.grpc_error_for(error.response_status)))
@@ -197,11 +261,12 @@ module Gapic
197
261
  #
198
262
  def apply_defaults retry_policy
199
263
  return unless retry_policy.is_a? Hash
200
- @retry_codes ||= convert_codes retry_policy[:retry_codes]
201
- @initial_delay ||= retry_policy[:initial_delay]
202
- @multiplier ||= retry_policy[:multiplier]
203
- @max_delay ||= retry_policy[:max_delay]
204
- @jitter ||= retry_policy[:jitter]
264
+ @retry_codes ||= convert_codes retry_policy[:retry_codes]
265
+ @initial_delay ||= retry_policy[:initial_delay]
266
+ @multiplier ||= retry_policy[:multiplier]
267
+ @max_delay ||= retry_policy[:max_delay]
268
+ @jitter ||= retry_policy[:jitter]
269
+ @retry_predicate ||= retry_policy[:retry_predicate]
205
270
 
206
271
  self
207
272
  end
@@ -214,13 +279,14 @@ module Gapic
214
279
  other.multiplier == multiplier &&
215
280
  other.retry_codes == retry_codes &&
216
281
  other.timeout == timeout &&
217
- other.jitter == jitter
282
+ other.jitter == jitter &&
283
+ other.retry_predicate == retry_predicate
218
284
  end
219
285
  alias == eql?
220
286
 
221
287
  # @private Hash code
222
288
  def hash
223
- [initial_delay, max_delay, multiplier, retry_codes, timeout, jitter].hash
289
+ [initial_delay, max_delay, multiplier, retry_codes, timeout, jitter, retry_predicate].hash
224
290
  end
225
291
 
226
292
  private
@@ -14,6 +14,6 @@
14
14
 
15
15
  module Gapic
16
16
  module Common
17
- VERSION = "1.3.0".freeze
17
+ VERSION = "1.6.1".freeze
18
18
  end
19
19
  end
@@ -67,6 +67,10 @@ module Gapic
67
67
  log(Logger::DEBUG, &)
68
68
  end
69
69
 
70
+ def warn(&)
71
+ log(Logger::WARN, &)
72
+ end
73
+
70
74
  ##
71
75
  # @private
72
76
  # Builder for a log entry, passed to {StubLogger#log}.
@@ -200,12 +204,14 @@ module Gapic
200
204
  input
201
205
  when Class
202
206
  mod = input.name.split("::")[..-2].inject(Object) { |m, n| m.const_get n }
203
- if mod.const_defined? "Service"
204
- mod.const_get("Service").service_name
207
+ # inherit: false so a top-level ::Service is not mistaken for the
208
+ # generated gRPC Service sibling that REST stubs do not define.
209
+ if mod.const_defined? "Service", false
210
+ mod.const_get("Service", false).service_name
205
211
  else
206
212
  name_segments = input.name.split("::")[..-3]
207
213
  mod = name_segments.inject(Object) { |m, n| m.const_get n }
208
- name_segments.join "." if mod.const_defined? "Rest"
214
+ name_segments.join "." if mod.const_defined? "Rest", false
209
215
  end
210
216
  end
211
217
  end
@@ -287,17 +287,27 @@ module Gapic
287
287
  entry.set "requestId", request_id
288
288
  entry.message = "Sending request to #{entry.service}.#{method_name} (try #{try_number})"
289
289
  end
290
- body = body.to_s
290
+ body_str = body.to_s
291
291
  metadata = metadata.to_h rescue {}
292
- return if body.empty? && metadata.empty?
292
+ return if body_str.empty? && metadata.empty?
293
293
  stub_logger.debug do |entry|
294
294
  entry.set "requestId", request_id
295
- entry.set "request", body
295
+ entry.set "request", abridge_request_body(body_str)
296
296
  entry.set "headers", metadata
297
297
  entry.message = "(request payload as JSON)"
298
298
  end
299
299
  end
300
300
 
301
+ def abridge_request_body body_str
302
+ utf8_body = body_str.dup.force_encoding Encoding::UTF_8
303
+ if body_str.bytesize > 1024 || !utf8_body.valid_encoding?
304
+ prefix_hex = body_str.byteslice(0, 32).unpack1 "H*"
305
+ "<#{body_str.bytesize} bytes, first 32: #{prefix_hex}>"
306
+ else
307
+ utf8_body
308
+ end
309
+ end
310
+
301
311
  def log_response method_name, request_id, try_number, response, is_server_streaming
302
312
  return unless stub_logger&.enabled?
303
313
  stub_logger.info do |entry|
@@ -22,6 +22,9 @@ module Gapic
22
22
  module Rest
23
23
  # Gapic REST exception class
24
24
  class Error < ::Gapic::Common::Error
25
+ # @private
26
+ REST_ERROR_PREFIX = "An error has occurred when making a REST request".freeze
27
+
25
28
  # @return [Integer, nil] the http status code for the error
26
29
  attr_reader :status_code
27
30
  # @return [Object, nil] the text representation of status as parsed from the response body
@@ -79,7 +82,7 @@ module Gapic
79
82
 
80
83
  if err.response_body
81
84
  msg, code, status, details = try_parse_from_body err.response_body
82
- message = "An error has occurred when making a REST request: #{msg}" unless msg.nil?
85
+ message = "#{REST_ERROR_PREFIX}: #{msg}" unless msg.nil?
83
86
  status_code = code unless code.nil?
84
87
  end
85
88
 
@@ -111,12 +111,14 @@ module Gapic
111
111
  # @return [Hash{String, String}]
112
112
  # Name to value hash of the variables for the uri template expansion.
113
113
  # The values are percent-escaped with slashes potentially preserved.
114
+ # @raise [Gapic::Common::Error] If any parameter value fails path traversal validation.
114
115
  def bind_uri_values! http_binding, request_hash
115
116
  http_binding.field_bindings.to_h do |field_binding|
116
117
  field_path_camel = field_binding.field_path.split(".").map { |part| camel_name_for part }.join(".")
117
118
  field_value = extract_scalar_value! request_hash, field_path_camel, field_binding.regex
118
119
 
119
120
  if field_value
121
+ validate_field_binding! field_binding, field_value
120
122
  field_value = field_value.split("/").map { |segment| percent_escape segment }.join("/")
121
123
  end
122
124
 
@@ -124,6 +126,43 @@ module Gapic
124
126
  end
125
127
  end
126
128
 
129
+ # Validates a user-supplied parameter value bound to a standard (*) or path (**) URI template variable
130
+ # to prevent directory traversal exploits.
131
+ #
132
+ # @param field_binding [HttpBinding::FieldBinding] The field binding template metadata.
133
+ # @param field_value [String] The parameter value to validate.
134
+ # @raise [Gapic::Common::Error] If validation fails.
135
+ def validate_field_binding! field_binding, field_value
136
+ validate_path_binding! field_binding, field_value
137
+ end
138
+
139
+ # Validates standard (*) and path (**) parameters by ensuring that no segment in the parameter
140
+ # value is a directory traversal segment (. or ..).
141
+ #
142
+ # Validation Mechanism:
143
+ # 1. Splits the parameter value by slash (`/`) using `-1` limit to preserve all segments.
144
+ # 2. Checks each segment. If any segment matches `.` or `..`, it immediately raises
145
+ # a `Gapic::Common::Error`, aborting the request.
146
+ # 3. Empty segments (e.g. duplicate slashes `//` or trailing slashes `/`) are allowed
147
+ # by this validator and passed to the server, which handles normalization or returns 400.
148
+ #
149
+ # @param field_binding [HttpBinding::FieldBinding] The field binding template metadata.
150
+ # @param field_value [String] The parameter value to validate.
151
+ # @raise [Gapic::Common::Error] If validation fails.
152
+ def validate_path_binding! field_binding, field_value
153
+ segments = field_value.split("/", -1)
154
+ segments.each do |segment|
155
+ next unless [".", ".."].include? segment
156
+ if field_binding.preserve_slashes
157
+ raise ::Gapic::Common::Error,
158
+ "Value for #{field_binding.field_path} must not contain segments that are exactly '#{segment}'."
159
+ else
160
+ raise ::Gapic::Common::Error,
161
+ "Invalid value for #{field_binding.field_path} '#{segment}'."
162
+ end
163
+ end
164
+ end
165
+
127
166
  # Percent-escapes a string.
128
167
  # @param str [String] String to escape.
129
168
  # @return [String] Escaped string.
@@ -160,7 +199,16 @@ module Gapic
160
199
  request_body_field = request.send body_template.to_sym if request.respond_to? body_template.to_sym
161
200
  if request_body_field
162
201
  request_hash_without_uri.delete camel_name_for body_template
163
- body = request_body_field.to_json emit_defaults: true
202
+ # `emit_defaults` is only meaningful for, and only accepted by,
203
+ # `Google::Protobuf::MessageExts#to_json`. When the body template points at a
204
+ # scalar, enum or repeated field, `to_json` resolves to the json gem's
205
+ # `Object#to_json`, which has never accepted the keyword: json 2.x silently
206
+ # discarded unknown keywords, json 3.x raises ArgumentError.
207
+ body = if request_body_field.is_a? ::Google::Protobuf::MessageExts
208
+ request_body_field.to_json emit_defaults: true
209
+ else
210
+ request_body_field.to_json
211
+ end
164
212
  end
165
213
 
166
214
  query_params = build_query_params request_hash_without_uri
@@ -204,7 +252,7 @@ module Gapic
204
252
  # Returns nil if:
205
253
  # - the field is not found
206
254
  # - the field is a Message or an array,
207
- # - the regex does not match
255
+ # - the regex does not match the entire string representation
208
256
  # @param request_hash [Hash]
209
257
  # A hash of the GRPC request or the sub-request with the unset
210
258
  # proto3_optional fields removed.
@@ -218,7 +266,9 @@ module Gapic
218
266
 
219
267
  # Covers the case where in `foo.bar.baz`, `baz` is still a submessage or an array.
220
268
  return nil if value.is_a?(::Hash) || value.is_a?(::Array)
221
- value.to_s if value.to_s =~ regex
269
+ str = value.to_s
270
+ match = regex.match str
271
+ str if match && match.pre_match.empty? && match.post_match.empty?
222
272
  end
223
273
 
224
274
  # Finds a value in the hash by path.
@@ -0,0 +1,77 @@
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 "gapic/rest/resumable_upload/data_types"
18
+ require "gapic/rest/resumable_upload/rules"
19
+
20
+ module Gapic
21
+ module Rest
22
+ module ResumableUpload
23
+ ##
24
+ # @private
25
+ # State machine container holding the immutable State snapshot.
26
+ # Contains zero protocol branching logic and zero side-effects.
27
+ #
28
+ # The middle tier of the three-tier design: `Driver` executes side effects, {Rules} decides transitions,
29
+ # and Core holds the {State} between the two. See {Rules} for the protocol narrative and state graph, and
30
+ # `design/resumable_upload/implementation-guide.md` section 1 for the tier boundaries.
31
+ #
32
+ class Core
33
+ # @private
34
+ # @return [State] Current immutable state snapshot
35
+ attr_reader :state
36
+
37
+ # @private
38
+ # @return [Decision, nil] Decision emitted during the last dispatch
39
+ attr_reader :last_decision
40
+
41
+ ##
42
+ # @private
43
+ # Initializes a Core state machine container.
44
+ #
45
+ # @param config [StartUploadConfig, ResumeUploadConfig] Upload session configuration
46
+ #
47
+ def initialize config
48
+ @config = config
49
+ @last_decision = nil
50
+ @state = State.new(
51
+ status: :initializing,
52
+ upload_url: nil,
53
+ offset: 0,
54
+ chunk_size: config.chunk_size || Rules::DEFAULT_CHUNK_SIZE,
55
+ chunk_granularity: nil,
56
+ in_flight_length: 0,
57
+ last_error: nil
58
+ )
59
+ end
60
+
61
+ ##
62
+ # @private
63
+ # Dispatches event to Rules and updates internal state snapshot.
64
+ #
65
+ # @param event [Object] Input event
66
+ # @return [Array<Object>] Driver instructions
67
+ #
68
+ def dispatch event
69
+ decision = Rules.decide @state, event, @config
70
+ @state = decision.next_state
71
+ @last_decision = decision
72
+ decision.instructions
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end