smplkit 1.0.8 → 1.0.10

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: 240ff26d3b6331fc2d18ff45139a51805256dba0f4f967cfa22908813018969a
4
- data.tar.gz: '039b4b6006898ba7c0d772145b7f7e020db449fdbe84e9a5ea4a86cdaa556fa1'
3
+ metadata.gz: 42f9ef066eeb2612d4c6c4cc042933337e10562047eb41400506466a47ec4e99
4
+ data.tar.gz: 69d8819176f26b5199f2c508a71d77397106bfabe0ea5b99b071a3e83199dd0d
5
5
  SHA512:
6
- metadata.gz: e29c56bca6774d6359cf6608b1af33f740f08f1279413840f828bc76f8156d47db0e63e276572e54ccdd8fb231711d57549dd541317df87c41f0ca27f9709c3b
7
- data.tar.gz: 6c503968b0071dc1b5ffdfca1dea9a7b264246e55d9d618aa515c3a6ae408472b8bc2973fc2c1466c2a658a1a3c79633b3c273a406e4a88caae5900ece333ff7
6
+ metadata.gz: 8f05531a8b81d3aa42895ed48f8e7b1680d9cf9a490c356b5ce4cf311b304d862964cb49e18b72e7e982ce1e351b2f7be2234c0be06be6c58cae3d3566c37c53
7
+ data.tar.gz: f45d376af397dad147c4ae24100bdfd990766e511ce4a2de10539e3d358375098c413bf46f09c7f8e149d0333aa3c5efdca88baaebdd275ebda03490fff5370c
@@ -25,6 +25,7 @@ module SmplkitGeneratedClient::App
25
25
  # @param [Hash] opts the optional parameters
26
26
  # @option opts [String] :mode (default to 'signin')
27
27
  # @option opts [String] :source
28
+ # @option opts [String] :entry_point
28
29
  # @return [nil]
29
30
  def begin_oidc_login(provider, opts = {})
30
31
  begin_oidc_login_with_http_info(provider, opts)
@@ -37,6 +38,7 @@ module SmplkitGeneratedClient::App
37
38
  # @param [Hash] opts the optional parameters
38
39
  # @option opts [String] :mode (default to 'signin')
39
40
  # @option opts [String] :source
41
+ # @option opts [String] :entry_point
40
42
  # @return [Array<(nil, Integer, Hash)>] nil, response status code and response headers
41
43
  def begin_oidc_login_with_http_info(provider, opts = {})
42
44
  if @api_client.config.debugging
@@ -53,6 +55,7 @@ module SmplkitGeneratedClient::App
53
55
  query_params = opts[:query_params] || {}
54
56
  query_params[:'mode'] = opts[:'mode'] if !opts[:'mode'].nil?
55
57
  query_params[:'source'] = opts[:'source'] if !opts[:'source'].nil?
58
+ query_params[:'entry_point'] = opts[:'entry_point'] if !opts[:'entry_point'].nil?
56
59
 
57
60
  # header parameters
58
61
  header_params = opts[:header_params] || {}
@@ -29,6 +29,12 @@ module SmplkitGeneratedClient::App
29
29
 
30
30
  attr_accessor :product_subscriptions
31
31
 
32
+ # Registration entry point (from account.data)
33
+ attr_accessor :entry_point
34
+
35
+ # Whether sample data is active (from account.settings)
36
+ attr_accessor :show_sample_data
37
+
32
38
  # Attribute mapping from ruby-style variable name to JSON key.
33
39
  def self.attribute_map
34
40
  {
@@ -38,7 +44,9 @@ module SmplkitGeneratedClient::App
38
44
  :'expires_at' => :'expires_at',
39
45
  :'created_at' => :'created_at',
40
46
  :'deleted_at' => :'deleted_at',
41
- :'product_subscriptions' => :'product_subscriptions'
47
+ :'product_subscriptions' => :'product_subscriptions',
48
+ :'entry_point' => :'entry_point',
49
+ :'show_sample_data' => :'show_sample_data'
42
50
  }
