stripe 1.27.2 → 5.33.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (192) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +942 -0
  3. data/CODE_OF_CONDUCT.md +77 -0
  4. data/Gemfile +29 -4
  5. data/History.txt +1 -361
  6. data/README.md +349 -0
  7. data/Rakefile +33 -3
  8. data/VERSION +1 -1
  9. data/bin/stripe-console +14 -5
  10. data/lib/data/ca-certificates.crt +4016 -5138
  11. data/lib/stripe.rb +102 -320
  12. data/lib/stripe/api_operations/create.rb +5 -9
  13. data/lib/stripe/api_operations/delete.rb +32 -4
  14. data/lib/stripe/api_operations/list.rb +11 -9
  15. data/lib/stripe/api_operations/nested_resource.rb +73 -0
  16. data/lib/stripe/api_operations/request.rb +66 -11
  17. data/lib/stripe/api_operations/save.rb +97 -0
  18. data/lib/stripe/api_resource.rb +96 -12
  19. data/lib/stripe/connection_manager.rb +164 -0
  20. data/lib/stripe/error_object.rb +94 -0
  21. data/lib/stripe/errors.rb +177 -0
  22. data/lib/stripe/instrumentation.rb +82 -0
  23. data/lib/stripe/list_object.rb +118 -13
  24. data/lib/stripe/multipart_encoder.rb +131 -0
  25. data/lib/stripe/oauth.rb +65 -0
  26. data/lib/stripe/object_types.rb +104 -0
  27. data/lib/stripe/resources.rb +87 -0
  28. data/lib/stripe/resources/account.rb +170 -0
  29. data/lib/stripe/resources/account_link.rb +10 -0
  30. data/lib/stripe/resources/alipay_account.rb +34 -0
  31. data/lib/stripe/resources/apple_pay_domain.rb +17 -0
  32. data/lib/stripe/resources/application_fee.rb +14 -0
  33. data/lib/stripe/resources/application_fee_refund.rb +30 -0
  34. data/lib/stripe/resources/balance.rb +8 -0
  35. data/lib/stripe/resources/balance_transaction.rb +10 -0
  36. data/lib/stripe/resources/bank_account.rb +43 -0
  37. data/lib/stripe/resources/billing_portal/configuration.rb +14 -0
  38. data/lib/stripe/resources/billing_portal/session.rb +12 -0
  39. data/lib/stripe/resources/bitcoin_receiver.rb +24 -0
  40. data/lib/stripe/resources/bitcoin_transaction.rb +15 -0
  41. data/lib/stripe/resources/capability.rb +33 -0
  42. data/lib/stripe/resources/card.rb +38 -0
  43. data/lib/stripe/resources/charge.rb +23 -0
  44. data/lib/stripe/resources/checkout/session.rb +16 -0
  45. data/lib/stripe/resources/country_spec.rb +10 -0
  46. data/lib/stripe/resources/coupon.rb +13 -0
  47. data/lib/stripe/resources/credit_note.rb +33 -0
  48. data/lib/stripe/resources/credit_note_line_item.rb +7 -0
  49. data/lib/stripe/resources/customer.rb +41 -0
  50. data/lib/stripe/resources/customer_balance_transaction.rb +30 -0
  51. data/lib/stripe/resources/discount.rb +7 -0
  52. data/lib/stripe/resources/dispute.rb +22 -0
  53. data/lib/stripe/resources/ephemeral_key.rb +20 -0
  54. data/lib/stripe/resources/event.rb +10 -0
  55. data/lib/stripe/resources/exchange_rate.rb +10 -0
  56. data/lib/stripe/resources/file.rb +36 -0
  57. data/lib/stripe/resources/file_link.rb +12 -0
  58. data/lib/stripe/resources/identity/verification_report.rb +12 -0
  59. data/lib/stripe/resources/identity/verification_session.rb +35 -0
  60. data/lib/stripe/resources/invoice.rb +74 -0
  61. data/lib/stripe/resources/invoice_item.rb +13 -0
  62. data/lib/stripe/resources/invoice_line_item.rb +7 -0
  63. data/lib/stripe/resources/issuing/authorization.rb +34 -0
  64. data/lib/stripe/resources/issuing/card.rb +25 -0
  65. data/lib/stripe/resources/issuing/card_details.rb +9 -0
  66. data/lib/stripe/resources/issuing/cardholder.rb +14 -0
  67. data/lib/stripe/resources/issuing/dispute.rb +25 -0
  68. data/lib/stripe/resources/issuing/transaction.rb +13 -0
  69. data/lib/stripe/resources/line_item.rb +7 -0
  70. data/lib/stripe/resources/login_link.rb +14 -0
  71. data/lib/stripe/resources/mandate.rb +8 -0
  72. data/lib/stripe/resources/order.rb +33 -0
  73. data/lib/stripe/resources/order_return.rb +10 -0
  74. data/lib/stripe/resources/payment_intent.rb +43 -0
  75. data/lib/stripe/resources/payment_method.rb +33 -0
  76. data/lib/stripe/resources/payout.rb +33 -0
  77. data/lib/stripe/resources/person.rb +31 -0
  78. data/lib/stripe/resources/plan.rb +13 -0
  79. data/lib/stripe/resources/price.rb +12 -0
  80. data/lib/stripe/resources/product.rb +13 -0
  81. data/lib/stripe/resources/promotion_code.rb +12 -0
  82. data/lib/stripe/resources/radar/early_fraud_warning.rb +12 -0
  83. data/lib/stripe/resources/radar/value_list.rb +15 -0
  84. data/lib/stripe/resources/radar/value_list_item.rb +14 -0
  85. data/lib/stripe/resources/recipient.rb +14 -0
  86. data/lib/stripe/resources/recipient_transfer.rb +7 -0
  87. data/lib/stripe/resources/refund.rb +12 -0
  88. data/lib/stripe/resources/reporting/report_run.rb +13 -0
  89. data/lib/stripe/resources/reporting/report_type.rb +13 -0
  90. data/lib/stripe/resources/reversal.rb +29 -0
  91. data/lib/stripe/resources/review.rb +21 -0
  92. data/lib/stripe/resources/setup_attempt.rb +10 -0
  93. data/lib/stripe/resources/setup_intent.rb +33 -0
  94. data/lib/stripe/resources/sigma/scheduled_query_run.rb +16 -0
  95. data/lib/stripe/resources/sku.rb +13 -0
  96. data/lib/stripe/resources/source.rb +47 -0
  97. data/lib/stripe/resources/source_transaction.rb +7 -0
  98. data/lib/stripe/resources/subscription.rb +26 -0
  99. data/lib/stripe/resources/subscription_item.rb +26 -0
  100. data/lib/stripe/resources/subscription_schedule.rb +33 -0
  101. data/lib/stripe/resources/tax_id.rb +26 -0
  102. data/lib/stripe/resources/tax_rate.rb +12 -0
  103. data/lib/stripe/resources/terminal/connection_token.rb +12 -0
  104. data/lib/stripe/resources/terminal/location.rb +15 -0
  105. data/lib/stripe/resources/terminal/reader.rb +15 -0
  106. data/lib/stripe/resources/three_d_secure.rb +14 -0
  107. data/lib/stripe/resources/token.rb +10 -0
  108. data/lib/stripe/resources/topup.rb +23 -0
  109. data/lib/stripe/resources/transfer.rb +27 -0
  110. data/lib/stripe/resources/usage_record.rb +7 -0
  111. data/lib/stripe/resources/usage_record_summary.rb +7 -0
  112. data/lib/stripe/resources/webhook_endpoint.rb +13 -0
  113. data/lib/stripe/singleton_api_resource.rb +13 -7
  114. data/lib/stripe/stripe_client.rb +989 -0
  115. data/lib/stripe/stripe_configuration.rb +194 -0
  116. data/lib/stripe/stripe_object.rb +481 -148
  117. data/lib/stripe/stripe_response.rb +82 -0
  118. data/lib/stripe/util.rb +265 -70
  119. data/lib/stripe/version.rb +3 -1
  120. data/lib/stripe/webhook.rb +121 -0
  121. data/stripe.gemspec +35 -21
  122. metadata +118 -198
  123. data/.gitignore +0 -4
  124. data/.travis.yml +0 -22
  125. data/README.rdoc +0 -43
  126. data/gemfiles/default-with-activesupport.gemfile +0 -10
  127. data/gemfiles/json.gemfile +0 -12
  128. data/gemfiles/yajl.gemfile +0 -12
  129. data/lib/stripe/account.rb +0 -39
  130. data/lib/stripe/api_operations/update.rb +0 -19
  131. data/lib/stripe/application_fee.rb +0 -20
  132. data/lib/stripe/application_fee_refund.rb +0 -14
  133. data/lib/stripe/balance.rb +0 -4
  134. data/lib/stripe/balance_transaction.rb +0 -9
  135. data/lib/stripe/bank_account.rb +0 -19
  136. data/lib/stripe/bitcoin_receiver.rb +0 -20
  137. data/lib/stripe/bitcoin_transaction.rb +0 -9
  138. data/lib/stripe/card.rb +0 -21
  139. data/lib/stripe/charge.rb +0 -62
  140. data/lib/stripe/coupon.rb +0 -8
  141. data/lib/stripe/customer.rb +0 -75
  142. data/lib/stripe/dispute.rb +0 -16
  143. data/lib/stripe/errors/api_connection_error.rb +0 -4
  144. data/lib/stripe/errors/api_error.rb +0 -4
  145. data/lib/stripe/errors/authentication_error.rb +0 -4
  146. data/lib/stripe/errors/card_error.rb +0 -12
  147. data/lib/stripe/errors/invalid_request_error.rb +0 -11
  148. data/lib/stripe/errors/rate_limit_error.rb +0 -4
  149. data/lib/stripe/errors/stripe_error.rb +0 -26
  150. data/lib/stripe/event.rb +0 -5
  151. data/lib/stripe/file_upload.rb +0 -22
  152. data/lib/stripe/invoice.rb +0 -27
  153. data/lib/stripe/invoice_item.rb +0 -8
  154. data/lib/stripe/order.rb +0 -19
  155. data/lib/stripe/plan.rb +0 -8
  156. data/lib/stripe/product.rb +0 -16
  157. data/lib/stripe/recipient.rb +0 -12
  158. data/lib/stripe/refund.rb +0 -7
  159. data/lib/stripe/reversal.rb +0 -14
  160. data/lib/stripe/sku.rb +0 -8
  161. data/lib/stripe/subscription.rb +0 -25
  162. data/lib/stripe/token.rb +0 -5
  163. data/lib/stripe/transfer.rb +0 -17
  164. data/test/stripe/account_test.rb +0 -118
  165. data/test/stripe/api_resource_test.rb +0 -632
  166. data/test/stripe/application_fee_refund_test.rb +0 -47
  167. data/test/stripe/application_fee_test.rb +0 -22
  168. data/test/stripe/balance_test.rb +0 -11
  169. data/test/stripe/bitcoin_receiver_test.rb +0 -61
  170. data/test/stripe/bitcoin_transaction_test.rb +0 -29
  171. data/test/stripe/charge_refund_test.rb +0 -55
  172. data/test/stripe/charge_test.rb +0 -118
  173. data/test/stripe/coupon_test.rb +0 -20
  174. data/test/stripe/customer_card_test.rb +0 -63
  175. data/test/stripe/customer_test.rb +0 -88
  176. data/test/stripe/dispute_test.rb +0 -45
  177. data/test/stripe/file_upload_test.rb +0 -28
  178. data/test/stripe/invoice_test.rb +0 -40
  179. data/test/stripe/list_object_test.rb +0 -16
  180. data/test/stripe/metadata_test.rb +0 -129
  181. data/test/stripe/order_test.rb +0 -52
  182. data/test/stripe/product_test.rb +0 -41
  183. data/test/stripe/recipient_card_test.rb +0 -57
  184. data/test/stripe/refund_test.rb +0 -55
  185. data/test/stripe/reversal_test.rb +0 -47
  186. data/test/stripe/sku_test.rb +0 -24
  187. data/test/stripe/stripe_object_test.rb +0 -28
  188. data/test/stripe/subscription_test.rb +0 -72
  189. data/test/stripe/transfer_test.rb +0 -25
  190. data/test/stripe/util_test.rb +0 -34
  191. data/test/test_data.rb +0 -666
  192. data/test/test_helper.rb +0 -41
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stripe
4
+ # StripeResponse encapsulates some vitals of a response that came back from
5
+ # the Stripe API.
6
+ class StripeResponse
7
+ # Headers provides an access wrapper to an API response's header data. It
8
+ # mainly exists so that we don't need to expose the entire
9
+ # `Net::HTTPResponse` object while still getting some of its benefits like
10
+ # case-insensitive access to header names and flattening of header values.
11
+ class Headers
12
+ # Initializes a Headers object from a Net::HTTP::HTTPResponse object.
13
+ def self.from_net_http(resp)
14
+ new(resp.to_hash)
15
+ end
16
+
17
+ # `hash` is expected to be a hash mapping header names to arrays of
18
+ # header values. This is the default format generated by calling
19
+ # `#to_hash` on a `Net::HTTPResponse` object because headers can be
20
+ # repeated multiple times. Using `#[]` will collapse values down to just
21
+ # the first.
22
+ def initialize(hash)
23
+ if !hash.is_a?(Hash) ||
24
+ !hash.keys.all? { |n| n.is_a?(String) } ||
25
+ !hash.values.all? { |a| a.is_a?(Array) } ||
26
+ !hash.values.all? { |a| a.all? { |v| v.is_a?(String) } }
27
+ raise ArgumentError,
28
+ "expect hash to be a map of string header names to arrays of " \
29
+ "header values"
30
+ end
31
+
32
+ @hash = {}
33
+
34
+ # This shouldn't be strictly necessary because `Net::HTTPResponse` will
35
+ # produce a hash with all headers downcased, but do it anyway just in
36
+ # case an object of this class was constructed manually.
37
+ #
38
+ # Also has the effect of duplicating the hash, which is desirable for a
39
+ # little extra object safety.
40
+ hash.each do |k, v|
41
+ @hash[k.downcase] = v
42
+ end
43
+ end
44
+
45
+ def [](name)
46
+ values = @hash[name.downcase]
47
+ if values && values.count > 1
48
+ warn("Duplicate header values for `#{name}`; returning only first")
49
+ end
50
+ values ? values.first : nil
51
+ end
52
+ end
53
+
54
+ # The data contained by the HTTP body of the response deserialized from
55
+ # JSON.
56
+ attr_accessor :data
57
+
58
+ # The raw HTTP body of the response.
59
+ attr_accessor :http_body
60
+
61
+ # A Hash of the HTTP headers of the response.
62
+ attr_accessor :http_headers
63
+
64
+ # The integer HTTP status code of the response.
65
+ attr_accessor :http_status
66
+
67
+ # The Stripe request ID of the response.
68
+ attr_accessor :request_id
69
+
70
+ # Initializes a StripeResponse object from a Net::HTTP::HTTPResponse
71
+ # object.
72
+ def self.from_net_http(http_resp)
73
+ resp = StripeResponse.new
74
+ resp.data = JSON.parse(http_resp.body, symbolize_names: true)
75
+ resp.http_body = http_resp.body
76
+ resp.http_headers = Headers.from_net_http(http_resp)
77
+ resp.http_status = http_resp.code.to_i
78
+ resp.request_id = http_resp["request-id"]
79
+ resp
80
+ end
81
+ end
82
+ end
data/lib/stripe/util.rb CHANGED
@@ -1,78 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
1
5
  module Stripe
