auth0 5.4.0 → 5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/auth0.gemspec +1 -0
- data/lib/auth0/mixins/httpproxy.rb +69 -38
- data/lib/auth0/mixins/initializer.rb +1 -0
- data/lib/auth0/version.rb +1 -1
- data/spec/lib/auth0/mixins/httpproxy_spec.rb +189 -0
- data/spec/lib/auth0/mixins/initializer_spec.rb +7 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a3d59041ebfdd2dfcbc326ea1ea9e416796afd2818607014757d90266ed16c07
|
4
|
+
data.tar.gz: e9e950591d2027fc475fa437f144dea929a724eeea94bd9491b0a0af5095b271
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4f6733b62fa9839d0c03f4a9b641c5f86ffd3a47b7961d76156881f95f34a5ca0cf63c765fccc124418affaca78f34d72e899d5592eddf690f18d4d939b3f9fe
|
7
|
+
data.tar.gz: b9bf5d5f1e570a595388fbd3555ca23d6964c6a445d150b233f51dd2a19da30ef4a906825567da350a6411adbdee0a2af562953a3034e41f0c714a0ad0a4d04a
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## [v5.5.0](https://github.com/auth0/ruby-auth0/tree/v5.5.0) (2021-08-06)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/auth0/ruby-auth0/compare/v5.4.0..v5.5.0)
|
6
|
+
|
7
|
+
**Added**
|
8
|
+
|
9
|
+
- Automatically retry requests when API returns a 429 rate-limit status header. [\#290](https://github.com/auth0/ruby-auth0/pull/290) ([davidpatrick](https://github.com/davidpatrick))
|
10
|
+
|
3
11
|
## [v5.4.0](https://github.com/auth0/ruby-auth0/tree/v5.4.0) (2021-07-23)
|
4
12
|
|
5
13
|
[Full Changelog](https://github.com/auth0/ruby-auth0/compare/v5.3.0..v5.4.0)
|
data/auth0.gemspec
CHANGED
@@ -20,6 +20,7 @@ Gem::Specification.new do |s|
|
|
20
20
|
s.add_runtime_dependency 'jwt', '~> 2.2'
|
21
21
|
s.add_runtime_dependency 'zache', '~> 0.12'
|
22
22
|
s.add_runtime_dependency 'addressable', '~> 2.8'
|
23
|
+
s.add_runtime_dependency 'retryable', '~> 3.0'
|
23
24
|
|
24
25
|
s.add_development_dependency 'bundler'
|
25
26
|
s.add_development_dependency 'rake', '~> 13.0'
|
@@ -1,56 +1,52 @@
|
|
1
1
|
require "addressable/uri"
|
2
|
+
require "retryable"
|
3
|
+
require_relative "../exception.rb"
|
2
4
|
|
3
5
|
module Auth0
|
4
6
|
module Mixins
|
5
7
|
# here's the proxy for Rest calls based on rest-client, we're building all request on that gem
|
6
8
|
# for now, if you want to feel free to use your own http client
|
7
9
|
module HTTPProxy
|
8
|
-
attr_accessor :headers, :base_uri, :timeout
|
10
|
+
attr_accessor :headers, :base_uri, :timeout, :retry_count
|
11
|
+
DEAFULT_RETRIES = 3
|
12
|
+
MAX_ALLOWED_RETRIES = 10
|
13
|
+
MAX_REQUEST_RETRY_JITTER = 250
|
14
|
+
MAX_REQUEST_RETRY_DELAY = 1000
|
15
|
+
MIN_REQUEST_RETRY_DELAY = 100
|
9
16
|
|
10
17
|
# proxying requests from instance methods to HTTP class methods
|
11
18
|
%i(get post post_file put patch delete delete_with_body).each do |method|
|
12
19
|
define_method(method) do |uri, body = {}, extra_headers = {}|
|
20
|
+
body = body.delete_if { |_, v| v.nil? }
|
13
21
|
|
14
|
-
|
15
|
-
|
16
|
-
safe_path = Addressable::URI.new(path: uri).normalized_path
|
17
|
-
else
|
18
|
-
safe_path = Addressable::URI.escape(uri)
|
22
|
+
Retryable.retryable(retry_options) do
|
23
|
+
request(method, uri, body, extra_headers)
|
19
24
|
end
|
25
|
+
end
|
26
|
+
end
|
20
27
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
# Make the call with extra_headers, if provided.
|
29
|
-
call(:get, url(safe_path), timeout, get_headers)
|
30
|
-
elsif method == :delete
|
31
|
-
call(:delete, url(safe_path), timeout, add_headers({params: body}))
|
32
|
-
elsif method == :delete_with_body
|
33
|
-
call(:delete, url(safe_path), timeout, headers, body.to_json)
|
34
|
-
elsif method == :post_file
|
35
|
-
body.merge!(multipart: true)
|
36
|
-
# Ignore the default Content-Type headers and let the HTTP client define them
|
37
|
-
post_file_headers = headers.slice(*headers.keys - ['Content-Type'])
|
38
|
-
# Actual call with the altered headers
|
39
|
-
call(:post, url(safe_path), timeout, post_file_headers, body)
|
40
|
-
else
|
41
|
-
call(method, url(safe_path), timeout, headers, body.to_json)
|
42
|
-
end
|
43
|
-
case result.code
|
44
|
-
when 200...226 then safe_parse_json(result.body)
|
45
|
-
when 400 then raise Auth0::BadRequest.new(result.body, code: result.code, headers: result.headers)
|
46
|
-
when 401 then raise Auth0::Unauthorized.new(result.body, code: result.code, headers: result.headers)
|
47
|
-
when 403 then raise Auth0::AccessDenied.new(result.body, code: result.code, headers: result.headers)
|
48
|
-
when 404 then raise Auth0::NotFound.new(result.body, code: result.code, headers: result.headers)
|
49
|
-
when 429 then raise Auth0::RateLimitEncountered.new(result.body, code: result.code, headers: result.headers)
|
50
|
-
when 500 then raise Auth0::ServerError.new(result.body, code: result.code, headers: result.headers)
|
51
|
-
else raise Auth0::Unsupported.new(result.body, code: result.code, headers: result.headers)
|
52
|
-
end
|
28
|
+
def retry_options
|
29
|
+
sleep_timer = lambda do |attempt|
|
30
|
+
wait = 1000 * 2**attempt # Exponential delay with each subsequent request attempt.
|
31
|
+
wait += rand(wait..wait+MAX_REQUEST_RETRY_JITTER) # Add jitter to the delay window.
|
32
|
+
wait = [MAX_REQUEST_RETRY_DELAY, wait].min # Cap delay at MAX_REQUEST_RETRY_DELAY.
|
33
|
+
wait = [MIN_REQUEST_RETRY_DELAY, wait].max # Ensure delay is no less than MIN_REQUEST_RETRY_DELAY.
|
34
|
+
wait / 1000.to_f.round(2) # convert ms to seconds
|
53
35
|
end
|
36
|
+
|
37
|
+
tries = 1 + [Integer(retry_count || DEAFULT_RETRIES), MAX_ALLOWED_RETRIES].min # Cap retries at MAX_ALLOWED_RETRIES
|
38
|
+
|
39
|
+
{
|
40
|
+
tries: tries,
|
41
|
+
sleep: sleep_timer,
|
42
|
+
on: Auth0::RateLimitEncountered
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def encode_uri(uri)
|
47
|
+
# if a base_uri is set then the uri can be encoded as a path
|
48
|
+
path = base_uri ? Addressable::URI.new(path: uri).normalized_path : Addressable::URI.escape(uri)
|
49
|
+
url(path)
|
54
50
|
end
|
55
51
|
|
56
52
|
def url(path)
|
@@ -69,6 +65,41 @@ module Auth0
|
|
69
65
|
body
|
70
66
|
end
|
71
67
|
|
68
|
+
def request(method, uri, body, extra_headers)
|
69
|
+
result = if method == :get
|
70
|
+
# Mutate the headers property to add parameters.
|
71
|
+
add_headers({params: body})
|
72
|
+
# Merge custom headers into existing ones for this req.
|
73
|
+
# This prevents future calls from using them.
|
74
|
+
get_headers = headers.merge extra_headers
|
75
|
+
# Make the call with extra_headers, if provided.
|
76
|
+
call(:get, encode_uri(uri), timeout, get_headers)
|
77
|
+
elsif method == :delete
|
78
|
+
call(:delete, encode_uri(uri), timeout, add_headers({params: body}))
|
79
|
+
elsif method == :delete_with_body
|
80
|
+
call(:delete, encode_uri(uri), timeout, headers, body.to_json)
|
81
|
+
elsif method == :post_file
|
82
|
+
body.merge!(multipart: true)
|
83
|
+
# Ignore the default Content-Type headers and let the HTTP client define them
|
84
|
+
post_file_headers = headers.slice(*headers.keys - ['Content-Type'])
|
85
|
+
# Actual call with the altered headers
|
86
|
+
call(:post, encode_uri(uri), timeout, post_file_headers, body)
|
87
|
+
else
|
88
|
+
call(method, encode_uri(uri), timeout, headers, body.to_json)
|
89
|
+
end
|
90
|
+
|
91
|
+
case result.code
|
92
|
+
when 200...226 then safe_parse_json(result.body)
|
93
|
+
when 400 then raise Auth0::BadRequest.new(result.body, code: result.code, headers: result.headers)
|
94
|
+
when 401 then raise Auth0::Unauthorized.new(result.body, code: result.code, headers: result.headers)
|
95
|
+
when 403 then raise Auth0::AccessDenied.new(result.body, code: result.code, headers: result.headers)
|
96
|
+
when 404 then raise Auth0::NotFound.new(result.body, code: result.code, headers: result.headers)
|
97
|
+
when 429 then raise Auth0::RateLimitEncountered.new(result.body, code: result.code, headers: result.headers)
|
98
|
+
when 500 then raise Auth0::ServerError.new(result.body, code: result.code, headers: result.headers)
|
99
|
+
else raise Auth0::Unsupported.new(result.body, code: result.code, headers: result.headers)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
72
103
|
def call(method, url, timeout, headers, body = nil)
|
73
104
|
RestClient::Request.execute(
|
74
105
|
method: method,
|
@@ -15,6 +15,7 @@ module Auth0
|
|
15
15
|
@base_uri = base_url(options)
|
16
16
|
@headers = client_headers
|
17
17
|
@timeout = options[:timeout] || 10
|
18
|
+
@retry_count = options[:retry_count]
|
18
19
|
extend Auth0::Api::AuthenticationEndpoints
|
19
20
|
@client_id = options[:client_id]
|
20
21
|
@client_secret = options[:client_secret]
|
data/lib/auth0/version.rb
CHANGED
@@ -6,6 +6,7 @@ describe Auth0::Mixins::HTTPProxy do
|
|
6
6
|
dummy_instance = DummyClassForProxy.new
|
7
7
|
dummy_instance.extend(Auth0::Mixins::HTTPProxy)
|
8
8
|
dummy_instance.base_uri = "https://auth0.com"
|
9
|
+
dummy_instance.retry_count = 0
|
9
10
|
|
10
11
|
@instance = dummy_instance
|
11
12
|
@exception = DummyClassForRestClient.new
|
@@ -152,6 +153,100 @@ describe Auth0::Mixins::HTTPProxy do
|
|
152
153
|
.and_return(StubResponse.new({}, true, 200))
|
153
154
|
expect { @instance.send(http_method, '/te st#test') }.not_to raise_error
|
154
155
|
end
|
156
|
+
|
157
|
+
context "when status 429 is recieved on send http #{http_method} method" do
|
158
|
+
it "should retry 3 times when retry_count is not set" do
|
159
|
+
retry_instance = DummyClassForProxy.new
|
160
|
+
retry_instance.extend(Auth0::Mixins::HTTPProxy)
|
161
|
+
retry_instance.base_uri = "https://auth0.com"
|
162
|
+
|
163
|
+
@exception.response = StubResponse.new({}, false, 429)
|
164
|
+
allow(RestClient::Request).to receive(:execute).with(method: http_method,
|
165
|
+
url: 'https://auth0.com/test',
|
166
|
+
timeout: nil,
|
167
|
+
headers: { params: {} },
|
168
|
+
payload: nil)
|
169
|
+
.and_raise(@exception)
|
170
|
+
expect(RestClient::Request).to receive(:execute).exactly(4).times
|
171
|
+
|
172
|
+
expect { retry_instance.send(http_method, '/test') }.to raise_error { |error|
|
173
|
+
expect(error).to be_a(Auth0::RateLimitEncountered)
|
174
|
+
}
|
175
|
+
end
|
176
|
+
|
177
|
+
it "should retry 2 times when retry_count is set to 2" do
|
178
|
+
retry_instance = DummyClassForProxy.new
|
179
|
+
retry_instance.extend(Auth0::Mixins::HTTPProxy)
|
180
|
+
retry_instance.base_uri = "https://auth0.com"
|
181
|
+
retry_instance.retry_count = 2
|
182
|
+
|
183
|
+
@exception.response = StubResponse.new({}, false, 429)
|
184
|
+
allow(RestClient::Request).to receive(:execute).with(method: http_method,
|
185
|
+
url: 'https://auth0.com/test',
|
186
|
+
timeout: nil,
|
187
|
+
headers: { params: {} },
|
188
|
+
payload: nil)
|
189
|
+
.and_raise(@exception)
|
190
|
+
expect(RestClient::Request).to receive(:execute).exactly(3).times
|
191
|
+
|
192
|
+
expect { retry_instance.send(http_method, '/test') }.to raise_error { |error|
|
193
|
+
expect(error).to be_a(Auth0::RateLimitEncountered)
|
194
|
+
}
|
195
|
+
end
|
196
|
+
|
197
|
+
it "should not retry when retry_count is set to 0" do
|
198
|
+
retry_instance = DummyClassForProxy.new
|
199
|
+
retry_instance.extend(Auth0::Mixins::HTTPProxy)
|
200
|
+
retry_instance.base_uri = "https://auth0.com"
|
201
|
+
retry_instance.retry_count = 0
|
202
|
+
|
203
|
+
@exception.response = StubResponse.new({}, false, 429)
|
204
|
+
|
205
|
+
allow(RestClient::Request).to receive(:execute).with(method: http_method,
|
206
|
+
url: 'https://auth0.com/test',
|
207
|
+
timeout: nil,
|
208
|
+
headers: { params: {} },
|
209
|
+
payload: nil)
|
210
|
+
.and_raise(@exception)
|
211
|
+
|
212
|
+
expect(RestClient::Request).to receive(:execute).exactly(1).times
|
213
|
+
expect { retry_instance.send(http_method, '/test') }.to raise_error { |error|
|
214
|
+
expect(error).to be_a(Auth0::RateLimitEncountered)
|
215
|
+
}
|
216
|
+
end
|
217
|
+
|
218
|
+
it "should have have random retry times grow with jitter backoff" do
|
219
|
+
retry_instance = DummyClassForProxy.new
|
220
|
+
retry_instance.extend(Auth0::Mixins::HTTPProxy)
|
221
|
+
retry_instance.base_uri = "https://auth0.com"
|
222
|
+
retry_instance.retry_count = 2
|
223
|
+
time_entries = []
|
224
|
+
@time_start
|
225
|
+
|
226
|
+
@exception.response = StubResponse.new({}, false, 429)
|
227
|
+
allow(RestClient::Request).to receive(:execute).with(method: http_method,
|
228
|
+
url: 'https://auth0.com/test',
|
229
|
+
timeout: nil,
|
230
|
+
headers: { params: {} },
|
231
|
+
payload: nil) do
|
232
|
+
|
233
|
+
time_entries.push(Time.now.to_f - @time_start.to_f)
|
234
|
+
@time_start = Time.now.to_f # restart the clock
|
235
|
+
raise @exception
|
236
|
+
end
|
237
|
+
|
238
|
+
@time_start = Time.now.to_f #start the clock
|
239
|
+
retry_instance.send(http_method, '/test') rescue nil
|
240
|
+
time_entries_first_set = time_entries.shift(time_entries.length)
|
241
|
+
|
242
|
+
retry_instance.send(http_method, '/test') rescue nil
|
243
|
+
time_entries.each_with_index do |entry, index|
|
244
|
+
if index > 0 #skip the first request
|
245
|
+
expect(entry != time_entries_first_set[index])
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
155
250
|
end
|
156
251
|
end
|
157
252
|
|
@@ -301,6 +396,100 @@ describe Auth0::Mixins::HTTPProxy do
|
|
301
396
|
.and_return(StubResponse.new(res, true, 404))
|
302
397
|
expect { @instance.send(http_method, '/test') }.to raise_error(Auth0::NotFound, res)
|
303
398
|
end
|
399
|
+
|
400
|
+
context "when status 429 is recieved on send http #{http_method} method" do
|
401
|
+
it "should retry 3 times when retry_count is not set" do
|
402
|
+
retry_instance = DummyClassForProxy.new
|
403
|
+
retry_instance.extend(Auth0::Mixins::HTTPProxy)
|
404
|
+
retry_instance.base_uri = "https://auth0.com"
|
405
|
+
|
406
|
+
@exception.response = StubResponse.new({}, false, 429)
|
407
|
+
allow(RestClient::Request).to receive(:execute).with(method: http_method,
|
408
|
+
url: 'https://auth0.com/test',
|
409
|
+
timeout: nil,
|
410
|
+
headers: nil,
|
411
|
+
payload: '{}')
|
412
|
+
.and_raise(@exception)
|
413
|
+
expect(RestClient::Request).to receive(:execute).exactly(4).times
|
414
|
+
|
415
|
+
expect { retry_instance.send(http_method, '/test') }.to raise_error { |error|
|
416
|
+
expect(error).to be_a(Auth0::RateLimitEncountered)
|
417
|
+
}
|
418
|
+
end
|
419
|
+
|
420
|
+
it "should retry 2 times when retry_count is set to 2" do
|
421
|
+
retry_instance = DummyClassForProxy.new
|
422
|
+
retry_instance.extend(Auth0::Mixins::HTTPProxy)
|
423
|
+
retry_instance.base_uri = "https://auth0.com"
|
424
|
+
retry_instance.retry_count = 2
|
425
|
+
|
426
|
+
@exception.response = StubResponse.new({}, false, 429)
|
427
|
+
allow(RestClient::Request).to receive(:execute).with(method: http_method,
|
428
|
+
url: 'https://auth0.com/test',
|
429
|
+
timeout: nil,
|
430
|
+
headers: nil,
|
431
|
+
payload: '{}')
|
432
|
+
.and_raise(@exception)
|
433
|
+
expect(RestClient::Request).to receive(:execute).exactly(3).times
|
434
|
+
|
435
|
+
expect { retry_instance.send(http_method, '/test') }.to raise_error { |error|
|
436
|
+
expect(error).to be_a(Auth0::RateLimitEncountered)
|
437
|
+
}
|
438
|
+
end
|
439
|
+
|
440
|
+
it "should not retry when retry_count is set to 0" do
|
441
|
+
retry_instance = DummyClassForProxy.new
|
442
|
+
retry_instance.extend(Auth0::Mixins::HTTPProxy)
|
443
|
+
retry_instance.base_uri = "https://auth0.com"
|
444
|
+
retry_instance.retry_count = 0
|
445
|
+
|
446
|
+
@exception.response = StubResponse.new({}, false, 429)
|
447
|
+
|
448
|
+
allow(RestClient::Request).to receive(:execute).with(method: http_method,
|
449
|
+
url: 'https://auth0.com/test',
|
450
|
+
timeout: nil,
|
451
|
+
headers: nil,
|
452
|
+
payload: '{}')
|
453
|
+
.and_raise(@exception)
|
454
|
+
|
455
|
+
expect(RestClient::Request).to receive(:execute).exactly(1).times
|
456
|
+
expect { retry_instance.send(http_method, '/test') }.to raise_error { |error|
|
457
|
+
expect(error).to be_a(Auth0::RateLimitEncountered)
|
458
|
+
}
|
459
|
+
end
|
460
|
+
|
461
|
+
it "should have have random retry times grow with jitter backoff" do
|
462
|
+
retry_instance = DummyClassForProxy.new
|
463
|
+
retry_instance.extend(Auth0::Mixins::HTTPProxy)
|
464
|
+
retry_instance.base_uri = "https://auth0.com"
|
465
|
+
retry_instance.retry_count = 2
|
466
|
+
time_entries = []
|
467
|
+
@time_start
|
468
|
+
|
469
|
+
@exception.response = StubResponse.new({}, false, 429)
|
470
|
+
allow(RestClient::Request).to receive(:execute).with(method: http_method,
|
471
|
+
url: 'https://auth0.com/test',
|
472
|
+
timeout: nil,
|
473
|
+
headers: nil,
|
474
|
+
payload: '{}') do
|
475
|
+
|
476
|
+
time_entries.push(Time.now.to_f - @time_start.to_f)
|
477
|
+
@time_start = Time.now.to_f # restart the clock
|
478
|
+
raise @exception
|
479
|
+
end
|
480
|
+
|
481
|
+
@time_start = Time.now.to_f #start the clock
|
482
|
+
retry_instance.send(http_method, '/test') rescue nil
|
483
|
+
time_entries_first_set = time_entries.shift(time_entries.length)
|
484
|
+
|
485
|
+
retry_instance.send(http_method, '/test') rescue nil
|
486
|
+
time_entries.each_with_index do |entry, index|
|
487
|
+
if index > 0 #skip the first request
|
488
|
+
expect(entry != time_entries_first_set[index])
|
489
|
+
end
|
490
|
+
end
|
491
|
+
end
|
492
|
+
end
|
304
493
|
end
|
305
494
|
end
|
306
495
|
end
|
@@ -26,5 +26,12 @@ describe Auth0::Mixins::Initializer do
|
|
26
26
|
|
27
27
|
expect(instance.instance_variable_get('@token')).to eq('123')
|
28
28
|
end
|
29
|
+
|
30
|
+
it 'sets retry_count when passed' do
|
31
|
+
params[:token] = '123'
|
32
|
+
params[:retry_count] = 10
|
33
|
+
|
34
|
+
expect(instance.instance_variable_get('@retry_count')).to eq(10)
|
35
|
+
end
|
29
36
|
end
|
30
37
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: auth0
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.
|
4
|
+
version: 5.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Auth0
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2021-07
|
14
|
+
date: 2021-08-07 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: rest-client
|
@@ -69,6 +69,20 @@ dependencies:
|
|
69
69
|
- - "~>"
|
70
70
|
- !ruby/object:Gem::Version
|
71
71
|
version: '2.8'
|
72
|
+
- !ruby/object:Gem::Dependency
|
73
|
+
name: retryable
|
74
|
+
requirement: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - "~>"
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '3.0'
|
79
|
+
type: :runtime
|
80
|
+
prerelease: false
|
81
|
+
version_requirements: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - "~>"
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '3.0'
|
72
86
|
- !ruby/object:Gem::Dependency
|
73
87
|
name: bundler
|
74
88
|
requirement: !ruby/object:Gem::Requirement
|