43
51
  end
44
52
 
@@ -61,7 +69,9 @@ module SmplkitGeneratedClient::App
61
69
  :'expires_at' => :'Time',
62
70
  :'created_at' => :'Time',
63
71
  :'deleted_at' => :'Time',
64
- :'product_subscriptions' => :'Hash<String, Object>'
72
+ :'product_subscriptions' => :'Hash<String, Object>',
73
+ :'entry_point' => :'String',
74
+ :'show_sample_data' => :'Boolean'
65
75
  }
66
76
  end
67
77
 
@@ -71,7 +81,9 @@ module SmplkitGeneratedClient::App
71
81
  :'expires_at',
72
82
  :'created_at',
73
83
  :'deleted_at',
74
- :'product_subscriptions'
84
+ :'product_subscriptions',
85
+ :'entry_point',
86
+ :'show_sample_data'
75
87
  ])
76
88
  end
77
89
 
@@ -126,6 +138,14 @@ module SmplkitGeneratedClient::App
126
138
  self.product_subscriptions = value
127
139
  end
128
140
  end
141
+
142
+ if attributes.key?(:'entry_point')
143
+ self.entry_point = attributes[:'entry_point']
144
+ end
145
+
146
+ if attributes.key?(:'show_sample_data')
147
+ self.show_sample_data = attributes[:'show_sample_data']
148
+ end
129
149
  end
130
150
 
131
151
  # Show invalid properties with the reasons. Usually used together with valid?
@@ -202,7 +222,9 @@ module SmplkitGeneratedClient::App
202
222
  expires_at == o.expires_at &&
203
223
  created_at == o.created_at &&
204
224
  deleted_at == o.deleted_at &&
205
- product_subscriptions == o.product_subscriptions
225
+ product_subscriptions == o.product_subscriptions &&
226
+ entry_point == o.entry_point &&
227
+ show_sample_data == o.show_sample_data
206
228
  end
207
229
 
208
230
  # @see the `==` method
@@ -214,7 +236,7 @@ module SmplkitGeneratedClient::App
214
236
  # Calculates hash code according to all attributes.
215
237
  # @return [Integer] Hash code
216
238
  def hash
217
- [name, key, has_stripe_customer, expires_at, created_at, deleted_at, product_subscriptions].hash
239
+ [name, key, has_stripe_customer, expires_at, created_at, deleted_at, product_subscriptions, entry_point, show_sample_data].hash
218
240
  end
219
241
 
220
242
  # Builds the object from hash
@@ -19,11 +19,37 @@ module SmplkitGeneratedClient::App
19
19
 
20
20
  attr_accessor :password
21
21
 
22
+ # Registration entry point. Allowed: login, get_started, live_demo, unknown. Defaults to unknown when omitted.
23
+ attr_accessor :entry_point
24
+
25
+ class EnumAttributeValidator
26
+ attr_reader :datatype
27
+ attr_reader :allowable_values
28
+
29
+ def initialize(datatype, allowable_values)
30
+ @allowable_values = allowable_values.map do |value|
31
+ case datatype.to_s
32
+ when /Integer/i
33
+ value.to_i
34
+ when /Float/i
35
+ value.to_f
36
+ else
37
+ value
38
+ end
39
+ end
40
+ end
41
+
42
+ def valid?(value)
43
+ !value || allowable_values.include?(value)
44
+ end
45
+ end
46
+
22
47
  # Attribute mapping from ruby-style variable name to JSON key.
23
48
  def self.attribute_map
24
49
  {
25
50
  :'email' => :'email',
26
- :'password' => :'password'
51
+ :'password' => :'password',
52
+ :'entry_point' => :'entry_point'
27
53
  }
28
54
  end
29
55
 
@@ -41,13 +67,15 @@ module SmplkitGeneratedClient::App
41
67
  def self.openapi_types