2
6
  module Util
3
- def self.objects_to_ids(h)
4
- case h
7
+ # Options that a user is allowed to specify.
8
+ OPTS_USER_SPECIFIED = Set[
9
+ :api_key,
10
+ :idempotency_key,
11
+ :stripe_account,
12
+ :stripe_version
13
+ ].freeze
14
+
15
+ # Options that should be copyable from one StripeObject to another
16
+ # including options that may be internal.
17
+ OPTS_COPYABLE = (
18
+ OPTS_USER_SPECIFIED + Set[:api_base]
19
+ ).freeze
20
+
21
+ # Options that should be persisted between API requests. This includes
22
+ # client, which is an object containing an HTTP client to reuse.
23
+ OPTS_PERSISTABLE = (
24
+ OPTS_USER_SPECIFIED + Set[:client] - Set[:idempotency_key]
25
+ ).freeze
26
+
27
+ def self.objects_to_ids(obj)
28
+ case obj
5
29
  when APIResource
6
- h.id
30
+ obj.id
7
31
  when Hash
8
32
  res = {}
9
- h.each { |k, v| res[k] = objects_to_ids(v) unless v.nil? }
33
+ obj.each { |k, v| res[k] = objects_to_ids(v) unless v.nil? }
10
34
  res
11
35
  when Array
