stripe-ruby V5 (#815)

* Convert library to use built-in `Net::HTTP`

Moves the library off of Faraday and over onto the standard library's
built-in `Net::HTTP` module. The upside of the transition is that we
break away from a few dependencies that have caused us a fair bit of
trouble in the past, the downside is that we need more of our own code
to do things (although surprisingly, not that much more).

The biggest new pieces are:

* `ConnectionManager`: A per-thread class that manages a connection to
  each Stripe infrastructure URL (like `api.stripe.com`,
  `connect.stripe.com`, etc.) so that we can reuse them between
  requests. It's also responsible for setting up and configuring new
  `Net::HTTP` connections, which is a little more heavyweight
  code-wise compared to other libraries. All of this could have lived in
  `StripeClient`, but I extracted it because that class has gotten so
  big.

* `MultipartEncoder`: A class that does multipart form encoding for file
  uploads. Unfortunately, Ruby doesn't bundle anything like this. I
  built this by referencing the Go implementation because the original
  RFC is not very detailed or well-written. I also made sure that it was
  behaving similarly to our other custom implementations like
  stripe-node, and that it can really upload a file outside the test
  suite.

There's some risk here in that it's easy to miss something across one of
these big transitions. I've tried to test out various error cases
through tests, but also by leaving scripts running as I terminate my
network connection and bring it back. That said, we'd certainly release
on a major version bump because some of the interface (like setting
`Stripe.default_client`) changes.

* Drop support for old versions of Ruby

Drops support for Ruby 2.1 (EOL March 31, 2017) and 2.2 (EOL March 31,
2018). They're removed from `.travis.yml` and the gemspec and RuboCop
configuration have also been updated to the new lower bound.

Most of the diff here are minor updates to styling as required by
RuboCop:

* String literals are frozen by default, so the `.freeze` we had
  everywhere is now considered redundant.

* We can now use Ruby 1.9 style hash syntax with string keys like `{
  "foo": "bar" }`.

* Converted a few heredocs over to use squiggly (leading whitespace
  removed) syntax.

As discussed in Slack, I didn't drop support for Ruby 2.3 (EOL March 31,
2019) as we still have quite a few users on it. As far as I know
dropping it doesn't get us access to any major syntax improvements or
anything, so it's probably not a big deal.

* Make `CardError`'s `code` parameter named instead of positional (#816)

Makes the `code` parameter on `CardError` named instead of positional.
This makes it more consistent with the rest of the constructor's
parameters and makes instantiating `CardError` from `StripeClient`
cleaner.

This is a minor breaking change so we're aiming to release it for the
next major version of stripe-ruby.