42
68
  {
43
69
  :'email' => :'String',
44
- :'password' => :'String'
70
+ :'password' => :'String',
71
+ :'entry_point' => :'String'
45
72
  }
46
73
  end
47
74
 
48
75
  # List of attributes with nullable: true
49
76
  def self.openapi_nullable
50
77
  Set.new([
78
+ :'entry_point'
51
79
  ])
52
80
  end
53
81
 
@@ -78,6 +106,10 @@ module SmplkitGeneratedClient::App
78
106
  else
79
107
  self.password = nil
80
108
  end
109
+
110
+ if attributes.key?(:'entry_point')
111
+ self.entry_point = attributes[:'entry_point']
112
+ end
81
113
  end
82
114
 
83
115
  # Show invalid properties with the reasons. Usually used together with valid?
@@ -112,6 +144,8 @@ module SmplkitGeneratedClient::App
112
144
  return false if @password.nil?
113
145
  return false if @password.to_s.length > 128
114
146
  return false if @password.to_s.length < 8
147
+ entry_point_validator = EnumAttributeValidator.new('String', ["login", "get_started", "live_demo", "unknown"])
148
+ return false unless entry_point_validator.valid?(@entry_point)
115
149
  true
116
150
  end
117
151
 
@@ -143,13 +177,24 @@ module SmplkitGeneratedClient::App
143
177
  @password = password
144
178
  end
145
179
 
180
+ # Custom attribute writer method checking allowed values (enum).
181
+ # @param [Object] entry_point Object to be assigned
182
+ def entry_point=(entry_point)
183
+ validator = EnumAttributeValidator.new('String', ["login", "get_started", "live_demo", "unknown"])
184
+ unless validator.valid?(entry_point)
185
+ fail ArgumentError, "invalid value for \"entry_point\", must be one of #{validator.allowable_values}."
186
+ end
187
+ @entry_point = entry_point
188
+ end
189
+
146
190
  # Checks equality by comparing each attribute.
147
191
  # @param [Object] Object to be compared
148
192
  def ==(o)
149
193
  return true if self.equal?(o)
150
194
  self.class == o.class &&
151
195
  email == o.email &&
152
- password == o.password
196
+ password == o.password &&
197
+ entry_point == o.entry_point
153
198
  end
154
199
 
155
200
  # @see the `==` method
@@ -161,7 +206,7 @@ module SmplkitGeneratedClient::App
161
206
  # Calculates hash code according to all attributes.
162
207
  # @return [Integer] Hash code
163
208
  def hash
164
- [email, password].hash
209
+ [email, password, entry_point].hash
165
210
  end
166
211
 
167
212
  # Builds the object from hash
@@ -39,6 +39,7 @@ describe 'AuthApi' do
39
39
  # @param [Hash] opts the optional parameters
40
40
  # @option opts [String] :mode
41
41
  # @option opts [String] :source
42
+ # @option opts [String] :entry_point
42
43
  # @return [nil]
43
44
  describe 'begin_oidc_login test' do
44
45
  it 'should work' do
@@ -69,4 +69,16 @@ describe SmplkitGeneratedClient::App::Account do
69
69
  end
70
70
  end
71
71
 
72
+ describe 'test attribute "entry_point"' do
73
+ it 'should work' do
74
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
75
+ end
76
+ end
77
+
78
+ describe 'test attribute "show_sample_data"' do
79
+ it 'should work' do
80
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
81
+ end
82
+ end
83
+
72
84
  end
@@ -39,4 +39,14 @@ describe SmplkitGeneratedClient::App::RegisterRequest do
39
39
  end
40
40
  end
41
41
 
42
+ describe 'test attribute "entry_point"' do
43
+ it 'should work' do
44
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
45
+ # validator = Petstore::EnumTest::EnumAttributeValidator.new('String', ["login", "get_started", "live_demo", "unknown"])
46
+ # validator.allowable_values.each do |value|
47
+ # expect { instance.entry_point = value }.not_to raise_error
48
+ # end
49
+ end
50
+ end
51
+
42
52
  end
