stripe 5.1.1 → 5.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -0
- data/.rubocop_todo.yml +0 -6
- data/CHANGELOG.md +15 -0
- data/Gemfile +0 -1
- data/VERSION +1 -1
- data/lib/stripe/api_operations/nested_resource.rb +28 -26
- data/lib/stripe/connection_manager.rb +36 -18
- data/lib/stripe/stripe_client.rb +114 -39
- data/lib/stripe/util.rb +12 -0
- data/lib/stripe/version.rb +1 -1
- data/test/stripe/connection_manager_test.rb +25 -0
- data/test/stripe/stripe_client_test.rb +188 -54
- data/test/test_helper.rb +0 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 04bf14074bb94dbb41edf733c9240f770026a9e51647359d4d1d55820c1f3c46
|
4
|
+
data.tar.gz: e5a8433b4284faf095da5ad4ebd102127b005a52b56b7651dadfa579a06188f1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e905a1f7fa40887d5c8bd78c428f1d377b42ead6586cf147f6334fafc6264d81ccbdb504e6ef2c8da3e473f40ad18b275f372cc70eea147435252d813ac948b8
|
7
|
+
data.tar.gz: 3d03fb7104356a787b85287455e74157761e92b159db33414685af565fd99e7ff596e9cd1f4ce7dd0e00f44e2c342618fc65f52ad91ed7ccedabd42db6d990c4
|
data/.rubocop.yml
CHANGED
@@ -19,8 +19,17 @@ Layout/IndentFirstHashElement:
|
|
19
19
|
Layout/IndentHeredoc:
|
20
20
|
Enabled: false
|
21
21
|
|
22
|
+
Metrics/BlockLength:
|
23
|
+
Max: 40
|
24
|
+
Exclude:
|
25
|
+
# `context` in tests are blocks and get quite large, so exclude the test
|
26
|
+
# directory from having to adhere to this rule.
|
27
|
+
- "test/**/*.rb"
|
28
|
+
|
22
29
|
Metrics/ClassLength:
|
23
30
|
Exclude:
|
31
|
+
# Test classes get quite large, so exclude the test directory from having
|
32
|
+
# to adhere to this rule.
|
24
33
|
- "test/**/*.rb"
|
25
34
|
|
26
35
|
Metrics/LineLength:
|
data/.rubocop_todo.yml
CHANGED
@@ -10,12 +10,6 @@
|
|
10
10
|
Metrics/AbcSize:
|
11
11
|
Max: 51
|
12
12
|
|
13
|
-
# Offense count: 33
|
14
|
-
# Configuration parameters: CountComments, ExcludedMethods.
|
15
|
-
# ExcludedMethods: refine
|
16
|
-
Metrics/BlockLength:
|
17
|
-
Max: 509
|
18
|
-
|
19
13
|
# Offense count: 12
|
20
14
|
# Configuration parameters: CountComments.
|
21
15
|
Metrics/ClassLength:
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 5.5.0 - 2019-10-03
|
4
|
+
* [#859](https://github.com/stripe/stripe-ruby/pull/859) User-friendly messages and retries for `EOFError`, `Errno::ECONNRESET`, `Errno::ETIMEDOUT`, and `Errno::EHOSTUNREACH` network errors
|
5
|
+
|
6
|
+
## 5.4.1 - 2019-10-01
|
7
|
+
* [#858](https://github.com/stripe/stripe-ruby/pull/858) Drop Timecop dependency
|
8
|
+
|
9
|
+
## 5.4.0 - 2019-10-01
|
10
|
+
* [#857](https://github.com/stripe/stripe-ruby/pull/857) Move to monotonic time for duration calculations
|
11
|
+
|
12
|
+
## 5.3.0 - 2019-10-01
|
13
|
+
* [#853](https://github.com/stripe/stripe-ruby/pull/853) Support `Stripe-Should-Retry` header
|
14
|
+
|
15
|
+
## 5.2.0 - 2019-09-19
|
16
|
+
* [#851](https://github.com/stripe/stripe-ruby/pull/851) Introduce system for garbage collecting connection managers
|
17
|
+
|
3
18
|
## 5.1.1 - 2019-09-04
|
4
19
|
* [#845](https://github.com/stripe/stripe-ruby/pull/845) Transfer the request_id from the http_headers to error.
|
5
20
|
|
data/Gemfile
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
5.
|
1
|
+
5.5.0
|
@@ -6,7 +6,7 @@ module Stripe
|
|
6
6
|
# that it's possible to do so from a static context (i.e. without a
|
7
7
|
# pre-existing collection of subresources on the parent).
|
8
8
|
#
|
9
|
-
# For
|
9
|
+
# For example, a transfer gains the static methods for reversals so that the
|
10
10
|
# methods `.create_reversal`, `.retrieve_reversal`, `.update_reversal`,
|
11
11
|
# etc. all become available.
|
12
12
|
module NestedResource
|
@@ -14,9 +14,11 @@ module Stripe
|
|
14
14
|
resource_plural: nil)
|
15
15
|
resource_plural ||= "#{resource}s"
|
16
16
|
path ||= resource_plural
|
17
|
+
|
17
18
|
raise ArgumentError, "operations array required" if operations.nil?
|
18
19
|
|
19
20
|
resource_url_method = :"#{resource}s_url"
|
21
|
+
|
20
22
|
define_singleton_method(resource_url_method) do |id, nested_id = nil|
|
21
23
|
url = "#{resource_url}/#{CGI.escape(id)}/#{CGI.escape(path)}"
|
22
24
|
url += "/#{CGI.escape(nested_id)}" unless nested_id.nil?
|
@@ -27,39 +29,39 @@ module Stripe
|
|
27
29
|
case operation
|
28
30
|
when :create
|
29
31
|
define_singleton_method(:"create_#{resource}") \
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
do |id, params = {}, opts = {}|
|
33
|
+
url = send(resource_url_method, id)
|
34
|
+
resp, opts = request(:post, url, params, opts)
|
35
|
+
Util.convert_to_stripe_object(resp.data, opts)
|
36
|
+
end
|
35
37
|
when :retrieve
|
36
38
|
define_singleton_method(:"retrieve_#{resource}") \
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
39
|
+
do |id, nested_id, opts = {}|
|
40
|
+
url = send(resource_url_method, id, nested_id)
|
41
|
+
resp, opts = request(:get, url, {}, opts)
|
42
|
+
Util.convert_to_stripe_object(resp.data, opts)
|
43
|
+
end
|
42
44
|
when :update
|
43
45
|
define_singleton_method(:"update_#{resource}") \
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
46
|
+
do |id, nested_id, params = {}, opts = {}|
|
47
|
+
url = send(resource_url_method, id, nested_id)
|
48
|
+
resp, opts = request(:post, url, params, opts)
|
49
|
+
Util.convert_to_stripe_object(resp.data, opts)
|
50
|
+
end
|
49
51
|
when :delete
|
50
52
|
define_singleton_method(:"delete_#{resource}") \
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
53
|
+
do |id, nested_id, params = {}, opts = {}|
|
54
|
+
url = send(resource_url_method, id, nested_id)
|
55
|
+
resp, opts = request(:delete, url, params, opts)
|
56
|
+
Util.convert_to_stripe_object(resp.data, opts)
|
57
|
+
end
|
56
58
|
when :list
|
57
59
|
define_singleton_method(:"list_#{resource_plural}") \
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
60
|
+
do |id, params = {}, opts = {}|
|
61
|
+
url = send(resource_url_method, id)
|
62
|
+
resp, opts = request(:get, url, params, opts)
|
63
|
+
Util.convert_to_stripe_object(resp.data, opts)
|
64
|
+
end
|
63
65
|
else
|
64
66
|
raise ArgumentError, "Unknown operation: #{operation.inspect}"
|
65
67
|
end
|
@@ -9,24 +9,36 @@ module Stripe
|
|
9
9
|
#
|
10
10
|
# Note that this class in itself is *not* thread safe. We expect it to be
|
11
11
|
# instantiated once per thread.
|
12
|
-
#
|
13
|
-
# Note also that this class doesn't currently clean up after itself because
|
14
|
-
# it expects to only ever have a few connections (unless `.clear` is called
|
15
|
-
# manually). It'd be possible to tank memory by constantly changing the value
|
16
|
-
# of `Stripe.api_base` or the like. A possible improvement might be to detect
|
17
|
-
# and prune old connections whenever a request is executed.
|
18
12
|
class ConnectionManager
|
13
|
+
# Timestamp (in seconds procured from the system's monotonic clock)
|
14
|
+
# indicating when the connection manager last made a request. This is used
|
15
|
+
# by `StripeClient` to determine whether a connection manager should be
|
16
|
+
# garbage collected or not.
|
17
|
+
attr_reader :last_used
|
18
|
+
|
19
19
|
def initialize
|
20
20
|
@active_connections = {}
|
21
|
+
@last_used = Util.monotonic_time
|
22
|
+
|
23
|
+
# A connection manager may be accessed across threads as one thread makes
|
24
|
+
# requests on it while another is trying to clear it (either because it's
|
25
|
+
# trying to garbage collect the manager or trying to clear it because a
|
26
|
+
# configuration setting has changed). That's probably thread-safe already
|
27
|
+
# because of Ruby's GIL, but just in case the library's running on JRuby
|
28
|
+
# or the like, use a mutex to synchronize access in this connection
|
29
|
+
# manager.
|
30
|
+
@mutex = Mutex.new
|
21
31
|
end
|
22
32
|
|
23
33
|
# Finishes any active connections by closing their TCP connection and
|
24
34
|
# clears them from internal tracking.
|
25
35
|
def clear
|
26
|
-
@
|
27
|
-
connection
|
36
|
+
@mutex.synchronize do
|
37
|
+
@active_connections.each do |_, connection|
|
38
|
+
connection.finish
|
39
|
+
end
|
40
|
+
@active_connections = {}
|
28
41
|
end
|
29
|
-
@active_connections = {}
|
30
42
|
end
|
31
43
|
|
32
44
|
# Gets a connection for a given URI. This is for internal use only as it's
|
@@ -35,17 +47,19 @@ module Stripe
|
|
35
47
|
#
|
36
48
|
# `uri` is expected to be a string.
|
37
49
|
def connection_for(uri)
|
38
|
-
|
39
|
-
|
50
|
+
@mutex.synchronize do
|
51
|
+
u = URI.parse(uri)
|
52
|
+
connection = @active_connections[[u.host, u.port]]
|
40
53
|
|
41
|
-
|
42
|
-
|
43
|
-
|
54
|
+
if connection.nil?
|
55
|
+
connection = create_connection(u)
|
56
|
+
connection.start
|
44
57
|
|
45
|
-
|
46
|
-
|
58
|
+
@active_connections[[u.host, u.port]] = connection
|
59
|
+
end
|
47
60
|
|
48
|
-
|
61
|
+
connection
|
62
|
+
end
|
49
63
|
end
|
50
64
|
|
51
65
|
# Executes an HTTP request to the given URI with the given method. Also
|
@@ -65,6 +79,8 @@ module Stripe
|
|
65
79
|
raise ArgumentError, "query should be a string" \
|
66
80
|
if query && !query.is_a?(String)
|
67
81
|
|
82
|
+
@last_used = Util.monotonic_time
|
83
|
+
|
68
84
|
connection = connection_for(uri)
|
69
85
|
|
70
86
|
u = URI.parse(uri)
|
@@ -74,7 +90,9 @@ module Stripe
|
|
74
90
|
u.path
|
75
91
|
end
|
76
92
|
|
77
|
-
|
93
|
+
@mutex.synchronize do
|
94
|
+
connection.send_request(method.to_s.upcase, path, body, headers)
|
95
|
+
end
|
78
96
|
end
|
79
97
|
|
80
98
|
#
|
data/lib/stripe/stripe_client.rb
CHANGED
@@ -5,18 +5,17 @@ module Stripe
|
|
5
5
|
# recover both a resource a call returns as well as a response object that
|
6
6
|
# contains information on the HTTP call.
|
7
7
|
class StripeClient
|
8
|
-
# A set of all known
|
8
|
+
# A set of all known thread contexts across all threads and a mutex to
|
9
9
|
# synchronize global access to them.
|
10
|
-
@
|
11
|
-
@
|
10
|
+
@thread_contexts_with_connection_managers = []
|
11
|
+
@thread_contexts_with_connection_managers_mutex = Mutex.new
|
12
|
+
@last_connection_manager_gc = Util.monotonic_time
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
#
|
16
|
-
#
|
17
|
-
def initialize(
|
18
|
-
self.connection_manager = connection_manager ||
|
19
|
-
self.class.default_connection_manager
|
14
|
+
# Initializes a new `StripeClient`.
|
15
|
+
#
|
16
|
+
# Takes a connection manager object for backwards compatibility only, and
|
17
|
+
# that use is DEPRECATED.
|
18
|
+
def initialize(_connection_manager = nil)
|
20
19
|
@system_profiler = SystemProfiler.new
|
21
20
|
@last_request_metrics = nil
|
22
21
|
end
|
@@ -42,17 +41,24 @@ module Stripe
|
|
42
41
|
# Just a quick path for when configuration is being set for the first
|
43
42
|
# time before any connections have been opened. There is technically some
|
44
43
|
# potential for thread raciness here, but not in a practical sense.
|
45
|
-
return if @
|
46
|
-
|
47
|
-
@
|
48
|
-
@
|
44
|
+
return if @thread_contexts_with_connection_managers.empty?
|
45
|
+
|
46
|
+
@thread_contexts_with_connection_managers_mutex.synchronize do
|
47
|
+
@thread_contexts_with_connection_managers.each do |thread_context|
|
48
|
+
# Note that the thread context itself is not destroyed, but we clear
|
49
|
+
# its connection manager and remove our reference to it. If it ever
|
50
|
+
# makes a new request we'll give it a new connection manager and
|
51
|
+
# it'll go back into `@thread_contexts_with_connection_managers`.
|
52
|
+
thread_context.default_connection_manager.clear
|
53
|
+
thread_context.default_connection_manager = nil
|
54
|
+
end
|
55
|
+
@thread_contexts_with_connection_managers.clear
|
49
56
|
end
|
50
57
|
end
|
51
58
|
|
52
59
|
# A default client for the current thread.
|
53
60
|
def self.default_client
|
54
|
-
current_thread_context.default_client ||=
|
55
|
-
StripeClient.new(default_connection_manager)
|
61
|
+
current_thread_context.default_client ||= StripeClient.new
|
56
62
|
end
|
57
63
|
|
58
64
|
# A default connection manager for the current thread.
|
@@ -60,8 +66,9 @@ module Stripe
|
|
60
66
|
current_thread_context.default_connection_manager ||= begin
|
61
67
|
connection_manager = ConnectionManager.new
|
62
68
|
|
63
|
-
@
|
64
|
-
|
69
|
+
@thread_contexts_with_connection_managers_mutex.synchronize do
|
70
|
+
maybe_gc_connection_managers
|
71
|
+
@thread_contexts_with_connection_managers << current_thread_context
|
65
72
|
end
|
66
73
|
|
67
74
|
connection_manager
|
@@ -74,17 +81,23 @@ module Stripe
|
|
74
81
|
def self.should_retry?(error, method:, num_retries:)
|
75
82
|
return false if num_retries >= Stripe.max_network_retries
|
76
83
|
|
77
|
-
|
78
|
-
|
79
|
-
|
84
|
+
case error
|
85
|
+
when Net::OpenTimeout, Net::ReadTimeout
|
86
|
+
# Retry on timeout-related problems (either on open or read).
|
87
|
+
true
|
88
|
+
when EOFError, Errno::ECONNREFUSED, Errno::ECONNRESET,
|
89
|
+
Errno::EHOSTUNREACH, Errno::ETIMEDOUT, SocketError
|
90
|
+
# Destination refused the connection, the connection was reset, or a
|
91
|
+
# variety of other connection failures. This could occur from a single
|
92
|
+
# saturated server, so retry in case it's intermittent.
|
93
|
+
true
|
94
|
+
when Stripe::StripeError
|
95
|
+
# The API may ask us not to retry (e.g. if doing so would be a no-op),
|
96
|
+
# or advise us to retry (e.g. in cases of lock timeouts). Defer to
|
97
|
+
# those instructions if given.
|
98
|
+
return false if error.http_headers["stripe-should-retry"] == "false"
|
99
|
+
return true if error.http_headers["stripe-should-retry"] == "true"
|
80
100
|
|
81
|
-
# Destination refused the connection, the connection was reset, or a
|
82
|
-
# variety of other connection failures. This could occur from a single
|
83
|
-
# saturated server, so retry in case it's intermittent.
|
84
|
-
return true if error.is_a?(Errno::ECONNREFUSED)
|
85
|
-
return true if error.is_a?(SocketError)
|
86
|
-
|
87
|
-
if error.is_a?(Stripe::StripeError)
|
88
101
|
# 409 Conflict
|
89
102
|
return true if error.http_status == 409
|
90
103
|
|
@@ -106,10 +119,10 @@ module Stripe
|
|
106
119
|
return true if error.http_status == 500 && method != :post
|
107
120
|
|
108
121
|
# 503 Service Unavailable
|
109
|
-
|
122
|
+
error.http_status == 503
|
123
|
+
else
|
124
|
+
false
|
110
125
|
end
|
111
|
-
|
112
|
-
false
|
113
126
|
end
|
114
127
|
|
115
128
|
def self.sleep_time(num_retries)
|
@@ -131,6 +144,15 @@ module Stripe
|
|
131
144
|
sleep_seconds
|
132
145
|
end
|
133
146
|
|
147
|
+
# Gets the connection manager in use for the current `StripeClient`.
|
148
|
+
#
|
149
|
+
# This method is DEPRECATED and for backwards compatibility only.
|
150
|
+
def connection_manager
|
151
|
+
self.class.default_connection_manager
|
152
|
+
end
|
153
|
+
extend Gem::Deprecate
|
154
|
+
deprecate :connection_manager, :none, 2020, 9
|
155
|
+
|
134
156
|
# Executes the API call within the given block. Usage looks like:
|
135
157
|
#
|
136
158
|
# client = StripeClient.new
|
@@ -207,10 +229,10 @@ module Stripe
|
|
207
229
|
context.query = query
|
208
230
|
|
209
231
|
http_resp = execute_request_with_rescues(method, api_base, context) do
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
232
|
+
self.class.default_connection_manager.execute_request(method, url,
|
233
|
+
body: body,
|
234
|
+
headers: headers,
|
235
|
+
query: query)
|
214
236
|
end
|
215
237
|
|
216
238
|
begin
|
@@ -233,6 +255,14 @@ module Stripe
|
|
233
255
|
# private
|
234
256
|
#
|
235
257
|
|
258
|
+
# Time (in seconds) that a connection manager has not been used before it's
|
259
|
+
# eligible for garbage collection.
|
260
|
+
CONNECTION_MANAGER_GC_LAST_USED_EXPIRY = 120
|
261
|
+
|
262
|
+
# How often to check (in seconds) for connection managers that haven't been
|
263
|
+
# used in a long time and which should be garbage collected.
|
264
|
+
CONNECTION_MANAGER_GC_PERIOD = 60
|
265
|
+
|
236
266
|
ERROR_MESSAGE_CONNECTION =
|
237
267
|
"Unexpected error communicating when trying to connect to " \
|
238
268
|
"Stripe (%s). You may be seeing this message because your DNS is not " \
|
@@ -266,7 +296,11 @@ module Stripe
|
|
266
296
|
# The original error message is also appended onto the final exception for
|
267
297
|
# full transparency.
|
268
298
|
NETWORK_ERROR_MESSAGES_MAP = {
|
299
|
+
EOFError => ERROR_MESSAGE_CONNECTION,
|
269
300
|
Errno::ECONNREFUSED => ERROR_MESSAGE_CONNECTION,
|
301
|
+
Errno::ECONNRESET => ERROR_MESSAGE_CONNECTION,
|
302
|
+
Errno::EHOSTUNREACH => ERROR_MESSAGE_CONNECTION,
|
303
|
+
Errno::ETIMEDOUT => ERROR_MESSAGE_TIMEOUT_CONNECT,
|
270
304
|
SocketError => ERROR_MESSAGE_CONNECTION,
|
271
305
|
|
272
306
|
Net::OpenTimeout => ERROR_MESSAGE_TIMEOUT_CONNECT,
|
@@ -320,6 +354,46 @@ module Stripe
|
|
320
354
|
Thread.current[:stripe_client__internal_use_only] ||= ThreadContext.new
|
321
355
|
end
|
322
356
|
|
357
|
+
# Garbage collects connection managers that haven't been used in some time,
|
358
|
+
# with the idea being that we want to remove old connection managers that
|
359
|
+
# belong to dead threads and the like.
|
360
|
+
#
|
361
|
+
# Prefixed with `maybe_` because garbage collection will only run
|
362
|
+
# periodically so that we're not constantly engaged in busy work. If
|
363
|
+
# connection managers live a little passed their useful age it's not
|
364
|
+
# harmful, so it's not necessary to get them right away.
|
365
|
+
#
|
366
|
+
# For testability, returns `nil` if it didn't run and the number of
|
367
|
+
# connection managers that were garbage collected otherwise.
|
368
|
+
#
|
369
|
+
# IMPORTANT: This method is not thread-safe and expects to be called inside
|
370
|
+
# a lock on `@thread_contexts_with_connection_managers_mutex`.
|
371
|
+
#
|
372
|
+
# For internal use only. Does not provide a stable API and may be broken
|
373
|
+
# with future non-major changes.
|
374
|
+
def self.maybe_gc_connection_managers
|
375
|
+
next_gc_time = @last_connection_manager_gc + CONNECTION_MANAGER_GC_PERIOD
|
376
|
+
return nil if next_gc_time > Util.monotonic_time
|
377
|
+
|
378
|
+
last_used_threshold =
|
379
|
+
Util.monotonic_time - CONNECTION_MANAGER_GC_LAST_USED_EXPIRY
|
380
|
+
|
381
|
+
pruned_thread_contexts = []
|
382
|
+
@thread_contexts_with_connection_managers.each do |thread_context|
|
383
|
+
connection_manager = thread_context.default_connection_manager
|
384
|
+
next if connection_manager.last_used > last_used_threshold
|
385
|
+
|
386
|
+
connection_manager.clear
|
387
|
+
thread_context.default_connection_manager = nil
|
388
|
+
pruned_thread_contexts << thread_context
|
389
|
+
end
|
390
|
+
|
391
|
+
@thread_contexts_with_connection_managers -= pruned_thread_contexts
|
392
|
+
@last_connection_manager_gc = Util.monotonic_time
|
393
|
+
|
394
|
+
pruned_thread_contexts.count
|
395
|
+
end
|
396
|
+
|
323
397
|
private def api_url(url = "", api_base = nil)
|
324
398
|
(api_base || Stripe.api_base) + url
|
325
399
|
end
|
@@ -375,7 +449,7 @@ module Stripe
|
|
375
449
|
private def execute_request_with_rescues(method, api_base, context)
|
376
450
|
num_retries = 0
|
377
451
|
begin
|
378
|
-
request_start =
|
452
|
+
request_start = Util.monotonic_time
|
379
453
|
log_request(context, num_retries)
|
380
454
|
resp = yield
|
381
455
|
context = context.dup_from_response_headers(resp)
|
@@ -385,7 +459,8 @@ module Stripe
|
|
385
459
|
log_response(context, request_start, resp.code.to_i, resp.body)
|
386
460
|
|
387
461
|
if Stripe.enable_telemetry? && context.request_id
|
388
|
-
request_duration_ms =
|
462
|
+
request_duration_ms =
|
463
|
+
((Util.monotonic_time - request_start) * 1000).to_int
|
389
464
|
@last_request_metrics =
|
390
465
|
StripeRequestMetrics.new(context.request_id, request_duration_ms)
|
391
466
|
end
|
@@ -656,7 +731,7 @@ module Stripe
|
|
656
731
|
Util.log_info("Response from Stripe API",
|
657
732
|
account: context.account,
|
658
733
|
api_version: context.api_version,
|
659
|
-
elapsed:
|
734
|
+
elapsed: Util.monotonic_time - request_start,
|
660
735
|
idempotency_key: context.idempotency_key,
|
661
736
|
method: context.method,
|
662
737
|
path: context.path,
|
@@ -678,7 +753,7 @@ module Stripe
|
|
678
753
|
|
679
754
|
private def log_response_error(context, request_start, error)
|
680
755
|
Util.log_error("Request error",
|
681
|
-
elapsed:
|
756
|
+
elapsed: Util.monotonic_time - request_start,
|
682
757
|
error_message: error.message,
|
683
758
|
idempotency_key: context.idempotency_key,
|
684
759
|
method: context.method,
|
data/lib/stripe/util.rb
CHANGED
@@ -172,6 +172,18 @@ module Stripe
|
|
172
172
|
result
|
173
173
|
end
|
174
174
|
|
175
|
+
# `Time.now` can be unstable in cases like an administrator manually
|
176
|
+
# updating its value or a reconcilation via NTP. For this reason, prefer
|
177
|
+
# the use of the system's monotonic clock especially where comparing times
|
178
|
+
# to calculate an elapsed duration.
|
179
|
+
#
|
180
|
+
# Shortcut for getting monotonic time, mostly for purposes of line length
|
181
|
+
# and test stubbing. Returns time in seconds since the event used for
|
182
|
+
# monotonic reference purposes by the platform (e.g. system boot time).
|
183
|
+
def self.monotonic_time
|
184
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
185
|
+
end
|
186
|
+
|
175
187
|
def self.normalize_id(id)
|
176
188
|
if id.is_a?(Hash) # overloaded id
|
177
189
|
params_hash = id.dup
|
data/lib/stripe/version.rb
CHANGED
@@ -8,6 +8,14 @@ module Stripe
|
|
8
8
|
@manager = Stripe::ConnectionManager.new
|
9
9
|
end
|
10
10
|
|
11
|
+
context "#initialize" do
|
12
|
+
should "set #last_used to current time" do
|
13
|
+
t = 123.0
|
14
|
+
Util.stubs(:monotonic_time).returns(t)
|
15
|
+
assert_equal t, Stripe::ConnectionManager.new.last_used
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
11
19
|
context "#clear" do
|
12
20
|
should "clear any active connections" do
|
13
21
|
stub_request(:post, "#{Stripe.api_base}/path")
|
@@ -133,6 +141,23 @@ module Stripe
|
|
133
141
|
end
|
134
142
|
assert_equal e.message, "query should be a string"
|
135
143
|
end
|
144
|
+
|
145
|
+
should "set #last_used to current time" do
|
146
|
+
stub_request(:post, "#{Stripe.api_base}/path")
|
147
|
+
.to_return(body: JSON.generate(object: "account"))
|
148
|
+
|
149
|
+
t = 123.0
|
150
|
+
Util.stubs(:monotonic_time).returns(t)
|
151
|
+
|
152
|
+
manager = Stripe::ConnectionManager.new
|
153
|
+
|
154
|
+
# `#last_used` is also set by the constructor, so make sure we get a
|
155
|
+
# new value for it.
|
156
|
+
Util.stubs(:monotonic_time).returns(t + 1.0)
|
157
|
+
|
158
|
+
manager.execute_request(:post, "#{Stripe.api_base}/path")
|
159
|
+
assert_equal t + 1.0, manager.last_used
|
160
|
+
end
|
136
161
|
end
|
137
162
|
end
|
138
163
|
end
|
@@ -17,6 +17,76 @@ module Stripe
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
+
context ".maybe_gc_connection_managers" do
|
21
|
+
should "garbage collect old connection managers when appropriate" do
|
22
|
+
stub_request(:post, "#{Stripe.api_base}/v1/path")
|
23
|
+
.to_return(body: JSON.generate(object: "account"))
|
24
|
+
|
25
|
+
# Make sure we start with a blank slate (state may have been left in
|
26
|
+
# place by other tests).
|
27
|
+
StripeClient.clear_all_connection_managers
|
28
|
+
|
29
|
+
# Establish a base time.
|
30
|
+
t = 0.0
|
31
|
+
|
32
|
+
# And pretend that `StripeClient` was just initialized for the first
|
33
|
+
# time. (Don't access instance variables like this, but it's tricky to
|
34
|
+
# test properly otherwise.)
|
35
|
+
StripeClient.instance_variable_set(:@last_connection_manager_gc, t)
|
36
|
+
|
37
|
+
#
|
38
|
+
# t
|
39
|
+
#
|
40
|
+
Util.stubs(:monotonic_time).returns(t)
|
41
|
+
|
42
|
+
# Execute an initial request to ensure that a connection manager was
|
43
|
+
# created.
|
44
|
+
client = StripeClient.new
|
45
|
+
client.execute_request(:post, "/v1/path")
|
46
|
+
|
47
|
+
# The GC shouldn't run yet (a `nil` return indicates that GC didn't run).
|
48
|
+
assert_equal nil, StripeClient.maybe_gc_connection_managers
|
49
|
+
|
50
|
+
#
|
51
|
+
# t + StripeClient::CONNECTION_MANAGER_GC_PERIOD - 1
|
52
|
+
#
|
53
|
+
# Move time to just *before* garbage collection is eligible to run.
|
54
|
+
# Nothing should happen.
|
55
|
+
#
|
56
|
+
Util.stubs(:monotonic_time).returns(t + StripeClient::CONNECTION_MANAGER_GC_PERIOD - 1)
|
57
|
+
|
58
|
+
assert_equal nil, StripeClient.maybe_gc_connection_managers
|
59
|
+
|
60
|
+
#
|
61
|
+
# t + StripeClient::CONNECTION_MANAGER_GC_PERIOD + 1
|
62
|
+
#
|
63
|
+
# Move time to just *after* garbage collection is eligible to run.
|
64
|
+
# Garbage collection will run, but because the connection manager is
|
65
|
+
# not passed its expiry age, it will not be collected. Zero is returned
|
66
|
+
# to indicate so.
|
67
|
+
#
|
68
|
+
Util.stubs(:monotonic_time).returns(t + StripeClient::CONNECTION_MANAGER_GC_PERIOD + 1)
|
69
|
+
|
70
|
+
assert_equal 0, StripeClient.maybe_gc_connection_managers
|
71
|
+
|
72
|
+
#
|
73
|
+
# t + StripeClient::CONNECTION_MANAGER_GC_LAST_USED_EXPIRY + 1
|
74
|
+
#
|
75
|
+
# Move us far enough into the future that we're passed the horizons for
|
76
|
+
# both a GC run as well as well as the expiry age of a connection
|
77
|
+
# manager. That means the GC will run and collect the connection
|
78
|
+
# manager that we created above.
|
79
|
+
#
|
80
|
+
Util.stubs(:monotonic_time).returns(t + StripeClient::CONNECTION_MANAGER_GC_LAST_USED_EXPIRY + 1)
|
81
|
+
|
82
|
+
assert_equal 1, StripeClient.maybe_gc_connection_managers
|
83
|
+
|
84
|
+
# And as an additional check, the connection manager of the current
|
85
|
+
# thread context should have been set to `nil` as it was GCed.
|
86
|
+
assert_nil StripeClient.current_thread_context.default_connection_manager
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
20
90
|
context ".clear_all_connection_managers" do
|
21
91
|
should "clear connection managers across all threads" do
|
22
92
|
stub_request(:post, "#{Stripe.api_base}/path")
|
@@ -102,6 +172,26 @@ module Stripe
|
|
102
172
|
method: :post, num_retries: 0)
|
103
173
|
end
|
104
174
|
|
175
|
+
should "retry on EOFError" do
|
176
|
+
assert StripeClient.should_retry?(EOFError.new,
|
177
|
+
method: :post, num_retries: 0)
|
178
|
+
end
|
179
|
+
|
180
|
+
should "retry on Errno::ECONNRESET" do
|
181
|
+
assert StripeClient.should_retry?(Errno::ECONNRESET.new,
|
182
|
+
method: :post, num_retries: 0)
|
183
|
+
end
|
184
|
+
|
185
|
+
should "retry on Errno::ETIMEDOUT" do
|
186
|
+
assert StripeClient.should_retry?(Errno::ETIMEDOUT.new,
|
187
|
+
method: :post, num_retries: 0)
|
188
|
+
end
|
189
|
+
|
190
|
+
should "retry on Errno::EHOSTUNREACH" do
|
191
|
+
assert StripeClient.should_retry?(Errno::EHOSTUNREACH.new,
|
192
|
+
method: :post, num_retries: 0)
|
193
|
+
end
|
194
|
+
|
105
195
|
should "retry on Net::OpenTimeout" do
|
106
196
|
assert StripeClient.should_retry?(Net::OpenTimeout.new,
|
107
197
|
method: :post, num_retries: 0)
|
@@ -117,6 +207,28 @@ module Stripe
|
|
117
207
|
method: :post, num_retries: 0)
|
118
208
|
end
|
119
209
|
|
210
|
+
should "retry when the `Stripe-Should-Retry` header is `true`" do
|
211
|
+
headers = StripeResponse::Headers.new(
|
212
|
+
"Stripe-Should-Retry" => ["true"]
|
213
|
+
)
|
214
|
+
|
215
|
+
# Note we send status 400 here, which would normally not be retried.
|
216
|
+
assert StripeClient.should_retry?(Stripe::StripeError.new(http_headers: headers,
|
217
|
+
http_status: 400),
|
218
|
+
method: :post, num_retries: 0)
|
219
|
+
end
|
220
|
+
|
221
|
+
should "not retry when the `Stripe-Should-Retry` header is `false`" do
|
222
|
+
headers = StripeResponse::Headers.new(
|
223
|
+
"Stripe-Should-Retry" => ["false"]
|
224
|
+
)
|
225
|
+
|
226
|
+
# Note we send status 409 here, which would normally be retried.
|
227
|
+
refute StripeClient.should_retry?(Stripe::StripeError.new(http_headers: headers,
|
228
|
+
http_status: 409),
|
229
|
+
method: :post, num_retries: 0)
|
230
|
+
end
|
231
|
+
|
120
232
|
should "retry on a 409 Conflict" do
|
121
233
|
assert StripeClient.should_retry?(Stripe::StripeError.new(http_status: 409),
|
122
234
|
method: :post, num_retries: 0)
|
@@ -199,20 +311,6 @@ module Stripe
|
|
199
311
|
end
|
200
312
|
end
|
201
313
|
|
202
|
-
context "#initialize" do
|
203
|
-
should "set Stripe.default_connection_manager" do
|
204
|
-
client = StripeClient.new
|
205
|
-
assert_equal StripeClient.default_connection_manager,
|
206
|
-
client.connection_manager
|
207
|
-
end
|
208
|
-
|
209
|
-
should "set a different connection if one was specified" do
|
210
|
-
connection_manager = ConnectionManager.new
|
211
|
-
client = StripeClient.new(connection_manager)
|
212
|
-
assert_equal connection_manager, client.connection_manager
|
213
|
-
end
|
214
|
-
end
|
215
|
-
|
216
314
|
context "#execute_request" do
|
217
315
|
context "headers" do
|
218
316
|
should "support literal headers" do
|
@@ -239,16 +337,11 @@ module Stripe
|
|
239
337
|
context "logging" do
|
240
338
|
setup do
|
241
339
|
# Freeze time for the purposes of the `elapsed` parameter that we
|
242
|
-
# emit for responses.
|
243
|
-
#
|
244
|
-
#
|
245
|
-
#
|
246
|
-
|
247
|
-
Timecop.freeze(Time.local(1990))
|
248
|
-
end
|
249
|
-
|
250
|
-
teardown do
|
251
|
-
Timecop.return
|
340
|
+
# emit for responses. Mocha's `anything` parameter can't match inside
|
341
|
+
# of a hash and is therefore not useful for this purpose. If we
|
342
|
+
# switch over to rspec-mocks at some point, we can probably remove
|
343
|
+
# this.
|
344
|
+
Util.stubs(:monotonic_time).returns(0.0)
|
252
345
|
end
|
253
346
|
|
254
347
|
should "produce appropriate logging" do
|
@@ -470,10 +563,10 @@ module Stripe
|
|
470
563
|
.to_return(body: "", status: 500)
|
471
564
|
|
472
565
|
client = StripeClient.new
|
566
|
+
|
473
567
|
e = assert_raises Stripe::APIError do
|
474
568
|
client.execute_request(:post, "/v1/charges")
|
475
569
|
end
|
476
|
-
|
477
570
|
assert_equal 'Invalid response object from API: "" (HTTP response code was 500)', e.message
|
478
571
|
end
|
479
572
|
|
@@ -482,22 +575,36 @@ module Stripe
|
|
482
575
|
.to_return(body: "", status: 200)
|
483
576
|
|
484
577
|
client = StripeClient.new
|
578
|
+
|
485
579
|
e = assert_raises Stripe::APIError do
|
486
580
|
client.execute_request(:post, "/v1/charges")
|
487
581
|
end
|
488
|
-
|
489
582
|
assert_equal 'Invalid response object from API: "" (HTTP response code was 200)', e.message
|
490
583
|
end
|
491
584
|
|
585
|
+
should "feed a request ID through to the error object" do
|
586
|
+
stub_request(:post, "#{Stripe.api_base}/v1/charges")
|
587
|
+
.to_return(body: JSON.generate(make_missing_id_error),
|
588
|
+
headers: { "Request-ID": "req_123" },
|
589
|
+
status: 400)
|
590
|
+
|
591
|
+
client = StripeClient.new
|
592
|
+
|
593
|
+
e = assert_raises Stripe::InvalidRequestError do
|
594
|
+
client.execute_request(:post, "/v1/charges")
|
595
|
+
end
|
596
|
+
assert_equal("req_123", e.request_id)
|
597
|
+
end
|
598
|
+
|
492
599
|
should "handle low level error" do
|
493
600
|
stub_request(:post, "#{Stripe.api_base}/v1/charges")
|
494
601
|
.to_raise(Errno::ECONNREFUSED.new)
|
495
602
|
|
496
603
|
client = StripeClient.new
|
604
|
+
|
497
605
|
e = assert_raises Stripe::APIConnectionError do
|
498
606
|
client.execute_request(:post, "/v1/charges")
|
499
607
|
end
|
500
|
-
|
501
608
|
assert_equal StripeClient::ERROR_MESSAGE_CONNECTION % Stripe.api_base +
|
502
609
|
"\n\n(Network error: Connection refused)",
|
503
610
|
e.message
|
@@ -508,10 +615,10 @@ module Stripe
|
|
508
615
|
.to_return(body: JSON.generate(bar: "foo"), status: 500)
|
509
616
|
|
510
617
|
client = StripeClient.new
|
618
|
+
|
511
619
|
e = assert_raises Stripe::APIError do
|
512
620
|
client.execute_request(:post, "/v1/charges")
|
513
621
|
end
|
514
|
-
|
515
622
|
assert_equal 'Invalid response object from API: "{\"bar\":\"foo\"}" (HTTP response code was 500)', e.message
|
516
623
|
end
|
517
624
|
|
@@ -521,6 +628,7 @@ module Stripe
|
|
521
628
|
|
522
629
|
stub_request(:post, "#{Stripe.api_base}/v1/charges")
|
523
630
|
.to_return(body: JSON.generate(data), status: 400)
|
631
|
+
|
524
632
|
client = StripeClient.new
|
525
633
|
|
526
634
|
e = assert_raises Stripe::IdempotencyError do
|
@@ -533,75 +641,81 @@ module Stripe
|
|
533
641
|
should "raise InvalidRequestError on other 400s" do
|
534
642
|
stub_request(:post, "#{Stripe.api_base}/v1/charges")
|
535
643
|
.to_return(body: JSON.generate(make_missing_id_error), status: 400)
|
644
|
+
|
536
645
|
client = StripeClient.new
|
537
|
-
|
646
|
+
|
647
|
+
e = assert_raises Stripe::InvalidRequestError do
|
538
648
|
client.execute_request(:post, "/v1/charges")
|
539
|
-
rescue Stripe::InvalidRequestError => e
|
540
|
-
assert_equal(400, e.http_status)
|
541
|
-
assert_equal(true, e.json_body.is_a?(Hash))
|
542
649
|
end
|
650
|
+
assert_equal(400, e.http_status)
|
651
|
+
assert_equal(true, e.json_body.is_a?(Hash))
|
543
652
|
end
|
544
653
|
|
545
654
|
should "raise AuthenticationError on 401" do
|
546
655
|
stub_request(:post, "#{Stripe.api_base}/v1/charges")
|
547
656
|
.to_return(body: JSON.generate(make_missing_id_error), status: 401)
|
657
|
+
|
548
658
|
client = StripeClient.new
|
549
|
-
|
659
|
+
|
660
|
+
e = assert_raises Stripe::AuthenticationError do
|
550
661
|
client.execute_request(:post, "/v1/charges")
|
551
|
-
rescue Stripe::AuthenticationError => e
|
552
|
-
assert_equal(401, e.http_status)
|
553
|
-
assert_equal(true, e.json_body.is_a?(Hash))
|
554
662
|
end
|
663
|
+
assert_equal(401, e.http_status)
|
664
|
+
assert_equal(true, e.json_body.is_a?(Hash))
|
555
665
|
end
|
556
666
|
|
557
667
|
should "raise CardError on 402" do
|
558
668
|
stub_request(:post, "#{Stripe.api_base}/v1/charges")
|
559
669
|
.to_return(body: JSON.generate(make_invalid_exp_year_error), status: 402)
|
670
|
+
|
560
671
|
client = StripeClient.new
|
561
|
-
|
672
|
+
|
673
|
+
e = assert_raises Stripe::CardError do
|
562
674
|
client.execute_request(:post, "/v1/charges")
|
563
|
-
rescue Stripe::CardError => e
|
564
|
-
assert_equal(402, e.http_status)
|
565
|
-
assert_equal(true, e.json_body.is_a?(Hash))
|
566
|
-
assert_equal("invalid_expiry_year", e.code)
|
567
|
-
assert_equal("exp_year", e.param)
|
568
675
|
end
|
676
|
+
assert_equal(402, e.http_status)
|
677
|
+
assert_equal(true, e.json_body.is_a?(Hash))
|
678
|
+
assert_equal("invalid_expiry_year", e.code)
|
679
|
+
assert_equal("exp_year", e.param)
|
569
680
|
end
|
570
681
|
|
571
682
|
should "raise PermissionError on 403" do
|
572
683
|
stub_request(:post, "#{Stripe.api_base}/v1/charges")
|
573
684
|
.to_return(body: JSON.generate(make_missing_id_error), status: 403)
|
685
|
+
|
574
686
|
client = StripeClient.new
|
575
|
-
|
687
|
+
|
688
|
+
e = assert_raises Stripe::PermissionError do
|
576
689
|
client.execute_request(:post, "/v1/charges")
|
577
|
-
rescue Stripe::PermissionError => e
|
578
|
-
assert_equal(403, e.http_status)
|
579
|
-
assert_equal(true, e.json_body.is_a?(Hash))
|
580
690
|
end
|
691
|
+
assert_equal(403, e.http_status)
|
692
|
+
assert_equal(true, e.json_body.is_a?(Hash))
|
581
693
|
end
|
582
694
|
|
583
695
|
should "raise InvalidRequestError on 404" do
|
584
696
|
stub_request(:post, "#{Stripe.api_base}/v1/charges")
|
585
697
|
.to_return(body: JSON.generate(make_missing_id_error), status: 404)
|
698
|
+
|
586
699
|
client = StripeClient.new
|
587
|
-
|
700
|
+
|
701
|
+
e = assert_raises Stripe::InvalidRequestError do
|
588
702
|
client.execute_request(:post, "/v1/charges")
|
589
|
-
rescue Stripe::InvalidRequestError => e
|
590
|
-
assert_equal(404, e.http_status)
|
591
|
-
assert_equal(true, e.json_body.is_a?(Hash))
|
592
703
|
end
|
704
|
+
assert_equal(404, e.http_status)
|
705
|
+
assert_equal(true, e.json_body.is_a?(Hash))
|
593
706
|
end
|
594
707
|
|
595
708
|
should "raise RateLimitError on 429" do
|
596
709
|
stub_request(:post, "#{Stripe.api_base}/v1/charges")
|
597
710
|
.to_return(body: JSON.generate(make_rate_limit_error), status: 429)
|
711
|
+
|
598
712
|
client = StripeClient.new
|
599
|
-
|
713
|
+
|
714
|
+
e = assert_raises Stripe::RateLimitError do
|
600
715
|
client.execute_request(:post, "/v1/charges")
|
601
|
-
rescue Stripe::RateLimitError => e
|
602
|
-
assert_equal(429, e.http_status)
|
603
|
-
assert_equal(true, e.json_body.is_a?(Hash))
|
604
716
|
end
|
717
|
+
assert_equal(429, e.http_status)
|
718
|
+
assert_equal(true, e.json_body.is_a?(Hash))
|
605
719
|
end
|
606
720
|
|
607
721
|
should "raise OAuth::InvalidRequestError when error is a string with value 'invalid_request'" do
|
@@ -610,6 +724,7 @@ module Stripe
|
|
610
724
|
error_description: "No grant type specified"), status: 400)
|
611
725
|
|
612
726
|
client = StripeClient.new
|
727
|
+
|
613
728
|
opts = { api_base: Stripe.connect_base }
|
614
729
|
e = assert_raises Stripe::OAuth::InvalidRequestError do
|
615
730
|
client.execute_request(:post, "/oauth/token", opts)
|
@@ -625,6 +740,7 @@ module Stripe
|
|
625
740
|
error_description: "This authorization code has already been used. All tokens issued with this code have been revoked."), status: 400)
|
626
741
|
|
627
742
|
client = StripeClient.new
|
743
|
+
|
628
744
|
opts = { api_base: Stripe.connect_base }
|
629
745
|
e = assert_raises Stripe::OAuth::InvalidGrantError do
|
630
746
|
client.execute_request(:post, "/oauth/token", opts)
|
@@ -641,6 +757,7 @@ module Stripe
|
|
641
757
|
error_description: "This application is not connected to stripe account acct_19tLK7DSlTMT26Mk, or that account does not exist."), status: 401)
|
642
758
|
|
643
759
|
client = StripeClient.new
|
760
|
+
|
644
761
|
opts = { api_base: Stripe.connect_base }
|
645
762
|
e = assert_raises Stripe::OAuth::InvalidClientError do
|
646
763
|
client.execute_request(:post, "/oauth/deauthorize", opts)
|
@@ -657,6 +774,7 @@ module Stripe
|
|
657
774
|
error_description: "Something."), status: 401)
|
658
775
|
|
659
776
|
client = StripeClient.new
|
777
|
+
|
660
778
|
opts = { api_base: Stripe.connect_base }
|
661
779
|
e = assert_raises Stripe::OAuth::OAuthError do
|
662
780
|
client.execute_request(:post, "/oauth/deauthorize", opts)
|
@@ -817,6 +935,22 @@ module Stripe
|
|
817
935
|
end
|
818
936
|
end
|
819
937
|
|
938
|
+
context "#connection_manager" do
|
939
|
+
should "warn that #connection_manager is deprecated" do
|
940
|
+
old_stderr = $stderr
|
941
|
+
$stderr = StringIO.new
|
942
|
+
begin
|
943
|
+
client = StripeClient.new
|
944
|
+
client.connection_manager
|
945
|
+
message = "NOTE: Stripe::StripeClient#connection_manager is " \
|
946
|
+
"deprecated"
|
947
|
+
assert_match Regexp.new(message), $stderr.string
|
948
|
+
ensure
|
949
|
+
$stderr = old_stderr
|
950
|
+
end
|
951
|
+
end
|
952
|
+
end
|
953
|
+
|
820
954
|
context "#request" do
|
821
955
|
should "return a result and response object" do
|
822
956
|
stub_request(:post, "#{Stripe.api_base}/v1/charges")
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stripe
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.
|
4
|
+
version: 5.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stripe
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-10-03 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Stripe is the easiest way to accept payments online. See https://stripe.com
|
14
14
|
for details.
|