* Bump Rubocop to latest version (#818)

* Ruby minimum version increase followup (#819)

* Remove old deprecated methods (#820)

* Remove all alias for list methods (#823)

* Remove UsageRecord.create method (#826)

* Remove IssuerFraudRecord (#827)

* Add ErrorObject to StripeError exceptions (#811)

* Tweak retry logic to be a little more like stripe-node (#828)

Tweaks the retry logic to be a little more like stripe-node's. In
particular, we also retry under these conditions:

* If we receive a 500 on a non-`POST` request.
* If we receive a 503.

I made it slightly different from stripe-node which checks for a 500
with `>= 500`. I don't really like that -- if we want to retry specific
status codes we should be explicit about it.

We're actively re-examining ways on how to make it easier for clients to
figure out when to retry right now, but I figure V5 is a good time to
tweak this because the modifications change the method signature of
`should_retry?` slightly, and it's technically a public method.

* Fix inverted sign for 500 retries (#830)

I messed up in #828 by (1) accidentally flipping the comparison against
`:post` when checking whether to retry on 500, and (2) forgetting to
write new tests for the condition, which is how (1) got through.

This patch fixes both those problems.

* Remove a few more very old deprecated methods (#831)

I noticed that we had a couple of other deprecated methods on `Stripe`
and `StripeObject` that have been around for a long time. May as well
get rid of them too -- luckily they were using `Gem::Deprecate` so
they've been producing annoying deprecated warnings for quite a while
now.

* Remove extraneous slash at the end of the line

* Reset connections when connection-changing configuration changes (#829)

Adds a few basic features around connection and connection manager
management:

* `clear` on connection manager, which calls `finish` on each active
  connection and then disposes of it.

* A centralized cross-thread tracking system for connection managers in
  `StripeClient` and `clear_all_connection_managers` which clears all
  known connection managers across all threads in a thread-safe way.

The addition of these allow us to modify the implementation of some of
our configuration on `Stripe` so that it can reset all currently open
connections when its value changes.

This fixes a currently problem with the library whereby certain
configuration must be set before the first request or it remains fixed
on any open connections. For example, if `Stripe.proxy` is set after a
request is made from the library, it has no effect because the proxy
must have been set when the connection was originally being initialized.

The impetus for getting this out is that I noticed that we will need
this internally in a few places when we're upgrading to stripe-ruby V5.
Those spots used to be able to hack around the unavailability of this
feature by just accessing the Faraday connection directly and resetting
state on it, but in V5 `StripeClient#conn` is gone, and that's no longer
possible.

* Minor cleanup in `StripeClient` (#832)

I ended up having to relax the maximum method line length in a few
previous PRs, so I wanted to try one more cleanup pass in
`execute_request` to see if I could get it back at all.

The answer was "not by much" (without reducing clarity), but I found a
few places that could be tweaked. Unfortunately, ~50 lines is probably
the "right" length for this method in that you _could_ extract it
further, but you'd end up passing huge amounts of state all over the
place in method parameters, and it really wouldn't look that good.

* Do better bookkeeping when tracking state in `Thread.current` (#833)

This is largely just another cleanup patch, but does a couple main
things:

* Hoists the `last_response` value into thread state. This is a very
  minor nicety, but effectively makes `StripeClient` fully thread-safe,
  which seems like a minor nicety. Two calls to `#request` to the same
  `StripeObject` can now be executed on two different threads and their
  results won't interfere with each other.

* Moves state off one-off `Thread.current` keys and into a single one
  for the whole client which stores a new simple type of record called
  `ThreadContext`. Again, this doesn't change much, but adds some minor
  type safety and lets us document each field we expect to have in a
  thread's context.

* Add Invoice.list_upcoming_line_items method (#834)
This commit is contained in:
Brandur 2019-08-20 11:35:24 -07:00 committed by Olivier Bellone
parent 8b45b1d980
commit 44766516d9
123 changed files with 1544 additions and 999 deletions

View File

@ -2,21 +2,31 @@ inherit_from: .rubocop_todo.yml
AllCops:
DisplayCopNames: true
TargetRubyVersion: 2.1
TargetRubyVersion: 2.3
Layout/CaseIndentation:
EnforcedStyle: end
Layout/IndentArray:
Layout/IndentFirstArrayElement:
EnforcedStyle: consistent
Layout/IndentHash:
Layout/IndentFirstHashElement:
EnforcedStyle: consistent
# This can be re-enabled once we're 2.3+ only and can use the squiggly heredoc
# operator. Prior to that, Rubocop recommended bringing in a library like
# ActiveSupport to get heredoc indentation, which is just terrible.
Layout/IndentHeredoc:
Enabled: false
Metrics/ClassLength:
Exclude:
- "test/**/*.rb"
Metrics/LineLength:
Exclude:
- "test/**/*.rb"
- "lib/stripe/resources/**/*.rb"
- "test/**/*.rb"
Metrics/MethodLength:
# There's ~2 long methods in `StripeClient`. If we want to truncate those a
@ -33,6 +43,9 @@ Style/AccessModifierDeclarations:
Style/FrozenStringLiteralComment:
EnforcedStyle: always
Style/NumericPredicate:
Enabled: false
Style/StringLiterals:
EnforcedStyle: double_quotes

View File

@ -1,24 +1,25 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2019-05-24 10:18:48 -0700 using RuboCop version 0.57.2.
# on 2019-07-30 09:56:31 +0800 using RuboCop version 0.73.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
# Offense count: 20
# Offense count: 23
Metrics/AbcSize:
Max: 53
Max: 51
# Offense count: 31
# Offense count: 33
# Configuration parameters: CountComments, ExcludedMethods.
# ExcludedMethods: refine
Metrics/BlockLength:
Max: 498
Max: 509
# Offense count: 11
# Offense count: 12
# Configuration parameters: CountComments.
Metrics/ClassLength:
Max: 673
Max: 694
# Offense count: 12
Metrics/CyclomaticComplexity:
@ -29,10 +30,10 @@ Metrics/CyclomaticComplexity:
Metrics/ParameterLists:
Max: 7
# Offense count: 7
# Offense count: 8
Metrics/PerceivedComplexity:
Max: 17
# Offense count: 84
# Offense count: 86
Style/Documentation:
Enabled: false

View File

@ -1,13 +1,11 @@
language: ruby
rvm:
- 2.1
- 2.2
- 2.3
- 2.4
- 2.5
- 2.6
- jruby-9.0.5.0
- jruby-9.2.7.0
notifications:
email:
@ -25,8 +23,6 @@ cache:
- stripe-mock
before_install:
# Install bundler 1.x, because we need to support Ruby 2.1 for now
- gem install bundler -v "~> 1.0"
# Unpack and start stripe-mock so that the test suite can talk to it
- |
if [ ! -d "stripe-mock/stripe-mock_${STRIPE_MOCK_VERSION}" ]; then

14
Gemfile
View File

@ -7,6 +7,7 @@ gemspec
group :development do
gem "coveralls", require: false
gem "mocha", "~> 0.13.2"
gem "rack", ">= 2.0.6"
gem "rake"
gem "shoulda-context"
gem "test-unit"
@ -18,18 +19,7 @@ group :development do
# `Gemfile.lock` checked in, so to prevent good builds from suddenly going
# bad, pin to a specific version number here. Try to keep this relatively
# up-to-date, but it's not the end of the world if it's not.
# Note that 0.57.2 is the most recent version we can use until we drop
# support for Ruby 2.1.
gem "rubocop", "0.57.2"
# Rack 2.0+ requires Ruby >= 2.2.2 which is problematic for the test suite on
# older Ruby versions. Check Ruby the version here and put a maximum
# constraint on Rack if necessary.
if RUBY_VERSION >= "2.2.2"
gem "rack", ">= 2.0.6"
else
gem "rack", ">= 1.6.11", "< 2.0" # rubocop:disable Bundler/DuplicatedGem
end
gem "rubocop", "0.73"
platforms :mri do
gem "byebug"

View File

@ -39,7 +39,7 @@ gem build stripe.gemspec
### Requirements
- Ruby 2.1+.
- Ruby 2.3+.
### Bundler
@ -112,15 +112,13 @@ Stripe::Charge.retrieve(
)
```
### Configuring a Client
### Accessing a response object
While a default HTTP client is used by default, it's also possible to have the
library use any client supported by [Faraday][faraday] by initializing a
`Stripe::StripeClient` object and giving it a connection:
Get access to response objects by initializing a client and using its `request`
method:
```ruby
conn = Faraday.new
client = Stripe::StripeClient.new(conn)
client = Stripe::StripeClient.new
charge, resp = client.request do
Stripe::Charge.retrieve(
"ch_18atAXCdGbJFKhCuBAa4532Z",
@ -275,7 +273,6 @@ Update the bundled [stripe-mock] by editing the version number found in
[api-keys]: https://dashboard.stripe.com/account/apikeys
[connect]: https://stripe.com/connect
[curl]: http://curl.haxx.se/docs/caextract.html
[faraday]: https://github.com/lostisland/faraday
[idempotency-keys]: https://stripe.com/docs/api/ruby#idempotent_requests
[stripe-mock]: https://github.com/stripe/stripe-mock
[versioning]: https://stripe.com/docs/api/ruby#versioning

View File

@ -13,7 +13,8 @@ RuboCop::RakeTask.new
desc "Update bundled certs"
task :update_certs do
require "faraday"
require "net/http"
require "uri"
fetch_file "https://curl.haxx.se/ca/cacert.pem",
::File.expand_path("../lib/data/ca-certificates.crt", __FILE__)
@ -23,14 +24,14 @@ end
# helpers
#
def fetch_file(url, dest)
def fetch_file(uri, dest)
::File.open(dest, "w") do |file|
resp = Faraday.get(url)
unless resp.status == 200
abort("bad response when fetching: #{url}\n" \
"Status #{resp.status}: #{resp.body}")
resp = Net::HTTP.get_response(URI.parse(uri))
unless resp.code.to_i == 200
abort("bad response when fetching: #{uri}\n" \
"Status #{resp.code}: #{resp.body}")
end
file.write(resp.body)
puts "Successfully fetched: #{url}"
puts "Successfully fetched: #{uri}"
end
end

View File

@ -3,9 +3,9 @@
# Stripe Ruby bindings
# API spec at https://stripe.com/docs/api
require "cgi"
require "faraday"
require "json"
require "logger"
require "net/http"
require "openssl"
require "rbconfig"
require "securerandom"
@ -28,10 +28,13 @@ require "stripe/api_operations/save"
require "stripe/errors"
require "stripe/object_types"
require "stripe/util"
require "stripe/connection_manager"
require "stripe/multipart_encoder"
require "stripe/stripe_client"
require "stripe/stripe_object"
require "stripe/stripe_response"
require "stripe/list_object"
require "stripe/error_object"
require "stripe/api_resource"
require "stripe/singleton_api_resource"
require "stripe/webhook"
@ -70,9 +73,20 @@ module Stripe
@enable_telemetry = true
class << self
attr_accessor :stripe_account, :api_key, :api_base, :verify_ssl_certs,
:api_version, :client_id, :connect_base, :uploads_base,
:open_timeout, :read_timeout, :proxy
attr_accessor :api_key
attr_accessor :api_version
attr_accessor :client_id
attr_accessor :stripe_account
# These all get manual attribute writers so that we can reset connections
# if they change.
attr_reader :api_base
attr_reader :connect_base
attr_reader :open_timeout
attr_reader :proxy
attr_reader :read_timeout
attr_reader :uploads_base
attr_reader :verify_ssl_certs
attr_reader :max_network_retry_delay, :initial_network_retry_delay
end
@ -87,6 +101,11 @@ module Stripe
@app_info = info
end
def self.api_base=(api_base)
@api_base = api_base
StripeClient.clear_all_connection_managers
end
# The location of a file containing a bundle of CA certificates. By default
# the library will use an included bundle that can successfully validate
# Stripe certificates.
@ -99,6 +118,8 @@ module Stripe
# empty this field so a new store is initialized
@ca_store = nil
StripeClient.clear_all_connection_managers
end
# A certificate store initialized from the the bundle in #ca_bundle_path and
@ -118,6 +139,19 @@ module Stripe
end
end
def self.connection_base=(connection_base)
@connection_base = connection_base
StripeClient.clear_all_connection_managers
end
def self.enable_telemetry?
@enable_telemetry
end
def self.enable_telemetry=(val)
@enable_telemetry = val
end
# map to the same values as the standard library's logger
LEVEL_DEBUG = Logger::DEBUG
LEVEL_ERROR = Logger::ERROR
@ -172,12 +206,19 @@ module Stripe
@max_network_retries = val.to_i
end
def self.enable_telemetry?
@enable_telemetry
def self.open_timeout=(open_timeout)
@open_timeout = open_timeout
StripeClient.clear_all_connection_managers
end
def self.enable_telemetry=(val)
@enable_telemetry = val
def self.proxy=(proxy)
@proxy = proxy
StripeClient.clear_all_connection_managers
end
def self.read_timeout=(read_timeout)
@read_timeout = read_timeout
StripeClient.clear_all_connection_managers
end
# Sets some basic information about the running application that's sent along
@ -194,14 +235,14 @@ module Stripe
}
end
# DEPRECATED. Use `Util#encode_parameters` instead.
def self.uri_encode(params)
Util.encode_parameters(params)
def self.uploads_base=(uploads_base)
@uploads_base = uploads_base
StripeClient.clear_all_connection_managers
end
private_class_method :uri_encode
class << self
extend Gem::Deprecate
deprecate :uri_encode, "Stripe::Util#encode_parameters", 2016, 1
def self.verify_ssl_certs=(verify_ssl_certs)
@verify_ssl_certs = verify_ssl_certs
StripeClient.clear_all_connection_managers
end
end

View File

@ -19,12 +19,6 @@ module Stripe
obj
end
# The original version of #list was given the somewhat unfortunate name of
# #all, and this alias allows us to maintain backward compatibility (the
# choice was somewhat misleading in the way that it only returned a single
# page rather than all objects).
alias all list
end
end
end

View File

@ -0,0 +1,131 @@
# frozen_string_literal: true
module Stripe
# Manages connections across multiple hosts which is useful because the
# library may connect to multiple hosts during a typical session (main API,
# Connect, Uploads). Ruby doesn't provide an easy way to make this happen
# easily, so this class is designed to track what we're connected to and
# manage the lifecycle of those connections.
#
# Note that this class in itself is *not* thread safe. We expect it to be
# instantiated once per thread.
#
# Note also that this class doesn't currently clean up after itself because
# it expects to only ever have a few connections. It'd be possible to tank
# memory by constantly changing the value of `Stripe.api_base` or the like. A
# possible improvement might be to detect and prune old connections whenever
# a request is executed.
class ConnectionManager
def initialize
@active_connections = {}
end
# Finishes any active connections by closing their TCP connection and
# clears them from internal tracking.
def clear
@active_connections.each do |_, connection|
connection.finish
end
@active_connections = {}
end
# Gets a connection for a given URI. This is for internal use only as it's
# subject to change (we've moved between HTTP client schemes in the past
# and may do it again).
#
# `uri` is expected to be a string.
def connection_for(uri)
u = URI.parse(uri)
connection = @active_connections[[u.host, u.port]]
if connection.nil?
connection = create_connection(u)
# TODO: what happens after TTL?
connection.start
@active_connections[[u.host, u.port]] = connection
end
connection
end
# Executes an HTTP request to the given URI with the given method. Also
# allows a request body, headers, and query string to be specified.
def execute_request(method, uri, body: nil, headers: nil, query: nil)
# Perform some basic argument validation because it's easy to get
# confused between strings and hashes for things like body and query
# parameters.
raise ArgumentError, "method should be a symbol" \
unless method.is_a?(Symbol)
raise ArgumentError, "uri should be a string" \
unless uri.is_a?(String)
raise ArgumentError, "body should be a string" \
if body && !body.is_a?(String)
raise ArgumentError, "headers should be a hash" \
if headers && !headers.is_a?(Hash)
raise ArgumentError, "query should be a string" \
if query && !query.is_a?(String)
connection = connection_for(uri)
u = URI.parse(uri)
path = if query
u.path + "?" + query
else
u.path
end
connection.send_request(method.to_s.upcase, path, body, headers)
end
#
# private
#
# `uri` should be a parsed `URI` object.
private def create_connection(uri)
# These all come back as `nil` if no proxy is configured.
proxy_host, proxy_port, proxy_user, proxy_pass = proxy_parts
connection = Net::HTTP.new(uri.host, uri.port,
proxy_host, proxy_port,
proxy_user, proxy_pass)
connection.open_timeout = Stripe.open_timeout
connection.read_timeout = Stripe.read_timeout
connection.use_ssl = uri.scheme == "https"
if Stripe.verify_ssl_certs
connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
connection.cert_store = Stripe.ca_store
else
connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
unless @verify_ssl_warned
@verify_ssl_warned = true
warn("WARNING: Running without SSL cert verification. " \
"You should never do this in production. " \
"Execute `Stripe.verify_ssl_certs = true` to enable " \
"verification.")
end
end
connection
end
# `Net::HTTP` somewhat awkwardly requires each component of a proxy URI
# (host, port, etc.) rather than the URI itself. This method simply parses
# out those pieces to make passing them into a new connection a little less
# ugly.
private def proxy_parts
if Stripe.proxy.nil?
[nil, nil, nil, nil]
else
u = URI.parse(Stripe.proxy)
[u.host, u.port, u.user, u.password]
end
end
end
end

View File

@ -0,0 +1,94 @@
# frozen_string_literal: true
module Stripe
# Represents an error object as returned by the API.
#
# @see https://stripe.com/docs/api/errors
class ErrorObject < StripeObject
# Unlike other objects, we explicitly declare getter methods here. This
# is because the API doesn't return `null` values for fields on this
# object, rather the fields are omitted entirely. Not declaring the getter
# methods would cause users to run into `NoMethodError` exceptions and
# get in the way of generic error handling.
# For card errors, the ID of the failed charge.
def charge
@values[:charge]
end
# For some errors that could be handled programmatically, a short string
# indicating the error code reported.
def code
@values[:code]
end
# For card errors resulting from a card issuer decline, a short string
# indicating the card issuer's reason for the decline if they provide one.
def decline_code
@values[:decline_code]
end
# A URL to more information about the error code reported.
def doc_url
@values[:doc_url]
end
# A human-readable message providing more details about the error. For card
# errors, these messages can be shown to your users.
def message
@values[:message]
end
# If the error is parameter-specific, the parameter related to the error.
# For example, you can use this to display a message near the correct form
# field.
def param
@values[:param]
end
# The PaymentIntent object for errors returned on a request involving a
# PaymentIntent.
def payment_intent
@values[:payment_intent]
end
# The PaymentMethod object for errors returned on a request involving a
# PaymentMethod.
def payment_method
@values[:payment_method]
end
# The SetupIntent object for errors returned on a request involving a
# SetupIntent.
def setup_intent
@values[:setup_intent]
end
# The source object for errors returned on a request involving a source.
def source
@values[:source]
end
# The type of error returned. One of `api_connection_error`, `api_error`,
# `authentication_error`, `card_error`, `idempotency_error`,
# `invalid_request_error`, or `rate_limit_error`.
def type
@values[:type]
end
end
# Represents on OAuth error returned by the OAuth API.
#
# @see https://stripe.com/docs/connect/oauth-reference#post-token-errors
class OAuthErrorObject < StripeObject
# A unique error code per error type.
def error
@values[:error]
end
# A human readable description of the error.
def error_description
@values[:error_description]
end
end
end

View File

@ -11,6 +11,7 @@ module Stripe
attr_accessor :response
attr_reader :code
attr_reader :error
attr_reader :http_body
attr_reader :http_headers
attr_reader :http_status
@ -27,6 +28,13 @@ module Stripe
@json_body = json_body
@code = code
@request_id = @http_headers[:request_id]
@error = construct_error_object
end
def construct_error_object
return nil if @json_body.nil? || !@json_body.key?(:error)
ErrorObject.construct_from(@json_body[:error])
end
def to_s
@ -59,8 +67,7 @@ module Stripe
class CardError < StripeError
attr_reader :param
# TODO: make code a keyword arg in next major release
def initialize(message, param, code, http_status: nil, http_body: nil,
def initialize(message, param, code: nil, http_status: nil, http_body: nil,
json_body: nil, http_headers: nil)
super(message, http_status: http_status, http_body: http_body,
json_body: json_body, http_headers: http_headers,
@ -119,6 +126,12 @@ module Stripe
json_body: json_body, http_headers: http_headers,
code: code)
end
def construct_error_object
return nil if @json_body.nil?
OAuthErrorObject.construct_from(@json_body)
end
end
# InvalidClientError is raised when the client doesn't belong to you, or

View File

@ -7,7 +7,7 @@ module Stripe
include Stripe::APIOperations::Request
include Stripe::APIOperations::Create
OBJECT_NAME = "list".freeze
OBJECT_NAME = "list"
# This accessor allows a `ListObject` to inherit various filters that were
# given to a predecessor. This allows for things like consistent limits,
@ -83,6 +83,7 @@ module Stripe
# was given, the default limit will be fetched again.
def next_page(params = {}, opts = {})
return self.class.empty_list(opts) unless has_more
last_id = data.last.id
params = filters.merge(starting_after: last_id).merge(params)

View File

@ -0,0 +1,131 @@
# frozen_string_literal: true
require "securerandom"
require "tempfile"
module Stripe
# Encodes parameters into a `multipart/form-data` payload as described by RFC
# 2388:
#
# https://tools.ietf.org/html/rfc2388
#
# This is most useful for transferring file-like objects.
#
# Parameters should be added with `#encode`. When ready, use `#body` to get
# the encoded result and `#content_type` to get the value that should be
# placed in the `Content-Type` header of a subsequent request (which includes
# a boundary value).
class MultipartEncoder
MULTIPART_FORM_DATA = "multipart/form-data"
# A shortcut for encoding a single set of parameters and finalizing a
# result.
#
# Returns an encoded body and the value that should be set in the content
# type header of a subsequent request.
def self.encode(params)
encoder = MultipartEncoder.new
encoder.encode(params)
encoder.close
[encoder.body, encoder.content_type]
end
# Gets the object's randomly generated boundary string.
attr_reader :boundary
# Initializes a new multipart encoder.
def initialize
# Kind of weird, but required by Rubocop because the unary plus operator
# is considered faster than `Stripe.new`.
@body = +""
# Chose the same number of random bytes that Go uses in its standard
# library implementation. Easily enough entropy to ensure that it won't
# be present in a file we're sending.
@boundary = SecureRandom.hex(30)
@closed = false
@first_field = true
end
# Gets the encoded body. `#close` must be called first.
def body
raise "object must be closed before getting body" unless @closed
@body
end
# Finalizes the object by writing the final boundary.
def close
raise "object already closed" if @closed
@body << "\r\n"
@body << "--#{@boundary}--"
@closed = true
nil
end
# Gets the value including boundary that should be put into a multipart
# request's `Content-Type`.
def content_type
"#{MULTIPART_FORM_DATA}; boundary=#{@boundary}"
end
# Encodes a set of parameters to the body.
#
# Note that parameters are expected to be a hash, but a "flat" hash such
# that complex substructures like hashes and arrays have already been
# appropriately Stripe-encoded. Pass a complex structure through
# `Util.flatten_params` first before handing it off to this method.
def encode(params)
raise "no more parameters can be written to closed object" if @closed
params.each do |name, val|
if val.is_a?(::File) || val.is_a?(::Tempfile)
write_field(name, val.read, filename: ::File.basename(val.path))
elsif val.respond_to?(:read)
write_field(name, val.read, filename: "blob")
else
write_field(name, val, filename: nil)
end
end
nil
end
#
# private
#
# Escapes double quotes so that the given value can be used in a
# double-quoted string and replaces any linebreak characters with spaces.
private def escape(str)
str.gsub('"', "%22").tr("\n", " ").tr("\r", " ")
end
private def write_field(name, data, filename:)
if !@first_field
@body << "\r\n"
else
@first_field = false
end
@body << "--#{@boundary}\r\n"
if filename
@body << %(Content-Disposition: form-data) +
%(; name="#{escape(name.to_s)}") +
%(; filename="#{escape(filename)}"\r\n)
@body << %(Content-Type: application/octet-stream\r\n)
else
@body << %(Content-Disposition: form-data) +
%(; name="#{escape(name.to_s)}"\r\n)
end
@body << "\r\n"
@body << data.to_s
end
end
end

View File

@ -41,7 +41,6 @@ module Stripe
Invoice::OBJECT_NAME => Invoice,
InvoiceItem::OBJECT_NAME => InvoiceItem,
InvoiceLineItem::OBJECT_NAME => InvoiceLineItem,
IssuerFraudRecord::OBJECT_NAME => IssuerFraudRecord,
Issuing::Authorization::OBJECT_NAME => Issuing::Authorization,
Issuing::Card::OBJECT_NAME => Issuing::Card,
Issuing::CardDetails::OBJECT_NAME => Issuing::CardDetails,

View File

@ -30,7 +30,6 @@ require "stripe/resources/file_link"
require "stripe/resources/invoice"
require "stripe/resources/invoice_item"
require "stripe/resources/invoice_line_item"
require "stripe/resources/issuer_fraud_record"
require "stripe/resources/issuing/authorization"
require "stripe/resources/issuing/card"
require "stripe/resources/issuing/card_details"

View File

@ -9,7 +9,7 @@ module Stripe
include Stripe::APIOperations::Save
extend Stripe::APIOperations::NestedResource
OBJECT_NAME = "account".freeze
OBJECT_NAME = "account"
custom_method :reject, http_verb: :post
@ -35,10 +35,6 @@ module Stripe
nested_resource_class_methods :login_link, operations: %i[create]
# This method is deprecated. Please use `#external_account=` instead.
save_nested_resource :bank_account
deprecate :bank_account=, "#external_account=", 2017, 8
def resource_url
if self["id"]
super

View File

@ -4,6 +4,6 @@ module Stripe
class AccountLink < APIResource
extend Stripe::APIOperations::Create
OBJECT_NAME = "account_link".freeze
OBJECT_NAME = "account_link"
end
end

View File

@ -5,7 +5,7 @@ module Stripe
include Stripe::APIOperations::Save
include Stripe::APIOperations::Delete
OBJECT_NAME = "alipay_account".freeze
OBJECT_NAME = "alipay_account"
def resource_url
if !respond_to?(:customer) || customer.nil?

View File

@ -7,7 +7,7 @@ module Stripe
include Stripe::APIOperations::Delete
extend Stripe::APIOperations::List
OBJECT_NAME = "apple_pay_domain".freeze
OBJECT_NAME = "apple_pay_domain"
def self.resource_url
"/v1/apple_pay/domains"

View File

@ -5,20 +5,9 @@ module Stripe
extend Stripe::APIOperations::List
extend Stripe::APIOperations::NestedResource
OBJECT_NAME = "application_fee".freeze
OBJECT_NAME = "application_fee"
nested_resource_class_methods :refund,
operations: %i[create retrieve update list]
# If you don't need access to an updated fee object after the refund, it's
# more performant to just call `fee.refunds.create` directly.
def refund(params = {}, opts = {})
refunds.create(params, opts)
# now that a refund has been created, we expect the state of this object
# to change as well (i.e. `refunded` will now be `true`) so refresh it
# from the server
refresh
end
end
end

View File

@ -5,7 +5,7 @@ module Stripe
include Stripe::APIOperations::Save
extend Stripe::APIOperations::List
OBJECT_NAME = "fee_refund".freeze
OBJECT_NAME = "fee_refund"
def resource_url
"#{ApplicationFee.resource_url}/#{CGI.escape(fee)}/refunds" \

View File

@ -2,6 +2,6 @@
module Stripe
class Balance < SingletonAPIResource
OBJECT_NAME = "balance".freeze
OBJECT_NAME = "balance"
end
end

View File

@ -4,6 +4,6 @@ module Stripe
class BalanceTransaction < APIResource
extend Stripe::APIOperations::List
OBJECT_NAME = "balance_transaction".freeze
OBJECT_NAME = "balance_transaction"
end
end

View File

@ -6,7 +6,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "bank_account".freeze
OBJECT_NAME = "bank_account"
def verify(params = {}, opts = {})
resp, opts = request(:post, resource_url + "/verify", params, opts)

View File

@ -6,7 +6,7 @@ module Stripe
class BitcoinReceiver < APIResource
extend Stripe::APIOperations::List
OBJECT_NAME = "bitcoin_receiver".freeze
OBJECT_NAME = "bitcoin_receiver"
def self.resource_url
"/v1/bitcoin/receivers"

View File

@ -6,7 +6,7 @@ module Stripe
# Sources API instead: https://stripe.com/docs/sources/bitcoin
extend Stripe::APIOperations::List
OBJECT_NAME = "bitcoin_transaction".freeze
OBJECT_NAME = "bitcoin_transaction"
def self.resource_url
"/v1/bitcoin/transactions"

View File

@ -5,7 +5,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "capability".freeze
OBJECT_NAME = "capability"
def resource_url
if !respond_to?(:account) || account.nil?

View File

@ -6,7 +6,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "card".freeze
OBJECT_NAME = "card"
def resource_url
if respond_to?(:recipient) && !recipient.nil? && !recipient.empty?

View File

@ -6,79 +6,13 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "charge".freeze
OBJECT_NAME = "charge"
custom_method :capture, http_verb: :post
def refund(params = {}, opts = {})
# Old versions of charge objects included a `refunds` field that was just
# a vanilla array instead of a Stripe list object.
#
# Where possible, we'd still like to use the new refund endpoint (thus
# `self.refunds.create`), but detect the old API version by looking for
# an `Array` and fall back to the old refund URL if necessary so as to
# maintain internal compatibility.
if refunds.is_a?(Array)
resp, opts = request(:post, refund_url, params, opts)
initialize_from(resp.data, opts)
else
refunds.create(params, opts)
# now that a refund has been created, we expect the state of this object
# to change as well (i.e. `refunded` will now be `true`) so refresh it
# from the server
refresh
end
end
def capture(params = {}, opts = {})
resp, opts = request(:post, capture_url, params, opts)
resp, opts = request(:post, resource_url + "/capture", params, opts)
initialize_from(resp.data, opts)
end
def update_dispute(params = {}, opts = {})
resp, opts = request(:post, dispute_url, params, opts)
initialize_from({ dispute: resp.data }, opts, true)
dispute
end
def close_dispute(params = {}, opts = {})
resp, opts = request(:post, close_dispute_url, params, opts)
initialize_from(resp.data, opts)
end
def mark_as_fraudulent
params = {
fraud_details: { user_report: "fraudulent" },
}
resp, opts = request(:post, resource_url, params)
initialize_from(resp.data, opts)
end
def mark_as_safe
params = {
fraud_details: { user_report: "safe" },
}
resp, opts = request(:post, resource_url, params)
initialize_from(resp.data, opts)
end
private def capture_url
resource_url + "/capture"
end
private def dispute_url
resource_url + "/dispute"
end
private def close_dispute_url
resource_url + "/dispute/close"
end
# Note that this is actually the *old* refund URL and its use is no longer
# preferred.
private def refund_url
resource_url + "/refund"
end
end
end

View File

@ -5,7 +5,7 @@ module Stripe
class Session < APIResource
extend Stripe::APIOperations::Create
OBJECT_NAME = "checkout.session".freeze
OBJECT_NAME = "checkout.session"
end
end
end

View File

@ -4,6 +4,6 @@ module Stripe
class CountrySpec < APIResource
extend Stripe::APIOperations::List
OBJECT_NAME = "country_spec".freeze
OBJECT_NAME = "country_spec"
end
end

View File

@ -7,6 +7,6 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "coupon".freeze
OBJECT_NAME = "coupon"
end
end

View File

@ -6,7 +6,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "credit_note".freeze
OBJECT_NAME = "credit_note"
custom_method :void_credit_note, http_verb: :post, http_path: "void"

View File

@ -8,7 +8,7 @@ module Stripe
include Stripe::APIOperations::Save
extend Stripe::APIOperations::NestedResource
OBJECT_NAME = "customer".freeze
OBJECT_NAME = "customer"
nested_resource_class_methods :balance_transaction,
operations: %i[create retrieve update list]
@ -27,69 +27,9 @@ module Stripe
alias detach_source delete_source
end
def add_invoice_item(params, opts = {})
opts = @opts.merge(Util.normalize_opts(opts))
InvoiceItem.create(params.merge(customer: id), opts)
end
def invoices(params = {}, opts = {})
opts = @opts.merge(Util.normalize_opts(opts))
Invoice.all(params.merge(customer: id), opts)
end
def invoice_items(params = {}, opts = {})
opts = @opts.merge(Util.normalize_opts(opts))
InvoiceItem.all(params.merge(customer: id), opts)
end
def upcoming_invoice(params = {}, opts = {})
opts = @opts.merge(Util.normalize_opts(opts))
Invoice.upcoming(params.merge(customer: id), opts)
end
def charges(params = {}, opts = {})
opts = @opts.merge(Util.normalize_opts(opts))
Charge.all(params.merge(customer: id), opts)
end
def create_upcoming_invoice(params = {}, opts = {})
opts = @opts.merge(Util.normalize_opts(opts))
Invoice.create(params.merge(customer: id), opts)
end
def cancel_subscription(params = {}, opts = {})
resp, opts = request(:delete, subscription_url, params, opts)
initialize_from({ subscription: resp.data }, opts, true)
subscription
end
def update_subscription(params = {}, opts = {})
resp, opts = request(:post, subscription_url, params, opts)
initialize_from({ subscription: resp.data }, opts, true)
subscription
end
def create_subscription(params = {}, opts = {})
resp, opts = request(:post, subscriptions_url, params, opts)
initialize_from({ subscription: resp.data }, opts, true)
subscription
end
def delete_discount
_, opts = request(:delete, discount_url)
initialize_from({ discount: nil }, opts, true)
end
private def discount_url
resource_url + "/discount"
end
private def subscription_url
resource_url + "/subscription"
end
private def subscriptions_url
resource_url + "/subscriptions"
resp, opts = request(:delete, resource_url + "/discount")
initialize_from(resp.data, opts, true)
end
end
end

View File

@ -5,7 +5,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "customer_balance_transaction".freeze
OBJECT_NAME = "customer_balance_transaction"
def resource_url
if !respond_to?(:customer) || customer.nil?

View File

@ -2,6 +2,6 @@
module Stripe
class Discount < StripeObject
OBJECT_NAME = "discount".freeze
OBJECT_NAME = "discount"
end
end

View File

@ -5,7 +5,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "dispute".freeze
OBJECT_NAME = "dispute"
custom_method :close, http_verb: :post
@ -17,11 +17,5 @@ module Stripe
opts: opts
)
end
def close_url
resource_url + "/close"
end
extend Gem::Deprecate
deprecate :close_url, :none, 2019, 11
end
end

View File

@ -5,7 +5,7 @@ module Stripe
extend Stripe::APIOperations::Create
include Stripe::APIOperations::Delete
OBJECT_NAME = "ephemeral_key".freeze
OBJECT_NAME = "ephemeral_key"
def self.create(params = {}, opts = {})
opts = Util.normalize_opts(opts)

View File

@ -4,6 +4,6 @@ module Stripe
class Event < APIResource
extend Stripe::APIOperations::List
OBJECT_NAME = "event".freeze
OBJECT_NAME = "event"
end
end

View File

@ -4,6 +4,6 @@ module Stripe
class ExchangeRate < APIResource
extend Stripe::APIOperations::List
OBJECT_NAME = "exchange_rate".freeze
OBJECT_NAME = "exchange_rate"
end
end

View File

@ -5,40 +5,30 @@ module Stripe
extend Stripe::APIOperations::Create
extend Stripe::APIOperations::List
OBJECT_NAME = "file".freeze
OBJECT_NAME = "file"
# This resource can have two different object names. In latter API
# versions, only `file` is used, but since stripe-ruby may be used with
# any API version, we need to support deserializing the older
# `file_upload` object into the same class.
OBJECT_NAME_ALT = "file_upload".freeze
OBJECT_NAME_ALT = "file_upload"
def self.resource_url
"/v1/files"
end
def self.create(params = {}, opts = {})
# rest-client would accept a vanilla `File` for upload, but Faraday does
# not. Support the old API by wrapping a `File`-like object with an
# `UploadIO` object if we're given one.
if params[:file] && !params[:file].is_a?(String)
unless params[:file].respond_to?(:read)
raise ArgumentError, "file must respond to `#read`"
end
params[:file] = Faraday::UploadIO.new(params[:file], nil)
end
opts = {
api_base: Stripe.uploads_base,
content_type: "multipart/form-data",
content_type: MultipartEncoder::MULTIPART_FORM_DATA,
}.merge(Util.normalize_opts(opts))
super
end
end
end
module Stripe
# For backwards compatibility, the `File` class is aliased to `FileUpload`.
FileUpload = File
end

View File

@ -6,6 +6,6 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "file_link".freeze
OBJECT_NAME = "file_link"
end
end

View File

@ -7,7 +7,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "invoice".freeze
OBJECT_NAME = "invoice"
custom_method :finalize_invoice, http_verb: :post, http_path: "finalize"
custom_method :mark_uncollectible, http_verb: :post
@ -64,5 +64,10 @@ module Stripe
resp, opts = request(:get, resource_url + "/upcoming", params, opts)
Util.convert_to_stripe_object(resp.data, opts)
end
def self.list_upcoming_line_items(params, opts = {})
resp, opts = request(:get, resource_url + "/upcoming/lines", params, opts)
Util.convert_to_stripe_object(resp.data, opts)
end
end
end

View File

@ -7,6 +7,6 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "invoiceitem".freeze
OBJECT_NAME = "invoiceitem"
end
end

View File

@ -2,6 +2,6 @@
module Stripe
class InvoiceLineItem < StripeObject
OBJECT_NAME = "line_item".freeze
OBJECT_NAME = "line_item"
end
end

View File

@ -1,9 +0,0 @@
# frozen_string_literal: true
module Stripe
class IssuerFraudRecord < APIResource
extend Stripe::APIOperations::List
OBJECT_NAME = "issuer_fraud_record".freeze
end
end

View File

@ -6,7 +6,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "issuing.authorization".freeze
OBJECT_NAME = "issuing.authorization"
custom_method :approve, http_verb: :post
custom_method :decline, http_verb: :post

View File

@ -7,7 +7,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "issuing.card".freeze
OBJECT_NAME = "issuing.card"
custom_method :details, http_verb: :get

View File

@ -3,7 +3,7 @@
module Stripe
module Issuing
class CardDetails < Stripe::StripeObject
OBJECT_NAME = "issuing.card_details".freeze
OBJECT_NAME = "issuing.card_details"
end
end
end

View File

@ -7,7 +7,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "issuing.cardholder".freeze
OBJECT_NAME = "issuing.cardholder"
end
end
end

View File

@ -7,7 +7,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "issuing.dispute".freeze
OBJECT_NAME = "issuing.dispute"
end
end
end

View File

@ -6,7 +6,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "issuing.transaction".freeze
OBJECT_NAME = "issuing.transaction"
end
end
end

View File

@ -2,7 +2,7 @@
module Stripe
class LoginLink < APIResource
OBJECT_NAME = "login_link".freeze
OBJECT_NAME = "login_link"
def self.retrieve(_id, _opts = nil)
raise NotImplementedError,

View File

@ -6,7 +6,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "order".freeze
OBJECT_NAME = "order"
custom_method :pay, http_verb: :post
custom_method :return_order, http_verb: :post, http_path: "returns"
@ -28,13 +28,5 @@ module Stripe
opts: opts
)
end
private def pay_url
resource_url + "/pay"
end
private def returns_url
resource_url + "/returns"
end
end
end

View File

@ -4,6 +4,6 @@ module Stripe
class OrderReturn < APIResource
extend Stripe::APIOperations::List
OBJECT_NAME = "order_return".freeze
OBJECT_NAME = "order_return"
end
end

View File

@ -6,7 +6,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "payment_intent".freeze
OBJECT_NAME = "payment_intent"
custom_method :cancel, http_verb: :post
custom_method :capture, http_verb: :post

View File

@ -6,7 +6,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "payment_method".freeze
OBJECT_NAME = "payment_method"
custom_method :attach, http_verb: :post
custom_method :detach, http_verb: :post

View File

@ -6,7 +6,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "payout".freeze
OBJECT_NAME = "payout"
custom_method :cancel, http_verb: :post
@ -18,11 +18,5 @@ module Stripe
opts: opts
)
end
def cancel_url
resource_url + "/cancel"
end
extend Gem::Deprecate
deprecate :cancel_url, :none, 2019, 11
end
end

View File

@ -5,7 +5,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "person".freeze
OBJECT_NAME = "person"
def resource_url
if !respond_to?(:account) || account.nil?

View File

@ -7,6 +7,6 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "plan".freeze
OBJECT_NAME = "plan"
end
end

View File

@ -7,6 +7,6 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "product".freeze
OBJECT_NAME = "product"
end
end

View File

@ -5,7 +5,7 @@ module Stripe
class EarlyFraudWarning < APIResource
extend Stripe::APIOperations::List
OBJECT_NAME = "radar.early_fraud_warning".freeze
OBJECT_NAME = "radar.early_fraud_warning"
end
end
end

View File

@ -8,7 +8,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "radar.value_list".freeze
OBJECT_NAME = "radar.value_list"
end
end
end

View File

@ -7,7 +7,7 @@ module Stripe
include Stripe::APIOperations::Delete
extend Stripe::APIOperations::List
OBJECT_NAME = "radar.value_list_item".freeze
OBJECT_NAME = "radar.value_list_item"
end
end
end

View File

@ -8,10 +8,6 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "recipient".freeze
def transfers
Transfer.all({ recipient: id }, @api_key)
end
OBJECT_NAME = "recipient"
end
end

View File

@ -2,6 +2,6 @@
module Stripe
class RecipientTransfer < StripeObject
OBJECT_NAME = "recipient_transfer".freeze
OBJECT_NAME = "recipient_transfer"
end
end

View File

@ -6,6 +6,6 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "refund".freeze
OBJECT_NAME = "refund"
end
end

View File

@ -6,7 +6,7 @@ module Stripe
extend Stripe::APIOperations::Create
extend Stripe::APIOperations::List
OBJECT_NAME = "reporting.report_run".freeze
OBJECT_NAME = "reporting.report_run"
end
end
end

View File

@ -6,7 +6,7 @@ module Stripe
extend Stripe::APIOperations::Create
extend Stripe::APIOperations::List
OBJECT_NAME = "reporting.report_type".freeze
OBJECT_NAME = "reporting.report_type"
end
end
end

View File

@ -5,7 +5,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "transfer_reversal".freeze
OBJECT_NAME = "transfer_reversal"
def resource_url
"#{Transfer.resource_url}/#{CGI.escape(transfer)}/reversals" \

View File

@ -4,7 +4,7 @@ module Stripe
class Review < APIResource
extend Stripe::APIOperations::List
OBJECT_NAME = "review".freeze
OBJECT_NAME = "review"
custom_method :approve, http_verb: :post

View File

@ -6,7 +6,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "setup_intent".freeze
OBJECT_NAME = "setup_intent"
custom_method :cancel, http_verb: :post
custom_method :confirm, http_verb: :post

View File

@ -5,7 +5,7 @@ module Stripe
class ScheduledQueryRun < APIResource
extend Stripe::APIOperations::List
OBJECT_NAME = "scheduled_query_run".freeze
OBJECT_NAME = "scheduled_query_run"
def self.resource_url
"/v1/sigma/scheduled_query_runs"

View File

@ -7,6 +7,6 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "sku".freeze
OBJECT_NAME = "sku"
end
end

View File

@ -5,7 +5,7 @@ module Stripe
extend Stripe::APIOperations::Create
include Stripe::APIOperations::Save
OBJECT_NAME = "source".freeze
OBJECT_NAME = "source"
custom_method :verify, http_verb: :post
@ -31,12 +31,6 @@ module Stripe
initialize_from(resp.data, opts)
end
def delete(params = {}, opts = {})
detach(params, opts)
end
extend Gem::Deprecate
deprecate :delete, "#detach", 2017, 10
def source_transactions(params = {}, opts = {})
resp, opts = request(:get, resource_url + "/source_transactions", params,
opts)

View File

@ -2,6 +2,6 @@
module Stripe
class SourceTransaction < StripeObject
OBJECT_NAME = "source_transaction".freeze
OBJECT_NAME = "source_transaction"
end
end

View File

@ -7,19 +7,15 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "subscription".freeze
OBJECT_NAME = "subscription"
custom_method :delete_discount, http_verb: :delete, http_path: "discount"
save_nested_resource :source
def delete_discount
_, opts = request(:delete, discount_url)
initialize_from({ discount: nil }, opts, true)
end
private def discount_url
resource_url + "/discount"
resp, opts = request(:delete, resource_url + "/discount")
initialize_from(resp.data, opts, true)
end
end
end

View File

@ -8,7 +8,7 @@ module Stripe
include Stripe::APIOperations::Save
extend Stripe::APIOperations::NestedResource
OBJECT_NAME = "subscription_item".freeze
OBJECT_NAME = "subscription_item"
nested_resource_class_methods :usage_record, operations: %i[create]

View File

@ -6,7 +6,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "subscription_schedule".freeze
OBJECT_NAME = "subscription_schedule"
custom_method :cancel, http_verb: :post
custom_method :release, http_verb: :post

View File

@ -5,7 +5,7 @@ module Stripe
include Stripe::APIOperations::Delete
extend Stripe::APIOperations::List
OBJECT_NAME = "tax_id".freeze
OBJECT_NAME = "tax_id"
def resource_url
if !respond_to?(:customer) || customer.nil?

View File

@ -6,6 +6,6 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "tax_rate".freeze
OBJECT_NAME = "tax_rate"
end
end

View File

@ -5,7 +5,7 @@ module Stripe
class ConnectionToken < APIResource
extend Stripe::APIOperations::Create
OBJECT_NAME = "terminal.connection_token".freeze
OBJECT_NAME = "terminal.connection_token"
end
end
end

View File

@ -8,7 +8,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "terminal.location".freeze
OBJECT_NAME = "terminal.location"
end
end
end

View File

@ -8,7 +8,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "terminal.reader".freeze
OBJECT_NAME = "terminal.reader"
end
end
end

View File

@ -4,7 +4,7 @@ module Stripe
class ThreeDSecure < APIResource
extend Stripe::APIOperations::Create
OBJECT_NAME = "three_d_secure".freeze
OBJECT_NAME = "three_d_secure"
def self.resource_url
"/v1/3d_secure"

View File

@ -4,6 +4,6 @@ module Stripe
class Token < APIResource
extend Stripe::APIOperations::Create
OBJECT_NAME = "token".freeze
OBJECT_NAME = "token"
end
end

View File

@ -6,7 +6,7 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "topup".freeze
OBJECT_NAME = "topup"
custom_method :cancel, http_verb: :post

View File

@ -7,7 +7,7 @@ module Stripe
include Stripe::APIOperations::Save
extend Stripe::APIOperations::NestedResource
OBJECT_NAME = "transfer".freeze
OBJECT_NAME = "transfer"
custom_method :cancel, http_verb: :post
@ -22,10 +22,5 @@ module Stripe
opts: opts
)
end
def cancel_url
resource_url + "/cancel"
end
deprecate :cancel_url, :none, 2019, 11
end
end

View File

@ -2,22 +2,6 @@
module Stripe
class UsageRecord < APIResource
OBJECT_NAME = "usage_record".freeze
def self.create(params = {}, opts = {})
unless params.key?(:subscription_item)
raise ArgumentError, "Params must have a subscription_item key"
end
req_params = params.clone.delete_if do |key, _value|
key == :subscription_item
end
resp, opts = request(
:post,
"/v1/subscription_items/#{params[:subscription_item]}/usage_records",
req_params,
opts
)
Util.convert_to_stripe_object(resp.data, opts)
end
OBJECT_NAME = "usage_record"
end
end

View File

@ -2,6 +2,6 @@
module Stripe
class UsageRecordSummary < StripeObject
OBJECT_NAME = "usage_record_summary".freeze
OBJECT_NAME = "usage_record_summary"
end
end

View File

@ -7,6 +7,6 @@ module Stripe
extend Stripe::APIOperations::List
include Stripe::APIOperations::Save
OBJECT_NAME = "webhook_endpoint".freeze
OBJECT_NAME = "webhook_endpoint"
end
end

View File

@ -5,85 +5,98 @@ module Stripe
# recover both a resource a call returns as well as a response object that
# contains information on the HTTP call.
class StripeClient
attr_accessor :conn
# A set of all known connection managers across all threads and a mutex to
# synchronize global access to them.
@all_connection_managers = []
@all_connection_managers_mutex = Mutex.new
# Initializes a new StripeClient. Expects a Faraday connection object, and
# uses a default connection unless one is passed.
def initialize(conn = nil)
self.conn = conn || self.class.default_conn
attr_accessor :connection_manager
# Initializes a new `StripeClient`. Expects a `ConnectionManager` object,
# and uses a default connection manager unless one is passed.
def initialize(connection_manager = nil)
self.connection_manager = connection_manager ||
self.class.default_connection_manager
@system_profiler = SystemProfiler.new
@last_request_metrics = nil
end
# Gets a currently active `StripeClient`. Set for the current thread when
# `StripeClient#request` is being run so that API operations being executed
# inside of that block can find the currently active client. It's reset to
# the original value (hopefully `nil`) after the block ends.
#
# For internal use only. Does not provide a stable API and may be broken
# with future non-major changes.
def self.active_client
Thread.current[:stripe_client] || default_client
current_thread_context.active_client || default_client
end
# Finishes any active connections by closing their TCP connection and
# clears them from internal tracking in all connection managers across all
# threads.
#
# For internal use only. Does not provide a stable API and may be broken
# with future non-major changes.
def self.clear_all_connection_managers
# Just a quick path for when configuration is being set for the first
# time before any connections have been opened. There is technically some
# potential for thread raciness here, but not in a practical sense.
return if @all_connection_managers.empty?
@all_connection_managers_mutex.synchronize do
@all_connection_managers.each(&:clear)
end
end
# A default client for the current thread.
def self.default_client
Thread.current[:stripe_client_default_client] ||=
StripeClient.new(default_conn)
current_thread_context.default_client ||=
StripeClient.new(default_connection_manager)
end
# A default Faraday connection to be used when one isn't configured. This
# object should never be mutated, and instead instantiating your own
# connection and wrapping it in a StripeClient object should be preferred.
def self.default_conn
# We're going to keep connections around so that we can take advantage
# of connection re-use, so make sure that we have a separate connection
# object per thread.
Thread.current[:stripe_client_default_conn] ||= begin
conn = Faraday.new do |builder|
builder.use Faraday::Request::Multipart
builder.use Faraday::Request::UrlEncoded
builder.use Faraday::Response::RaiseError
# A default connection manager for the current thread.
def self.default_connection_manager
current_thread_context.default_connection_manager ||= begin
connection_manager = ConnectionManager.new
# Net::HTTP::Persistent doesn't seem to do well on Windows or JRuby,
# so fall back to default there.
if Gem.win_platform? || RUBY_PLATFORM == "java"
builder.adapter :net_http
else
builder.adapter :net_http_persistent
end
@all_connection_managers_mutex.synchronize do
@all_connection_managers << connection_manager
end
conn.proxy = Stripe.proxy if Stripe.proxy
if Stripe.verify_ssl_certs
conn.ssl.verify = true
conn.ssl.cert_store = Stripe.ca_store
else
conn.ssl.verify = false
unless @verify_ssl_warned
@verify_ssl_warned = true
warn("WARNING: Running without SSL cert verification. " \
"You should never do this in production. " \
"Execute `Stripe.verify_ssl_certs = true` to enable " \
"verification.")
end
end
conn
connection_manager
end
end
# Checks if an error is a problem that we should retry on. This includes
# both socket errors that may represent an intermittent problem and some
# special HTTP statuses.
def self.should_retry?(error, num_retries)
def self.should_retry?(error, method:, num_retries:)
return false if num_retries >= Stripe.max_network_retries
# Retry on timeout-related problems (either on open or read).
return true if error.is_a?(Faraday::TimeoutError)
return true if error.is_a?(Net::OpenTimeout)
return true if error.is_a?(Net::ReadTimeout)
# Destination refused the connection, the connection was reset, or a
# variety of other connection failures. This could occur from a single
# saturated server, so retry in case it's intermittent.
return true if error.is_a?(Faraday::ConnectionFailed)
return true if error.is_a?(Errno::ECONNREFUSED)
return true if error.is_a?(SocketError)
if error.is_a?(Faraday::ClientError) && error.response
# 409 conflict
return true if error.response[:status] == 409
if error.is_a?(Stripe::StripeError)
# 409 Conflict
return true if error.http_status == 409
# 500 Internal Server Error
#
# We only bother retrying these for non-POST requests. POSTs end up
# being cached by the idempotency layer so there's no purpose in
# retrying them.
return true if error.http_status == 500 && method != :post
# 503 Service Unavailable
return true if error.http_status == 503
end
false
@ -114,127 +127,187 @@ module Stripe
# charge, resp = client.request { Charge.create }
#
def request
@last_response = nil
old_stripe_client = Thread.current[:stripe_client]
Thread.current[:stripe_client] = self
old_stripe_client = self.class.current_thread_context.active_client
self.class.current_thread_context.active_client = self
if self.class.current_thread_context.last_responses&.key?(object_id)
raise "calls to StripeClient#request cannot be nested within a thread"
end
self.class.current_thread_context.last_responses ||= {}
self.class.current_thread_context.last_responses[object_id] = nil
begin
res = yield
[res, @last_response]
[res, self.class.current_thread_context.last_responses[object_id]]
ensure
Thread.current[:stripe_client] = old_stripe_client
self.class.current_thread_context.active_client = old_stripe_client
self.class.current_thread_context.last_responses.delete(object_id)
end
end
def execute_request(method, path,
api_base: nil, api_key: nil, headers: {}, params: {})
raise ArgumentError, "method should be a symbol" \
unless method.is_a?(Symbol)
raise ArgumentError, "path should be a string" \
unless path.is_a?(String)
api_base ||= Stripe.api_base
api_key ||= Stripe.api_key
params = Util.objects_to_ids(params)
check_api_key!(api_key)
body = nil
body_params = nil
query_params = nil
case method.to_s.downcase.to_sym
case method
when :get, :head, :delete
query_params = params
else
body = params
body_params = params
end
# This works around an edge case where we end up with both query
# parameters in `query_params` and query parameters that are appended
# onto the end of the given path. In this case, Faraday will silently
# discard the URL's parameters which may break a request.
#
# Here we decode any parameters that were added onto the end of a path
# and add them to `query_params` so that all parameters end up in one
# place and all of them are correctly included in the final request.
u = URI.parse(path)
unless u.query.nil?
query_params ||= {}
query_params = Hash[URI.decode_www_form(u.query)].merge(query_params)
# Reset the path minus any query parameters that were specified.
path = u.path
end
query_params, path = merge_query_params(query_params, path)
headers = request_headers(api_key, method)
.update(Util.normalize_headers(headers))
params_encoder = FaradayStripeEncoder.new
url = api_url(path, api_base)
# Merge given query parameters with any already encoded in the path.
query = query_params ? Util.encode_parameters(query_params) : nil
# Encoding body parameters is a little more complex because we may have
# to send a multipart-encoded body. `body_log` is produced separately as
# a log-friendly variant of the encoded form. File objects are displayed
# as such instead of as their file contents.
body, body_log =
body_params ? encode_body(body_params, headers) : [nil, nil]
# stores information on the request we're about to make so that we don't
# have to pass as many parameters around for logging.
context = RequestLogContext.new
context.account = headers["Stripe-Account"]
context.api_key = api_key
context.api_version = headers["Stripe-Version"]
context.body = body ? params_encoder.encode(body) : nil
context.body = body_log
context.idempotency_key = headers["Idempotency-Key"]
context.method = method
context.path = path
context.query_params = if query_params
params_encoder.encode(query_params)
end
context.query = query
# note that both request body and query params will be passed through
# `FaradayStripeEncoder`
http_resp = execute_request_with_rescues(api_base, context) do
conn.run_request(method, url, body, headers) do |req|
req.options.open_timeout = Stripe.open_timeout
req.options.params_encoder = params_encoder
req.options.timeout = Stripe.read_timeout
req.params = query_params unless query_params.nil?
end
http_resp = execute_request_with_rescues(method, api_base, context) do
connection_manager.execute_request(method, url,
body: body,
headers: headers,
query: query)
end
begin
resp = StripeResponse.from_faraday_response(http_resp)
resp = StripeResponse.from_net_http(http_resp)
rescue JSON::ParserError
raise general_api_error(http_resp.status, http_resp.body)
raise general_api_error(http_resp.code.to_i, http_resp.body)
end
# If being called from `StripeClient#request`, put the last response in
# thread-local memory so that it can be returned to the user. Don't store
# anything otherwise so that we don't leak memory.
if self.class.current_thread_context.last_responses&.key?(object_id)
self.class.current_thread_context.last_responses[object_id] = resp
end
# Allows StripeClient#request to return a response object to a caller.
@last_response = resp
[resp, api_key]
end
# Used to workaround buggy behavior in Faraday: the library will try to
# reshape anything that we pass to `req.params` with one of its default
# encoders. I don't think this process is supposed to be lossy, but it is
# -- in particular when we send our integer-indexed maps (i.e. arrays),
# Faraday ends up stripping out the integer indexes.
#
# We work around the problem by implementing our own simplified encoder and
# telling Faraday to use that.
# private
#
# The class also performs simple caching so that we don't have to encode
# parameters twice for every request (once to build the request and once
# for logging).
#
# When initialized with `multipart: true`, the encoder just inspects the
# hash instead to get a decent representation for logging. In the case of a
# multipart request, Faraday won't use the result of this encoder.
class FaradayStripeEncoder
def initialize
@cache = {}
end
# This is quite subtle, but for a `multipart/form-data` request Faraday
# will throw away the result of this encoder and build its body.
def encode(hash)
@cache.fetch(hash) do |k|
@cache[k] = Util.encode_parameters(hash)
end
end
ERROR_MESSAGE_CONNECTION =
"Unexpected error communicating when trying to connect to " \
"Stripe (%s). You may be seeing this message because your DNS is not " \
"working or you don't have an internet connection. To check, try " \
"running `host stripe.com` from the command line."
ERROR_MESSAGE_SSL =
"Could not establish a secure connection to Stripe (%s), you " \
"may need to upgrade your OpenSSL version. To check, try running " \
"`openssl s_client -connect api.stripe.com:443` from the command " \
"line."
# We should never need to do this so it's not implemented.
def decode(_str)
raise NotImplementedError,
"#{self.class.name} does not implement #decode"
end
# Common error suffix sared by both connect and read timeout messages.
ERROR_MESSAGE_TIMEOUT_SUFFIX =
"Please check your internet connection and try again. " \
"If this problem persists, you should check Stripe's service " \
"status at https://status.stripe.com, or let us know at " \
"support@stripe.com."
ERROR_MESSAGE_TIMEOUT_CONNECT = (
"Timed out connecting to Stripe (%s). " +
ERROR_MESSAGE_TIMEOUT_SUFFIX
).freeze
ERROR_MESSAGE_TIMEOUT_READ = (
"Timed out communicating with Stripe (%s). " +
ERROR_MESSAGE_TIMEOUT_SUFFIX
).freeze
# Maps types of exceptions that we're likely to see during a network
# request to more user-friendly messages that we put in front of people.
# The original error message is also appended onto the final exception for
# full transparency.
NETWORK_ERROR_MESSAGES_MAP = {
Errno::ECONNREFUSED => ERROR_MESSAGE_CONNECTION,
SocketError => ERROR_MESSAGE_CONNECTION,
Net::OpenTimeout => ERROR_MESSAGE_TIMEOUT_CONNECT,
Net::ReadTimeout => ERROR_MESSAGE_TIMEOUT_READ,
OpenSSL::SSL::SSLError => ERROR_MESSAGE_SSL,
}.freeze
private_constant :NETWORK_ERROR_MESSAGES_MAP
# A record representing any data that `StripeClient` puts into
# `Thread.current`. Making it a class likes this gives us a little extra
# type safety and lets us document what each field does.
#
# For internal use only. Does not provide a stable API and may be broken
# with future non-major changes.
class ThreadContext
# A `StripeClient` that's been flagged as currently active within a
# thread by `StripeClient#request`. A client stays active until the
# completion of the request block.
attr_accessor :active_client
# A default `StripeClient` object for the thread. Used in all cases where
# the user hasn't specified their own.
attr_accessor :default_client
# A default `ConnectionManager` for the thread. Normally shared between
# all `StripeClient` objects on a particular thread, and created so as to
# minimize the number of open connections that an application needs.
attr_accessor :default_connection_manager
# A temporary map of object IDs to responses from last executed API
# calls. Used to return a responses from calls to `StripeClient#request`.
#
# Stored in the thread data to make the use of a single `StripeClient`
# object safe across multiple threads. Stored as a map so that multiple
# `StripeClient` objects can run concurrently on the same thread.
#
# Responses are only left in as long as they're needed, which means
# they're removed as soon as a call leaves `StripeClient#request`, and
# because that's wrapped in an `ensure` block, they should never leave
# garbage in `Thread.current`.
attr_accessor :last_responses
end
# Access data stored for `StripeClient` within the thread's current
# context. Returns `ThreadContext`.
#
# For internal use only. Does not provide a stable API and may be broken
# with future non-major changes.
def self.current_thread_context
Thread.current[:stripe_client__internal_use_only] ||= ThreadContext.new
end
private def api_url(url = "", api_base = nil)
@ -258,14 +331,48 @@ module Stripe
"email support@stripe.com if you have any questions.)"
end
private def execute_request_with_rescues(api_base, context)
# Encodes a set of body parameters using multipart if `Content-Type` is set
# for that, or standard form-encoding otherwise. Returns the encoded body
# and a version of the encoded body that's safe to be logged.
private def encode_body(body_params, headers)
body = nil
flattened_params = Util.flatten_params(body_params)
if headers["Content-Type"] == MultipartEncoder::MULTIPART_FORM_DATA
body, content_type = MultipartEncoder.encode(flattened_params)
# Set a new content type that also includes the multipart boundary.
# See `MultipartEncoder` for details.
headers["Content-Type"] = content_type
# `#to_s` any complex objects like files and the like to build output
# that's more condusive to logging.
flattened_params =
flattened_params.map { |k, v| [k, v.is_a?(String) ? v : v.to_s] }.to_h
else
body = Util.encode_parameters(body_params)
end
# We don't use `Util.encode_parameters` partly as an optimization (to not
# redo work we've already done), and partly because the encoded forms of
# certain characters introduce a lot of visual noise and it's nice to
# have a clearer format for logs.
body_log = flattened_params.map { |k, v| "#{k}=#{v}" }.join("&")
[body, body_log]
end
private def execute_request_with_rescues(method, api_base, context)
num_retries = 0
begin
request_start = Time.now
log_request(context, num_retries)
resp = yield
context = context.dup_from_response(resp)
log_response(context, request_start, resp.status, resp.body)
context = context.dup_from_response_headers(resp)
handle_error_response(resp, context) if resp.code.to_i >= 400
log_response(context, request_start, resp.code.to_i, resp.body)
if Stripe.enable_telemetry? && context.request_id
request_duration_ms = ((Time.now - request_start) * 1000).to_int
@ -281,27 +388,25 @@ module Stripe
# taint the original on a retry.
error_context = context
if e.respond_to?(:response) && e.response
error_context = context.dup_from_response(e.response)
if e.is_a?(Stripe::StripeError)
error_context = context.dup_from_response_headers(e.http_headers)
log_response(error_context, request_start,
e.response[:status], e.response[:body])
e.http_status, e.http_body)
else
log_response_error(error_context, request_start, e)
end
if self.class.should_retry?(e, num_retries)
if self.class.should_retry?(e, method: method, num_retries: num_retries)
num_retries += 1
sleep self.class.sleep_time(num_retries)
retry
end
case e
when Faraday::ClientError
if e.response
handle_error_response(e.response, error_context)
else
handle_network_error(e, error_context, num_retries, api_base)
end
when Stripe::StripeError
raise
when *NETWORK_ERROR_MESSAGES_MAP.keys
handle_network_error(e, error_context, num_retries, api_base)
# Only handle errors when we know we can do so, and re-raise otherwise.
# This should be pretty infrequent.
@ -332,12 +437,12 @@ module Stripe
private def handle_error_response(http_resp, context)
begin
resp = StripeResponse.from_faraday_hash(http_resp)
resp = StripeResponse.from_net_http(http_resp)
error_data = resp.data[:error]
raise StripeError, "Indeterminate error" unless error_data
rescue JSON::ParserError, StripeError
raise general_api_error(http_resp[:status], http_resp[:body])
raise general_api_error(http_resp.code.to_i, http_resp.body)
end
error = if error_data.is_a?(String)
@ -350,6 +455,28 @@ module Stripe
raise(error)
end
# Works around an edge case where we end up with both query parameters from
# parameteers and query parameters that were appended onto the end of the
# given path.
#
# Decode any parameters that were added onto the end of a path and add them
# to a unified query parameter hash so that all parameters end up in one
# place and all of them are correctly included in the final request.
private def merge_query_params(query_params, path)
u = URI.parse(path)
# Return original results if there was nothing to be found.
return query_params, path if u.query.nil?
query_params ||= {}
query_params = Hash[URI.decode_www_form(u.query)].merge(query_params)
# Reset the path minus any query parameters that were specified.
path = u.path
[query_params, path]
end
private def specific_api_error(resp, error_data, context)
Util.log_error("Stripe API error",
status: resp.http_status,
@ -384,11 +511,8 @@ module Stripe
when 401
AuthenticationError.new(error_data[:message], opts)
when 402
# TODO: modify CardError constructor to make code a keyword argument
# so we don't have to delete it from opts
opts.delete(:code)
CardError.new(
error_data[:message], error_data[:param], error_data[:code],
error_data[:message], error_data[:param],
opts
)
when 403
@ -444,33 +568,18 @@ module Stripe
idempotency_key: context.idempotency_key,
request_id: context.request_id)
case error
when Faraday::ConnectionFailed
message = "Unexpected error communicating when trying to connect to " \
"Stripe. You may be seeing this message because your DNS is not " \
"working. To check, try running `host stripe.com` from the " \
"command line."
when Faraday::SSLError
message = "Could not establish a secure connection to Stripe, you " \
"may need to upgrade your OpenSSL version. To check, try running " \
"`openssl s_client -connect api.stripe.com:443` from the command " \
"line."
when Faraday::TimeoutError
api_base ||= Stripe.api_base
message = "Could not connect to Stripe (#{api_base}). " \
"Please check your internet connection and try again. " \
"If this problem persists, you should check Stripe's service " \
"status at https://status.stripe.com, or let us know at " \
"support@stripe.com."
else
message = "Unexpected error communicating with Stripe. " \
"If this problem persists, let us know at support@stripe.com."
errors, message = NETWORK_ERROR_MESSAGES_MAP.detect do |(e, _)|
error.is_a?(e)
end
if errors.nil?
message = "Unexpected error #{error.class.name} communicating " \
"with Stripe. Please let us know at support@stripe.com."
end
api_base ||= Stripe.api_base
message = message % api_base
message += " Request was retried #{num_retries} times." if num_retries > 0
raise APIConnectionError,
@ -530,7 +639,7 @@ module Stripe
Util.log_debug("Request details",
body: context.body,
idempotency_key: context.idempotency_key,
query_params: context.query_params)
query: context.query)
end
private def log_response(context, request_start, status, body)
@ -577,7 +686,7 @@ module Stripe
attr_accessor :idempotency_key
attr_accessor :method
attr_accessor :path
attr_accessor :query_params
attr_accessor :query
attr_accessor :request_id
# The idea with this method is that we might want to update some of
@ -586,18 +695,7 @@ module Stripe
# with for a request. For example, we should trust whatever came back in
# a `Stripe-Version` header beyond what configuration information that we
# might have had available.
def dup_from_response(resp)
return self if resp.nil?
# Faraday's API is a little unusual. Normally it'll produce a response
# object with a `headers` method, but on error what it puts into
# `e.response` is an untyped `Hash`.
headers = if resp.is_a?(Faraday::Response)
resp.headers
else
resp[:headers]
end
def dup_from_response_headers(headers)
context = dup
context.account = headers["Stripe-Account"]
context.api_version = headers["Stripe-Version"]

View File

@ -127,18 +127,6 @@ module Stripe
JSON.pretty_generate(@values)
end
# Re-initializes the object based on a hash of values (usually one that's
# come back from an API call). Adds or removes value accessors as necessary
# and updates the state of internal data.
#
# Please don't use this method. If you're trying to do mass assignment, try
# #initialize_from instead.
def refresh_from(values, opts, partial = false)
initialize_from(values, opts, partial)
end
extend Gem::Deprecate
deprecate :refresh_from, "#update_attributes", 2016, 1
# Mass assigns attributes on the model.
#
# This is a version of +update_attributes+ that takes some extra options
@ -192,7 +180,9 @@ module Stripe
def to_hash
maybe_to_hash = lambda do |value|
value && value.respond_to?(:to_hash) ? value.to_hash : value
return nil if value.nil?
value.respond_to?(:to_hash) ? value.to_hash : value
end
@values.each_with_object({}) do |(key, value), acc|
@ -256,6 +246,7 @@ module Stripe
#
unsaved = @unsaved_values.include?(k)
next unless options[:force] || unsaved || v.is_a?(StripeObject)
update_hash[k.to_sym] = serialize_params_value(
@values[k], @original_values[k], unsaved, options[:force], key: k
)
@ -268,16 +259,6 @@ module Stripe
update_hash
end
class << self
# This class method has been deprecated in favor of the instance method
# of the same name.
def serialize_params(obj, options = {})
obj.serialize_params(options)
end
extend Gem::Deprecate
deprecate :serialize_params, "#serialize_params", 2016, 9
end
# A protected field is one that doesn't get an accessor assigned to it
# (i.e. `obj.public = ...`) and one which is not allowed to be updated via
# the class level `Model.update(id, { ... })`.

View File

@ -4,6 +4,53 @@ module Stripe
# StripeResponse encapsulates some vitals of a response that came back from
# the Stripe API.
class StripeResponse
# Headers provides an access wrapper to an API response's header data. It
# mainly exists so that we don't need to expose the entire
# `Net::HTTPResponse` object while still getting some of its benefits like
# case-insensitive access to header names and flattening of header values.
class Headers
# Initializes a Headers object from a Net::HTTP::HTTPResponse object.
def self.from_net_http(resp)
new(resp.to_hash)
end
# `hash` is expected to be a hash mapping header names to arrays of
# header values. This is the default format generated by calling
# `#to_hash` on a `Net::HTTPResponse` object because headers can be
# repeated multiple times. Using `#[]` will collapse values down to just
# the first.
def initialize(hash)
if !hash.is_a?(Hash) ||
!hash.keys.all? { |n| n.is_a?(String) } ||
!hash.values.all? { |a| a.is_a?(Array) } ||
!hash.values.all? { |a| a.all? { |v| v.is_a?(String) } }
raise ArgumentError,
"expect hash to be a map of string header names to arrays of " \
"header values"
end
@hash = {}
# This shouldn't be strictly necessary because `Net::HTTPResponse` will
# produce a hash with all headers downcased, but do it anyway just in
# case an object of this class was constructed manually.
#
# Also has the effect of duplicating the hash, which is desirable for a
# little extra object safety.
hash.each do |k, v|
@hash[k.downcase] = v
end
end
def [](name)
values = @hash[name.downcase]
if values && values.count > 1
warn("Duplicate header values for `#{name}`; returning only first")
end
values ? values.first : nil
end
end
# The data contained by the HTTP body of the response deserialized from
# JSON.
attr_accessor :data
@ -20,30 +67,15 @@ module Stripe
# The Stripe request ID of the response.
attr_accessor :request_id
# Initializes a StripeResponse object from a Hash like the kind returned as
# part of a Faraday exception.
#
# This may throw JSON::ParserError if the response body is not valid JSON.
def self.from_faraday_hash(http_resp)
resp = StripeResponse.new
resp.data = JSON.parse(http_resp[:body], symbolize_names: true)
resp.http_body = http_resp[:body]
resp.http_headers = http_resp[:headers]
resp.http_status = http_resp[:status]
resp.request_id = http_resp[:headers]["Request-Id"]
resp
end
# Initializes a StripeResponse object from a Faraday HTTP response object.
#
# This may throw JSON::ParserError if the response body is not valid JSON.
def self.from_faraday_response(http_resp)
# Initializes a StripeResponse object from a Net::HTTP::HTTPResponse
# object.
def self.from_net_http(http_resp)
resp = StripeResponse.new
resp.data = JSON.parse(http_resp.body, symbolize_names: true)
resp.http_body = http_resp.body
resp.http_headers = http_resp.headers
resp.http_status = http_resp.status
resp.request_id = http_resp.headers["Request-Id"]
resp.http_headers = Headers.from_net_http(http_resp)
resp.http_status = http_resp.code.to_i
resp.request_id = http_resp["request-id"]
resp
end
end

View File

@ -198,11 +198,13 @@ module Stripe
def self.check_string_argument!(key)
raise TypeError, "argument must be a string" unless key.is_a?(String)
key
end
def self.check_api_key!(key)
raise TypeError, "api_key must be a string" unless key.is_a?(String)
key
end
@ -245,14 +247,14 @@ module Stripe
#
COLOR_CODES = {
black: 0, light_black: 60,
red: 1, light_red: 61,
green: 2, light_green: 62,
yellow: 3, light_yellow: 63,
blue: 4, light_blue: 64,
black: 0, light_black: 60,
red: 1, light_red: 61,
green: 2, light_green: 62,
yellow: 3, light_yellow: 63,
blue: 4, light_blue: 64,
magenta: 5, light_magenta: 65,
cyan: 6, light_cyan: 66,
white: 7, light_white: 67,
cyan: 6, light_cyan: 66,
white: 7, light_white: 67,
default: 9,
}.freeze
private_constant :COLOR_CODES
@ -281,10 +283,7 @@ module Stripe
end
private_class_method :level_name
# TODO: Make these named required arguments when we drop support for Ruby
# 2.0.
def self.log_internal(message, data = {}, color: nil, level: nil,
logger: nil, out: nil)
def self.log_internal(message, data = {}, color:, level:, logger:, out:)
data_str = data.reject { |_k, v| v.nil? }
.map do |(k, v)|
format("%<key>s=%<value>s",

View File

@ -1,5 +1,5 @@
# frozen_string_literal: true
module Stripe
VERSION = "4.24.0".freeze
VERSION = "4.24.0"
end

View File

@ -22,7 +22,7 @@ module Stripe
end
module Signature
EXPECTED_SCHEME = "v1".freeze
EXPECTED_SCHEME = "v1"
def self.compute_signature(payload, secret)
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, payload)

View File

@ -7,7 +7,7 @@ require "stripe/version"
Gem::Specification.new do |s|
s.name = "stripe"
s.version = Stripe::VERSION
s.required_ruby_version = ">= 2.1.0"
s.required_ruby_version = ">= 2.3.0"
s.summary = "Ruby bindings for the Stripe API"
s.description = "Stripe is the easiest way to accept payments online. " \
"See https://stripe.com for details."
@ -17,18 +17,15 @@ Gem::Specification.new do |s|
s.license = "MIT"
s.metadata = {
"bug_tracker_uri" => "https://github.com/stripe/stripe-ruby/issues",
"changelog_uri" =>
"bug_tracker_uri" => "https://github.com/stripe/stripe-ruby/issues",
"changelog_uri" =>
"https://github.com/stripe/stripe-ruby/blob/master/CHANGELOG.md",
"documentation_uri" => "https://stripe.com/docs/api/ruby",
"github_repo" => "ssh://github.com/stripe/stripe-ruby",
"homepage_uri" => "https://stripe.com/docs/api/ruby",
"source_code_uri" => "https://github.com/stripe/stripe-ruby",
"github_repo" => "ssh://github.com/stripe/stripe-ruby",
"homepage_uri" => "https://stripe.com/docs/api/ruby",
"source_code_uri" => "https://github.com/stripe/stripe-ruby",
}
s.add_dependency("faraday", "~> 0.13")
s.add_dependency("net-http-persistent", "~> 3.0")
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- test/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n")

View File

@ -82,22 +82,6 @@ module Stripe
assert persons.data[0].is_a?(Stripe::Person)
end
context "#bank_account=" do
should "warn that #bank_account= is deprecated" do
old_stderr = $stderr
$stderr = StringIO.new
begin
account = Stripe::Account.retrieve("acct_123")
account.bank_account = "tok_123"
message = "NOTE: Stripe::Account#bank_account= is " \
"deprecated; use #external_account= instead"
assert_match Regexp.new(message), $stderr.string
ensure
$stderr = old_stderr
end
end
end
context "#deauthorize" do
should "deauthorize an account" do
account = Stripe::Account.retrieve("acct_123")

View File

@ -7,7 +7,7 @@ module Stripe
class UpdateableResource < APIResource
include Stripe::APIOperations::Save
OBJECT_NAME = "updateableresource".freeze
OBJECT_NAME = "updateableresource"
def self.protected_fields
[:protected]
@ -34,7 +34,7 @@ module Stripe
context ".nested_resource_class_methods" do
class MainResource < APIResource
extend Stripe::APIOperations::NestedResource
OBJECT_NAME = "mainresource".freeze
OBJECT_NAME = "mainresource"
nested_resource_class_methods :nested,
operations: %i[create retrieve update delete list]
end

Some files were not shown because too many files have changed in this diff Show More