data/lib/smplkit/ws.rb CHANGED
@@ -1,42 +1,55 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+ require "async"
5
+ require "async/http/endpoint"
6
+ require "async/websocket/client"
3
7
  require "concurrent"
4
8
 
5
9
  module Smplkit
6
10
  # Manages a single WebSocket connection to the app service event gateway.
7
11
  #
8
12
  # A single +SharedWebSocket+ instance is shared across all product modules
9
- # (config, flags) within one +Smplkit::Client+. Product modules register
10
- # listeners for specific event types; the shared connection dispatches
11
- # incoming events to the appropriate listeners.
13
+ # (config, flags, logging) within one +Smplkit::Client+. Product modules
14
+ # register listeners for specific event types; the shared connection
15
+ # dispatches incoming events to the appropriate listeners.
12
16
  #
13
- # The connection runs on a dedicated SDK-owned thread; public methods are
14
- # thread-safe and non-blocking.
17
+ # The connection runs on a dedicated SDK-owned thread that hosts the
18
+ # +Async+ reactor and the underlying +async-websocket+ I/O. Public
19
+ # methods are thread-safe and non-blocking.
20
+ #
21
+ # Gateway protocol (mirrors the Python reference in +smplkit._ws+):
15
22
  #
16
- # The app service gateway protocol:
17
23
  # - Connect to +wss://app.<base_domain>/api/ws/v1/events?api_key={key}+
18
24
  # - Receive +{"type": "connected"}+ on success
19
25
  # - Receive events: +{"event": "config_changed", ...}+, etc.
20
- # - No subscribe message - the API key determines the account
26
+ # - No subscribe message the API key determines the account
21
27
  # - Heartbeat: server sends +"ping"+ (text), client responds with +"pong"+
22
28
  #
23
- # NOTE: The actual WebSocket I/O is wired to async-websocket on a worker
24
- # thread. The initial Ruby SDK release defers full live-update wiring to a
25
- # follow-up because async-websocket interactions need integration testing
26
- # against the real platform.
29
+ # On disconnect the reactor reconnects with exponential backoff
30
+ # (1, 2, 4, 8, 16, 32, 60 seconds, then capped). +stop+ closes the
31
+ # connection from the outer thread; the reader exits and the daemon
32
+ # thread terminates.
27
33
  class SharedWebSocket
28
34
  BACKOFF_SCHEDULE = [1, 2, 4, 8, 16, 32, 60].freeze
29
35
 
36
+ USER_AGENT = "smplkit-ruby-sdk/#{Smplkit::VERSION}".freeze
37
+
30
38
  def initialize(app_base_url:, api_key:, metrics: nil)
31
39
  @app_base_url = app_base_url
32
40
  @api_key = api_key
33
41
  @metrics = metrics
34
- @listeners = Concurrent::Hash.new { |h, k| h[k] = [] }
42
+ @listeners = Hash.new { |h, k| h[k] = [] }
35
43
  @listeners_lock = Mutex.new
36
44
  @connection_status = "disconnected"
37
45
  @closed = false
46
+ @ws_thread = nil
47
+ @connection = nil
48
+ @connection_lock = Mutex.new
38
49
  end
39
50
 
51
+ # ----- Listener registration ------------------------------------
52
+
40
53
  def on(event_name, &callback)
41
54
  @listeners_lock.synchronize { @listeners[event_name] << callback }
42
55
  end
@@ -45,6 +58,9 @@ module Smplkit
45
58
  @listeners_lock.synchronize { @listeners[event_name].delete(callback) }
46
59
  end
47
60
 
61
+ # Dispatch +data+ to every listener registered for +event_name+.
62
+ # Listener exceptions are caught and logged; one bad listener never
63
+ # blocks the rest.
48
64
  def dispatch(event_name, data)
49
65
  callbacks = @listeners_lock.synchronize { @listeners[event_name].dup }
50
66
  callbacks.each do |cb|
