Skip to content

Commit c87403f

Browse files
committed
Add API resource instance methods to StripeClient
This change introduces a proof-of-concept to add convenience methods to access API resources through a StripeClient for per-client configuration. This first iteration only allows for the `api_key` to be configured but can be extended to allow other options such as `stripe_version`, which should solve stripe#872. The primary workhorse for this feature is a new module called `Stripe::ClientAPIOperations` that defines instance methods on `StripeClient` when it is included. A `ClientProxy` is used to send any method calls to an API resource with the instantiated client injected. There are a few noteworthy aspects of this approach: - Many resources are namespaced, which introduces a unique challenge when it comes to method chaining calls (e.g. client.issuing.authorizations). In order to handle those cases, we create a `ClientProxy` object for the root namespace (e.g., "issuing") and define all resource methods (e.g. "authorizations") at once to avoid re-defining the proxy object when there are multiple resources per namespace. - Sigma deviates from other namespaced API resources and does not have an `OBJECT_NAME` separated by a period. We account for that nuance directly. - `method_missing` is substantially slower than direct calls. Therefore, methods are defined where possible but `method_missing` is still used at the last step when delegating resource methods to the actual resource.
1 parent 0620436 commit c87403f

18 files changed

+578
-82
lines changed

lib/stripe.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
require "stripe/util"
3232
require "stripe/connection_manager"
3333
require "stripe/multipart_encoder"
34-
require "stripe/stripe_client"
3534
require "stripe/stripe_object"
3635
require "stripe/stripe_response"
3736
require "stripe/list_object"
@@ -40,10 +39,15 @@
4039
require "stripe/singleton_api_resource"
4140
require "stripe/webhook"
4241
require "stripe/stripe_configuration"
42+
require "stripe/client_api_operations"
4343

4444
# Named API resources
4545
require "stripe/resources"
4646

47+
# StripeClient requires API Resources to be loaded
48+
# due to dynamic methods defined by ClientAPIOperations
49+
require "stripe/stripe_client"
50+
4751
# OAuth
4852
require "stripe/oauth"
4953

@@ -62,6 +66,8 @@ module Stripe
6266
class << self
6367
extend Forwardable
6468

69+
attr_reader :configuration
70+
6571
# User configurable options
6672
def_delegators :@configuration, :api_key, :api_key=
6773
def_delegators :@configuration, :api_version, :api_version=

lib/stripe/api_operations/list.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ def list(filters = {}, opts = {})
99
resp, opts = execute_resource_request(:get, resource_url, filters, opts)
1010
obj = ListObject.construct_from(resp.data, opts)
1111

12+
filters ||= {}
1213
# set filters so that we can fetch the same limit, expansions, and
1314
# predicates when accessing the next and previous pages
1415
obj.filters = filters.dup

lib/stripe/client_api_operations.rb

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# frozen_string_literal: true
2+
3+
module Stripe
4+
# Define instance methods on the including class (i.e. StripeClient)
5+
# to access API resources.
6+
module ClientAPIOperations
7+
# Proxy object to inject the client into API resources. When included,
8+
# all resources are defined as singleton methods on the client in the
9+
# plural form (e.g. Stripe::Account => client.accounts).
10+
class ClientProxy
11+
def initialize(client:, resource: nil)
12+
@client = client
13+
@resource = resource
14+
end
15+
16+
attr_reader :client
17+
18+
def with_client(client)
19+
@client = client
20+
self
21+
end
22+
23+
# Used to either send a method to the API resource or the nested
24+
# ClientProxy when a resource is namespaced. Since the method signature
25+
# differs when operating on a collection versus a singular resource, it's
26+
# required to perform introspection on the parameters to respect any
27+
# passed in options or overrides.
28+
def method_missing(method, *args)
29+
super unless @resource
30+
opts_pos = @resource.method(method).parameters.index(%i[opt opts])
31+
args[opts_pos] = { client: @client }.merge(args[opts_pos] || {})
32+
33+
@resource.public_send(method, *args) || super
34+
end
35+
36+
def respond_to_missing?(symbol, include_private = false)
37+
super unless @resource
38+
@resource.respond_to?(symbol) || super
39+
end
40+
end
41+
42+
def self.included(base)
43+
base.class_eval do
44+
# Sigma, unlike other namespaced API objects, is not separated by a
45+
# period so we modify the object name to follow the expected convention.
46+
api_resources = Stripe::Util.api_object_classes
47+
sigma_class = api_resources.delete("scheduled_query_run")
48+
api_resources["sigma.scheduled_query_run"] = sigma_class
49+
50+
# Group namespaces that have mutiple resourses
51+
grouped_resources = api_resources.group_by do |key, _|
52+
key.include?(".") ? key.split(".").first : key
53+
end
54+
55+
grouped_resources.each do |resource_namespace, resources|
56+
# Namespace resource names are separated with a period by convention.
57+
if resources[0][0].include?(".")
58+
59+
# Defines the methods required for chaining calls for resources that
60+
# are namespaced. A proxy object is created so that all resource
61+
# methods can be defined at once.
62+
#
63+
# NOTE: At some point, a smarter pluralization scheme may be
64+
# necessary for resource names with complex pluralization rules.
65+
proxy = ClientProxy.new(client: nil)
66+
resources.each do |resource_name, resource_class|
67+
method_name = resource_name.split(".").last
68+
proxy.define_singleton_method("#{method_name}s") do
69+
ClientProxy.new(client: proxy.client, resource: resource_class)
70+
end
71+
end
72+
73+
# Defines the first method for resources that are namespaced. By
74+
# convention these methods are singular. A proxy object is returned
75+
# so that the client can be injected along the method chain.
76+
define_method(resource_namespace) do
77+
proxy.with_client(self)
78+
end
79+
else
80+
# Defines plural methods for non-namespaced resources
81+
define_method("#{resource_namespace}s".to_sym) do
82+
ClientProxy.new(client: self, resource: resources[0][1])
83+
end
84+
end
85+
end
86+
end
87+
end
88+
end
89+
end