12
- h.map { |v| objects_to_ids(v) }
36
+ obj.map { |v| objects_to_ids(v) }
13
37
  else
14
- h
38
+ obj
15
39
  end
16
40
  end
17
41
 
18
42
  def self.object_classes
19
- @object_classes ||= {
20
- # data structures
21
- 'list' => ListObject,
22
-
23
- # business objects
24
- 'account' => Account,
25
- 'application_fee' => ApplicationFee,
26
- 'balance' => Balance,
27
- 'balance_transaction' => BalanceTransaction,
28
- 'bank_account' => BankAccount,
29
- 'card' => Card,
30
- 'charge' => Charge,
31
- 'coupon' => Coupon,
32
- 'customer' => Customer,
33
- 'event' => Event,
34
- 'fee_refund' => ApplicationFeeRefund,
35
- 'invoiceitem' => InvoiceItem,
36
- 'invoice' => Invoice,
37
- 'plan' => Plan,
38
- 'recipient' => Recipient,
39
- 'refund' => Refund,
40
- 'subscription' => Subscription,
41
- 'file_upload' => FileUpload,
42
- 'token' => Token,
43
- 'transfer' => Transfer,
44
- 'transfer_reversal' => Reversal,
45
- 'bitcoin_receiver' => BitcoinReceiver,
46
- 'bitcoin_transaction' => BitcoinTransaction,
47
- 'dispute' => Dispute,
48
- 'product' => Product,
49
- 'sku' => SKU,
50
- 'order' => Order,
51
- }
52
- end
53
-
54
- def self.convert_to_stripe_object(resp, opts)
55
- case resp
43
+ @object_classes ||= Stripe::ObjectTypes.object_names_to_classes
44
+ end
45
+
46
+ def self.object_name_matches_class?(object_name, klass)
47
+ Util.object_classes[object_name] == klass
48
+ end
49
+
50
+ # Converts a hash of fields or an array of hashes into a +StripeObject+ or
51
+ # array of +StripeObject+s. These new objects will be created as a concrete
52
+ # type as dictated by their `object` field (e.g. an `object` value of
53
+ # `charge` would create an instance of +Charge+), but if `object` is not
54
+ # present or of an unknown type, the newly created instance will fall back
55
+ # to being a +StripeObject+.
56
+ #
57
+ # ==== Attributes
58
+ #
59
+ # * +data+ - Hash of fields and values to be converted into a StripeObject.
60
+ # * +opts+ - Options for +StripeObject+ like an API key that will be reused
61
+ # on subsequent API calls.
62
+ def self.convert_to_stripe_object(data, opts = {})
63
+ opts = normalize_opts(opts)
64
+
65
+ case data
56
66
  when Array