@@ -54,39 +70,186 @@ module Smplkit
54
70
  end
55
71
  end
56
72
 
73
+ # ----- Connection status ----------------------------------------
74
+
57
75
  attr_reader :connection_status
58
76
 
59
- # Marked as connected for in-process testing without a real WS connection.
60
- # Production wiring overrides this from the I/O thread once the gateway
61
- # confirms the handshake.
62
- def mark_connected!
63
- @connection_status = "connected"
64
- end
77
+ # ----- Lifecycle ------------------------------------------------
65
78
 
66
79
  def start
67
- Smplkit.debug("websocket", "starting shared WebSocket (Ruby SDK initial release: in-memory only)")
68
- # Live wiring is deferred. Behave as if the handshake succeeded so the
69
- # rest of the runtime can proceed - listeners still fire for any events
70
- # other code dispatches into this instance.
71
- mark_connected!
80
+ return if @ws_thread&.alive?
81
+
82
+ Smplkit.debug("websocket", "starting shared WebSocket background thread")
83
+ @closed = false
84
+ @connection_status = "connecting"
85
+ @ws_thread = Thread.new { run_reactor }
86
+ @ws_thread.name = "smplkit-shared-ws" if @ws_thread.respond_to?(:name=)
72
87
  end
73
88
 
74
89
  def stop
90
+ Smplkit.debug("websocket", "stopping shared WebSocket")
75
91
  @closed = true
76
92
  @connection_status = "disconnected"
93
+ close_active_connection
94
+ thread = @ws_thread
95
+ @ws_thread = nil
96
+ return unless thread
97
+
98
+ thread.join(2.0)
99
+ thread.kill if thread.alive?
77
100
  end
78
101
 
102
+ # ----- URL builder ----------------------------------------------
103
+
79
104
  def build_ws_url
80
105
  url = @app_base_url.dup
81
- ws_url = if url.start_with?("https://")
82
- "wss://#{url[("https://".length)..]}"
83
- elsif url.start_with?("http://")
84
- "ws://#{url[("http://".length)..]}"
85
- else
86
- "wss://#{url}"
87
- end
106
+ ws_url =
107
+ if url.start_with?("https://")
108
+ "wss://#{url[("https://".length)..]}"
109
+ elsif url.start_with?("http://")
110
+ "ws://#{url[("http://".length)..]}"
111
+ else
112
+ "wss://#{url}"
113
+ end
88
114
  ws_url = ws_url.chomp("/")
89
115
  "#{ws_url}/api/ws/v1/events?api_key=#{@api_key}"
90
116
  end
