-
Notifications
You must be signed in to change notification settings - Fork 548
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add API resource instance methods to StripeClient #954
Conversation
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The primary regressions that I found during the first PR all had to do with nuances when it came time to merge the options. I was able to identify the scenarios and lock them in under test: https://github.com/stripe/stripe-ruby/pull/954/files#diff-50b82fc5b424d8d4c21c2ddd6c67485e40fbc76fdf5cc1e4320949a4ad20fb62R56
lib/stripe/resources/account.rb
Outdated
@@ -45,12 +45,8 @@ def resource_url | |||
end | |||
|
|||
# @override To make id optional | |||
def self.retrieve(id = ARGUMENT_NOT_PROVIDED, opts = {}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ARGUMENT_NOT_PROVIDED
doesn't play very well with how we merge options. The args
object in method_missing
is an array and merging options when an ID is omitted results in [nil, {}]
being sent to the resource, which isn't desirable.
However, I wasn't able to re-create any of the scenarios that warranted the addition of it in c269e9b and believe it's safe for removal.
lib/stripe/stripe_client.rb
Outdated
def store_last_response(object_id, resp) | ||
return unless last_response_has_key?(object_id) | ||
|
||
self.class.current_thread_context.last_responses[object_id] = resp | ||
end | ||
|
||
def last_response_has_key?(object_id) | ||
self.class.current_thread_context.last_responses&.key?(object_id) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rubocop didn't like the complexity of execute_request
so I extracted these out.
lib/stripe/stripe_configuration.rb
Outdated
def max_network_retry_delay=(val) | ||
@max_network_retry_delay = val.to_i | ||
end | ||
|
||
def initial_network_retry_delay=(val) | ||
@initial_network_retry_delay = val.to_i | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Figured these are worth exposing since everything else is.
@brandur-stripe this is the alternative approach I mentioned. The most noteworthy change is the testing strategy. Considering that testing the client implicitly tests the original implementation, I thought it was a viable path but I'm certainly open to feedback. Either way, I was able to find some edge cases and refactor some of the original logic with a high level of confidence. There are a few outstanding questions:
|
47e8dc1
to
b989aed
Compare
@brandur-stripe Moving the conversation over from #921. I'm a fan of extracting out the StripeClient configuration portion from this PR. Considering how large this PR is, that seems like a good seam for more manageable chunks. I'll follow-up with a PR this week. Regarding testing, I'm curious what your thoughts are one #954 (comment)? |
Awesome! And thanks for the quick reply on this. I also took a shot at this on Friday and although I didn't get it 100% working before the day ended, it was relatively straightforward. It was all your code though, and it'd be better if it all came in under your name, so will wait for your PR. Testing answers below.
@joeltaylor Nice. As long as the two usage types are relatively bound together, I think this approach is okay. The only part that strikes me as a bit a problem is that it's now hard to find usage examples of the original "style" in the test suite. IMO though, we should try to bias towards getting this merged and figure that out later.
Hmm, would the code to have it not be at the top level be a little gnarly? (Like, did you not add it because no special casing is needed right now?) I think it's fine as long as we're erroring, but hmm, I guess it would be slightly better if the invalid methods were not available at all. Separately: what do you think about potentially removing the pluralization of resources? It's not as "Ruby-ish", but the upsides are that we don't have to either (1) deal with pluralization oddities like "capabilitys", or (2) introduce a pluralization engine that can map the special exceptions. It might be simpler overall.
Ah, good to call these out.
Let's leave this one as is.
Hm, I know this is based off our weird endpoint naming, but IMO we should remap this to
We don't formally support EOLed Ruby, but we do sort of have a policy of trying to keep them on life support as long as possible. It kind of looks like from the failing build that it might be relatively easy to put in a patch that keeps 2.3 working (probably just an extra check on
Yeah, good call. That's the type of thing that I'm more than willing to do as well. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left some comments. Thanks for working on this @joeltaylor!
lib/stripe/oauth.rb
Outdated
opts[:client] ||= params[:client] || StripeClient.active_client | ||
opts[:api_base] ||= opts[:client].config.connect_base | ||
|
||
params.delete(:client) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OAuth methods are a bit weird, but I think the client should always be passed in opts
, not in params
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great call out. I'll see if I can get that behavior under test and will make the change.
lib/stripe/oauth.rb
Outdated
client = params[:client] || StripeClient.active_client | ||
base = opts[:connect_base] || client.config.connect_base |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here.
}.merge(api_object_names_to_classes) | ||
end | ||
|
||
# business objects | ||
def self.api_object_names_to_classes | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OOC, why is this change necessary? list
is an API object name 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ob-stripe it was done so I could get a list of API resources to dynamically define the methods here: https://github.com/stripe/stripe-ruby/pull/954/files#diff-992f231b0add61aeb46ab11fb68529936809a99a74ed9f1c1755a4b01f57fb58R63
Certainly open to other suggestions!
lib/stripe/stripe_client.rb
Outdated
@system_profiler = SystemProfiler.new | ||
@last_request_metrics = nil | ||
@config_overides = config_overides |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In e.g. stripe-php we support two different syntaxes for initializing clients:
- passing a configuration hash (what you're doing already)
- passing just the API key as a string
Would you mind adding support for the latter? I think it should be as simple as:
@config_overides = config_overrides.is_a?(String) ? {api_key: config_overrides} : config_overrides
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a great suggestion and certainly improves usability. I'll get it added!
@brandur-stripe I'll get this PR up to date with the main branch shortly. Wanted to give a 👍 to your answers and suggestions in #954 (comment) To recap:
I think we're in the home stretch 😄 |
55c4b2c
to
e3ed0a2
Compare
@@ -11,7 +11,7 @@ def list(filters = {}, opts = {}) | |||
|
|||
# set filters so that we can fetch the same limit, expansions, and | |||
# predicates when accessing the next and previous pages | |||
obj.filters = filters.dup | |||
obj.filters = filters.dup unless filters.nil? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's possible for filters to be nil
due to how we delegate methods to the API resources.
0c9a8c1
to
36d27d9
Compare
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.customer.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 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".
It's possible for `filters` to be `nil`, which causes Ruby 2.3 to raise an error: `TypeError: can't dup NilClass`
36d27d9
to
de9c3de
Compare
# Update `invoiceitems` to match snake case convention | ||
invoice_item_class = api_resources.delete("invoiceitem") | ||
api_resources["invoice_item"] = invoice_item_class |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@brandur-stripe Here's the update to turn invoiceitem
into invoice_item
that you previously mentioned.
@richardm-stripe Reassigned this to you since Brandur is no longer around. |
@richardm-stripe I'm going to close this PR out given it's been awhile and there are a few merge conflicts. Happy to revisit in the future if this is still a desired direction. |
This change introduces convenience methods to access API resources
through
StripeClient
for per-client configuration. The instance clientcan be configured with the same global Stripe configurations. As a
result, an instance of
StripeClient
is able to override or fallbackto the global configuration that was present at the time of
initialization.
Here's an example:
The primary workhorse for this feature is a new module called
Stripe::ClientAPIOperations
that defines instance methods onStripeClient
when it is included. AClientProxy
is used to send anymethod 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 nuancedirectly.
method_missing
is substantially slower than direct calls. Therefore,methods are defined where possible but
method_missing
is still usedat 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".
This is a clean-up of #921 as I felt that it didn't have reliable enough test coverage.