57
- resp.map { |i| convert_to_stripe_object(i, opts) }
67
+ data.map { |i| convert_to_stripe_object(i, opts) }
58
68
  when Hash
59
- # Try converting to a known object class. If none available, fall back to generic StripeObject
60
- object_classes.fetch(resp[:object], StripeObject).construct_from(resp, opts)
69
+ # Try converting to a known object class. If none available, fall back
70
+ # to generic StripeObject
71
+ object_classes.fetch(data[:object], StripeObject)
72
+ .construct_from(data, opts)
61
73
  else
62
- resp
74
+ data
63
75
  end
64
76
  end
65
77
 
66
- def self.file_readable(file)
67
- # This is nominally equivalent to File.readable?, but that can
68
- # report incorrect results on some more oddball filesystems
69
- # (such as AFS)
70
- begin
71
- File.open(file) { |f| }
72
- rescue
73
- false
74
- else
75
- true
78
+ def self.log_error(message, data = {})
79
+ config = data.delete(:config) || Stripe.config
80
+ logger = config.logger || Stripe.logger
81
+ if !logger.nil? ||
82
+ !config.log_level.nil? && config.log_level <= Stripe::LEVEL_ERROR
83
+ log_internal(message, data, color: :cyan, level: Stripe::LEVEL_ERROR,
84
+ logger: Stripe.logger, out: $stderr)
85
+ end
86
+ end
87
+
88
+ def self.log_info(message, data = {})
89
+ config = data.delete(:config) || Stripe.config
90
+ logger = config.logger || Stripe.logger
91
+ if !logger.nil? ||
92
+ !config.log_level.nil? && config.log_level <= Stripe::LEVEL_INFO
93
+ log_internal(message, data, color: :cyan, level: Stripe::LEVEL_INFO,
94
+ logger: Stripe.logger, out: $stdout)
95
+ end
96
+ end
97
+
98
+ def self.log_debug(message, data = {})
99
+ config = data.delete(:config) || Stripe.config
100
+ logger = config.logger || Stripe.logger
101
+ if !logger.nil? ||
102
+ !config.log_level.nil? && config.log_level <= Stripe::LEVEL_DEBUG
103
+ log_internal(message, data, color: :blue, level: Stripe::LEVEL_DEBUG,
104
+ logger: Stripe.logger, out: $stdout)
76
105
  end
