-
Notifications
You must be signed in to change notification settings - Fork 548
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add API resource instance methods to StripeClient
This change introduces convenience methods to access API resources through `StripeClient` for per-client configuration. The instance client can be configured with the same global Stripe configurations. As a result, an instance of `StripeClient` is able to override or fallback to the global configuration that was present at the time of initialization. Here's an example: ```ruby Stripe::Customer.list() == StripeClient.new.customers.list() ``` 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. - Each API resource is pluralized to align with the conventions of other Stripe libraries (e.g. Node and PHP). The pluralization itself is quite naive but can easily be switched out for something more advanced once the need arises. - Each API resource spec was converted to use instance based methods and was done to ensure adequate test coverage. Since this entire feature is built on proxying methods, testing via the client implicitly tests the original implementation for "free".
- Loading branch information
1 parent
7d46045
commit b989aed
Showing
86 changed files
with
1,074 additions
and
527 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
# frozen_string_literal: true | ||
|
||
module Stripe | ||
# Define instance methods on the including class (i.e. StripeClient) | ||
# to access API resources. | ||
module ClientAPIOperations | ||
# Proxy object to inject the client into API resources. When included, | ||
# all resources are defined as singleton methods on the client in the | ||
# plural form (e.g. Stripe::Account => client.accounts). | ||
class ClientProxy | ||
def initialize(client:, resource: nil) | ||
@client = client | ||
@resource = resource | ||
end | ||
|
||
attr_reader :client | ||
|
||
def with_client(client) | ||
@client = client | ||
self | ||
end | ||
|
||
# Used to either send a method to the API resource or the nested | ||
# ClientProxy when a resource is namespaced. | ||
def method_missing(method, *args) | ||
super unless @resource.respond_to?(method) | ||
|
||
update_args_with_client!(method, args) | ||
|
||
@resource.public_send(method, *args) | ||
end | ||
|
||
def respond_to_missing?(symbol, include_private = false) | ||
super unless @resource | ||
@resource.respond_to?(symbol) || super | ||
end | ||
|
||
# Since the method signature differs when operating on a collection versus | ||
# a singular resource, it's required to perform introspection on the | ||
# parameters to respect any passed in options or overrides. | ||
# | ||
# Two noteworthy caveats: | ||
# 1) Does not merge into methods that use `_opts` as that means | ||
# the param is unused. | ||
# 2) Preserves incorrect options (e.g. passing nil) so that APIResource | ||
# can handle errors. | ||
def update_args_with_client!(method, args) | ||
opts_pos = @resource.method(method).parameters.index(%i[opt opts]) | ||
|
||
return unless opts_pos | ||
|
||
opts = opts_pos >= args.length ? {} : args[opts_pos] | ||
|
||
normalized_opts = Stripe::Util.normalize_opts(opts) | ||
args[opts_pos] = { client: @client }.merge(normalized_opts) | ||
end | ||
end | ||
|
||
def self.included(base) | ||
base.class_eval do | ||
# Sigma, unlike other namespaced API objects, is not separated by a | ||
# period so we modify the object name to follow the expected convention. | ||
api_resources = Stripe::Util.api_object_classes | ||
sigma_class = api_resources.delete("scheduled_query_run") | ||
api_resources["sigma.scheduled_query_run"] = sigma_class | ||
|
||
# Group namespaces that have mutiple resourses | ||
grouped_resources = api_resources.group_by do |key, _| | ||
key.include?(".") ? key.split(".").first : key | ||
end | ||
|
||
grouped_resources.each do |resource_namespace, resources| | ||
# Namespace resource names are separated with a period by convention. | ||
if resources[0][0].include?(".") | ||
|
||
# Defines the methods required for chaining calls for resources that | ||
# are namespaced. A proxy object is created so that all resource | ||
# methods can be defined at once. | ||
# | ||
# NOTE: At some point, a smarter pluralization scheme may be | ||
# necessary for resource names with complex pluralization rules. | ||
proxy = ClientProxy.new(client: nil) | ||
resources.each do |resource_name, resource_class| | ||
method_name = resource_name.split(".").last | ||
proxy.define_singleton_method("#{method_name}s") do | ||
ClientProxy.new(client: proxy.client, resource: resource_class) | ||
end | ||
end | ||
|
||
# Defines the first method for resources that are namespaced. By | ||
# convention these methods are singular. A proxy object is returned | ||
# so that the client can be injected along the method chain. | ||
define_method(resource_namespace) do | ||
proxy.with_client(self) | ||
end | ||
else | ||
# Defines plural methods for non-namespaced resources | ||
define_method("#{resource_namespace}s".to_sym) do | ||
ClientProxy.new(client: self, resource: resources[0][1]) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.