117
+
118
+ # ----- Inbound message handling (extracted for tests) -----------
119
+
120
+ # Process a single inbound text frame the way the live reactor does:
121
+ # +"ping"+ → call +send_pong+ with +"pong"+; otherwise parse JSON and,
122
+ # if a +"event"+ key is present, dispatch to listeners.
123
+ #
124
+ # Returns one of +:ping+, +:event+, +:no_event+, +:unparseable+ for the
125
+ # caller to log/observe; the live reactor ignores the return value.
126
+ def handle_inbound(text, send_pong:)
127
+ if text == "ping"
128
+ send_pong.call("pong")
129
+ return :ping
130
+ end
131
+
132
+ data =
133
+ begin
134
+ JSON.parse(text)
135
+ rescue JSON::ParserError
136
+ return :unparseable
137
+ end
138
+
139
+ event = data["event"]
140
+ if event
141
+ dispatch(event, data)
142
+ :event
143
+ else
144
+ :no_event
145
+ end
146
+ end
147
+
148
+ private
149
+
150
+ def run_reactor
151
+ Sync do |task|
152
+ ws_main(task)
153
+ end
154
+ rescue StandardError => e
155
+ Smplkit.debug("websocket", "shared WebSocket thread exited unexpectedly: #{e.class}: #{e.message}")
156
+ end
157
+
158
+ def ws_main(task)
159
+ connect(task)
160
+ rescue StandardError => e
161
+ return if @closed
162
+
163
+ Smplkit.debug(
164
+ "websocket",
165
+ "connection failed on startup (url: #{safe_url}): #{e.class}: #{e.message}"
166
+ )
167
+ reconnect(task)
168
+ end
169
+
170
+ def connect(task)
171
+ url = build_ws_url
172
+ @connection_status = "connecting"
173
+ Smplkit.debug("websocket", "connecting to #{safe_url}")
174
+
175
+ endpoint = Async::HTTP::Endpoint.parse(url)
176
+ headers = { "user-agent" => USER_AGENT }
177
+ connection = Async::WebSocket::Client.connect(endpoint, headers: headers)
178
+ @connection_lock.synchronize { @connection = connection }
179
+ Smplkit.debug("websocket", "WebSocket connected, waiting for confirmation")
180
+
181
+ raw = connection.read
182
+ data = JSON.parse(message_to_string(raw))
183
+ if data["type"] == "error"
184
+ err = data["message"]
185
+ Smplkit.debug("websocket", "connection error from server: #{err.inspect}")
186
+ raise "Connection error: #{err}"
187
+ end
188
+
189
+ @connection_status = "connected"
190
+ @metrics&.record_gauge("platform.websocket_connections", 1, unit: "connections")
191
+ receive_loop(task, connection)
192
+ end
193
+
194
+ def receive_loop(task, connection)
195
+ until @closed
196
+ message = connection.read
197
+ break if message.nil?
198
+
199
+ text = message_to_string(message)
200
+ handle_inbound(text, send_pong: ->(reply) { connection.write(reply) })
201
+ end
202
+ rescue StandardError => e
203
+ return if @closed
204
+
205
+ Smplkit.debug("websocket", "receive loop error: #{e.class}: #{e.message}")
206
+ @connection_status = "reconnecting"
207
+ @metrics&.record_gauge("platform.websocket_connections", 0, unit: "connections")
208
+ reconnect(task)
209
+ end
210
+
211
+ def reconnect(task)
212
+ attempt = 0
213
+ until @closed
214
+ delay = BACKOFF_SCHEDULE[[attempt, BACKOFF_SCHEDULE.length - 1].min]
215
+ Smplkit.debug("websocket", "reconnecting in #{delay}s (attempt #{attempt + 1})")
216
+ task.sleep(delay)
217
+ return if @closed
218
+
219
+ begin
220
+ connect(task)
221
+ return
222
+ rescue StandardError => e
223
+ Smplkit.debug("websocket", "reconnect attempt #{attempt + 1} failed: #{e.class}: #{e.message}")
224
+ attempt += 1
225
+ end
226
+ end
227
+ end
228
+
229
+ def message_to_string(message)
230
+ return message if message.is_a?(String)
231
+ return message.to_str if message.respond_to?(:to_str)
232
+
233
+ message.to_s
234
+ end
235
+
236
+ def close_active_connection
237
+ conn = @connection_lock.synchronize do
238
+ c = @connection
239
+ @connection = nil
240
+ c
241
+ end
242
+ return unless conn
243
+
244
+ begin
245
+ conn.close
246
+ rescue StandardError => e
247
+ Smplkit.debug("websocket", "close raised: #{e.class}: #{e.message}")
248
+ end
249
+ end
250
+
251
+ def safe_url
252
+ build_ws_url.split("?", 2).first
253
+ end
91
254
  end
92
255
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smplkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.8
4
+ version: 1.0.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Smpl Solutions LLC
@@ -9,6 +9,48 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: async
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.39'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.39'
26
+ - !ruby/object:Gem::Dependency
27
+ name: async-http
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.95'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.95'
40
+ - !ruby/object:Gem::Dependency
41
+ name: async-websocket
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.30'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.30'
12
54
  - !ruby/object:Gem::Dependency
13
55
  name: concurrent-ruby
14
56
  requirement: !ruby/object:Gem::Requirement