77
106
  end
78
107
 
@@ -81,7 +110,11 @@ module Stripe
81
110
  when Hash
82
111
  new_hash = {}
83
112
  object.each do |key, value|
84
- key = (key.to_sym rescue key) || key
113
+ key = (begin
114
+ key.to_sym
115
+ rescue StandardError
116
+ key
117
+ end) || key
85
118
  new_hash[key] = symbolize_names(value)
86
119
  end
87
120
  new_hash
@@ -92,14 +125,33 @@ module Stripe
92
125
  end
93
126
  end
94
127
 
128
+ # Encodes a hash of parameters in a way that's suitable for use as query
129
+ # parameters in a URI or as form parameters in a request body. This mainly
130
+ # involves escaping special characters from parameter keys and values (e.g.
131
+ # `&`).
132
+ def self.encode_parameters(params)
133
+ Util.flatten_params(params)
134
+ .map { |k, v| "#{url_encode(k)}=#{url_encode(v)}" }.join("&")
135
+ end
136
+
137
+ # Encodes a string in a way that makes it suitable for use in a set of
138
+ # query parameters in a URI or in a set of form parameters in a request
139
+ # body.
95
140
  def self.url_encode(key)
96
- URI.escape(key.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
141
+ CGI.escape(key.to_s).
142
+ # Don't use strict form encoding by changing the square bracket control
143
+ # characters back to their literals. This is fine by the server, and
144
+ # makes these parameter strings easier to read.
145
+ gsub("%5B", "[").gsub("%5D", "]")
97
146
  end
98
147
 
99
- def self.flatten_params(params, parent_key=nil)
148
+ def self.flatten_params(params, parent_key = nil)
100
149
  result = []
150
+
151
+ # do not sort the final output because arrays (and arrays of hashes
152
+ # especially) can be order sensitive, but do sort incoming parameters
101
153
  params.each do |key, value|
102
- calculated_key = parent_key ? "#{parent_key}[#{url_encode(key)}]" : url_encode(key)
154
+ calculated_key = parent_key ? "#{parent_key}[#{key}]" : key.to_s
103
155
  if value.is_a?(Hash)
104
156
  result += flatten_params(value, calculated_key)
105
157
  elsif value.is_a?(Array)
@@ -108,25 +160,38 @@ module Stripe
108
160
  result << [calculated_key, value]
109
161
  end
110
162
  end
163
+
111
164
  result
112
165
  end
113
166
 
114
167
  def self.flatten_params_array(value, calculated_key)
115
168
  result = []
116
- value.each do |elem|
169
+ value.each_with_index do |elem, i|
117
170
  if elem.is_a?(Hash)
118
- result += flatten_params(elem, "#{calculated_key}[]")
171
+ result += flatten_params(elem, "#{calculated_key}[#{i}]")
119
172
  elsif elem.is_a?(Array)
120
173
  result += flatten_params_array(elem, calculated_key)
121
174
  else
122
- result << ["#{calculated_key}[]", elem]
175
+ result << ["#{calculated_key}[#{i}]", elem]
123
176
  end
124
177
  end
125
178
  result
126
179
  end
127
180
 
181
+ # `Time.now` can be unstable in cases like an administrator manually
182
+ # updating its value or a reconcilation via NTP. For this reason, prefer
183
+ # the use of the system's monotonic clock especially where comparing times
184
+ # to calculate an elapsed duration.
185
+ #
186
+ # Shortcut for getting monotonic time, mostly for purposes of line length
187
+ # and test stubbing. Returns time in seconds since the event used for
188
+ # monotonic reference purposes by the platform (e.g. system boot time).
189
+ def self.monotonic_time
190
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
191
+ end
192
+
128
193
  def self.normalize_id(id)
129
- if id.kind_of?(Hash) # overloaded id
194
+ if id.is_a?(Hash) # overloaded id
130
195
  params_hash = id.dup
131
196
  id = params_hash.delete(:id)
132
197
  else
@@ -140,23 +205,153 @@ module Stripe
140
205
  def self.normalize_opts(opts)
141
206
  case opts
142
207
  when String
143
- {:api_key => opts}
208
+ { api_key: opts }
144
209
  when Hash
145
- check_api_key!(opts.fetch(:api_key)) if opts.has_key?(:api_key)
210
+ check_api_key!(opts.fetch(:api_key)) if opts.key?(:api_key)
146
211
  opts.clone
147
212
  else
148
- raise TypeError.new('normalize_opts expects a string or a hash')
213
+ raise TypeError, "normalize_opts expects a string or a hash"
149
214
  end
150
215
  end
151
216
 
152
217
  def self.check_string_argument!(key)
153
- raise TypeError.new("argument must be a string") unless key.is_a?(String)
218
+ raise TypeError, "argument must be a string" unless key.is_a?(String)
219
+
154
220
  key
155
221
  end
156
222
 
157
223
  def self.check_api_key!(key)
158
- raise TypeError.new("api_key must be a string") unless key.is_a?(String)
224
+ raise TypeError, "api_key must be a string" unless key.is_a?(String)
225
+
159
226
  key
160
227
  end
228
+
229
+ # Normalizes header keys so that they're all lower case and each
230
+ # hyphen-delimited section starts with a single capitalized letter. For
231
+ # example, `request-id` becomes `Request-Id`. This is useful for extracting
232
+ # certain key values when the user could have set them with a variety of
233
+ # diffent naming schemes.
234
+ def self.normalize_headers(headers)
235
+ headers.each_with_object({}) do |(k, v), new_headers|
236
+ k = k.to_s.tr("_", "-") if k.is_a?(Symbol)
237
+ k = k.split("-").reject(&:empty?).map(&:capitalize).join("-")
238
+
239
+ new_headers[k] = v
240
+ end
241
+ end
242
+
243
+ # Generates a Dashboard link to inspect a request ID based off of a request
244
+ # ID value and an API key, which is used to attempt to extract whether the
245
+ # environment is livemode or testmode.
246
+ def self.request_id_dashboard_url(request_id, api_key)
247
+ env = !api_key.nil? && api_key.start_with?("sk_live") ? "live" : "test"
248
+ "https://dashboard.stripe.com/#{env}/logs/#{request_id}"
249
+ end
250
+
251
+ # Constant time string comparison to prevent timing attacks
252
+ # Code borrowed from ActiveSupport
253
+ def self.secure_compare(str_a, str_b)
254
+ return false unless str_a.bytesize == str_b.bytesize
255
+
256
+ l = str_a.unpack "C#{str_a.bytesize}"
257
+
258
+ res = 0
259
+ str_b.each_byte { |byte| res |= byte ^ l.shift }
260
+ res.zero?
261
+ end
262
+
263
+ #
264
+ # private
265
+ #
266
+
267
+ COLOR_CODES = {
268
+ black: 0, light_black: 60,
269
+ red: 1, light_red: 61,
270
+ green: 2, light_green: 62,
271
+ yellow: 3, light_yellow: 63,
272
+ blue: 4, light_blue: 64,
273
+ magenta: 5, light_magenta: 65,
274
+ cyan: 6, light_cyan: 66,
275
+ white: 7, light_white: 67,
276
+ default: 9,
277
+ }.freeze
278
+ private_constant :COLOR_CODES
279
+
280
+ # Uses an ANSI escape code to colorize text if it's going to be sent to a
281
+ # TTY.
282
+ def self.colorize(val, color, isatty)
283
+ return val unless isatty
284
+
285
+ mode = 0 # default
286
+ foreground = 30 + COLOR_CODES.fetch(color)
287
+ background = 40 + COLOR_CODES.fetch(:default)
288
+
289
+ "\033[#{mode};#{foreground};#{background}m#{val}\033[0m"
290
+ end
291
+ private_class_method :colorize
292
+
293
+ # Turns an integer log level into a printable name.
294
+ def self.level_name(level)
295
+ case level
296
+ when LEVEL_DEBUG then "debug"
297
+ when LEVEL_ERROR then "error"
298
+ when LEVEL_INFO then "info"
299
+ else level
300
+ end
301
+ end
302
+ private_class_method :level_name
303
+
304
+ def self.log_internal(message, data = {}, color:, level:, logger:, out:)
305
+ data_str = data.reject { |_k, v| v.nil? }
306
+ .map do |(k, v)|
307
+ format("%<key>s=%<value>s",
308
+ key: colorize(k, color, logger.nil? && !out.nil? && out.isatty),
309
+ value: wrap_logfmt_value(v))
310
+ end.join(" ")
311
+
312
+ if !logger.nil?
313
+ # the library's log levels are mapped to the same values as the
314
+ # standard library's logger
315
+ logger.log(level,
316
+ format("message=%<message>s %<data_str>s",
317
+ message: wrap_logfmt_value(message),
318
+ data_str: data_str))
319
+ elsif out.isatty
320
+ out.puts format("%<level>s %<message>s %<data_str>s",
321
+ level: colorize(level_name(level)[0, 4].upcase,
322
+ color, out.isatty),
323
+ message: message,
324
+ data_str: data_str)
325
+ else
326
+ out.puts format("message=%<message>s level=%<level>s %<data_str>s",
327
+ message: wrap_logfmt_value(message),
328
+ level: level_name(level),
329
+ data_str: data_str)
330
+ end
331
+ end
332
+ private_class_method :log_internal
333
+
334
+ # Wraps a value in double quotes if it looks sufficiently complex so that
335
+ # it can be read by logfmt parsers.
336
+ def self.wrap_logfmt_value(val)
337
+ # If value is any kind of number, just allow it to be formatted directly
338
+ # to a string (this will handle integers or floats).
339
+ return val if val.is_a?(Numeric)
340
+
341
+ # Hopefully val is a string, but protect in case it's not.
342
+ val = val.to_s
343
+
344
+ if %r{[^\w\-/]} =~ val
345
+ # If the string contains any special characters, escape any double
346
+ # quotes it has, remove newlines, and wrap the whole thing in quotes.
347
+ format(%("%<value>s"), value: val.gsub('"', '\"').delete("\n"))
348
+ else
349
+ # Otherwise use the basic value if it looks like a standard set of
350
+ # characters (and allow a few special characters like hyphens, and
351
+ # slashes)
352
+ val
353
+ end
354
+ end
355
+ private_class_method :wrap_logfmt_value
161
356
  end
162
357
  end