lib/stripe/connection_manager.rb

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ class ConnectionManager
1616
# garbage collected or not.
1717
attr_reader :last_used
1818

19-
def initialize
19+
def initialize(config = Stripe.configuration)
20+
@config = config
2021
@active_connections = {}
2122
@last_used = Util.monotonic_time
2223

@@ -117,14 +118,14 @@ def execute_request(method, uri, body: nil, headers: nil, query: nil)
117118
# reused Go's default for `DefaultTransport`.
118119
connection.keep_alive_timeout = 30
119120

120-
connection.open_timeout = Stripe.open_timeout
121-
connection.read_timeout = Stripe.read_timeout
121+
connection.open_timeout = @config.open_timeout
122+
connection.read_timeout = @config.read_timeout
122123

123124
connection.use_ssl = uri.scheme == "https"
124125

125-
if Stripe.verify_ssl_certs
126+
if @config.verify_ssl_certs
126127
connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
127-
connection.cert_store = Stripe.ca_store
128+
connection.cert_store = @config.ca_store
128129
else
129130
connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
130131
warn_ssl_verify_none
@@ -138,10 +139,10 @@ def execute_request(method, uri, body: nil, headers: nil, query: nil)
138139
# out those pieces to make passing them into a new connection a little less
139140
# ugly.
140141
private def proxy_parts
141-
if Stripe.proxy.nil?
142+
if @config.proxy.nil?
142143
[nil, nil, nil, nil]
143144
else
144-
u = URI.parse(Stripe.proxy)
145+
u = URI.parse(@config.proxy)
145146
[u.host, u.port, u.user, u.password]
146147
end
147148
end

lib/stripe/oauth.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ module OAuthOperations
77

88
def self.execute_resource_request(method, url, params, opts)
99
opts = Util.normalize_opts(opts)
10-
opts[:client] ||= StripeClient.active_client
11-
opts[:api_base] ||= Stripe.connect_base
10+
opts[:client] ||= params[:client] || StripeClient.active_client
11+
opts[:api_base] ||= opts[:client].config.connect_base
1212

13+
params.delete(:client)
1314
super(method, url, params, opts)
1415
end
1516
end
@@ -29,7 +30,8 @@ def self.get_client_id(params = {})
2930
end
3031

3132
def self.authorize_url(params = {}, opts = {})
32-
base = opts[:connect_base] || Stripe.connect_base
33+
client = params[:client] || StripeClient.active_client
34+
base = opts[:connect_base] || client.config.connect_base
3335

3436
path = "/oauth/authorize"
3537
path = "/express" + path if opts[:express]

lib/stripe/object_types.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
# frozen_string_literal: true
22

33
# rubocop:disable Metrics/MethodLength
4-
54
module Stripe
65
module ObjectTypes
76
def self.object_names_to_classes
87
{
98
# data structures
109
ListObject::OBJECT_NAME => ListObject,
10+
}.merge(api_object_names_to_classes)
11+
end
1112

12-
# business objects
13+
def self.api_object_names_to_classes
14+
{
1315
Account::OBJECT_NAME => Account,
1416
AccountLink::OBJECT_NAME => AccountLink,
1517
AlipayAccount::OBJECT_NAME => AlipayAccount,
@@ -96,5 +98,4 @@ def self.object_names_to_classes
9698
end
9799
end
98100
end
99-
100101
# rubocop:enable Metrics/MethodLength

lib/stripe/resources/account.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ def deauthorize(client_id = nil, opts = {})
136136
client_id: client_id,
137137
stripe_user_id: id,
138138
}
139+
opts = @opts.merge(Util.normalize_opts(opts))
139140
OAuth.deauthorize(params, opts)
140141
end
141142

lib/stripe/resources/file.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ def self.create(params = {}, opts = {})
2525
end
2626
end
2727

28+
config = opts[:client]&.config || Stripe.configuration
2829
opts = {
29-
api_base: Stripe.uploads_base,
30+
api_base: config.uploads_base,
3031
content_type: MultipartEncoder::MULTIPART_FORM_DATA,
3132
}.merge(Util.normalize_opts(opts))
3233
super

0 commit comments

Comments
 (0)