Compatibility with various versions of the Stripe API
andrewsolomon opened this issue · 32 comments
For context, see here: https://stripe.com/docs/upgrades
The Stripe API is frequently updated and when the change is backwards-incompatible it is a new version. Every Stripe account has a setting indicating which version of the API it's using.
At present Net::Stripe is version agnostic which means... not much! In practice @fheyer has shown that this module is valid only for versions up to 2015-02-16.
It would good for this module to be version aware so that it can be used against all versions of the Stripe API. This is a major design change so any ideas on the best approach would be very welcome.
I just noticed this as integrating with a Catalyst app and the post_charge has card =>
when it's now source => 'tok_blah'
What's the plan?
Thanks for bringing this up again!
We can identify breaking changes in the api which have to be dealt with.
Those are labeled major in the api changelog: https://stripe.com/docs/upgrades
Goals
I think there are multiple goals when dealing with the versioned api and the versioned perl module. There may be more goals and not all have to be met.
- the newest module version should be compatible with the current api version
- avoid breaking existing installations during module updates
- allow using older api versions (which ones?)
Proposal
Major changes which affect the module can be selected by a version flag during module init. The default can be the api version which is supported by the current module version (i think it was stable for a long time).
Two obvious versions with major differences would be up to 2015-02-16 and later. So 2015-02-16 ist default and 2015-02-18 can be selected.
Your proposal sounds good from the user's perspective @fheyer. I've just been looking through the Ruby and Python libraries which are maintained by Stripe to see how they manage versioning.
https://github.com/stripe/stripe-python
https://github.com/stripe/stripe-ruby
As far as I can tell, the code tries to be 'version agnostic' by:
- for any request, providing optionally all the fields which have been available in previous versions of the API
- not performing much logic on the response data
This is not quite as developer friendly as Net::Stripe which:
- does type casting of fields in objects for both requests and responses
- makes some object attributes 'required' (which may change across versions)
- turning non-scalar fields of a response into the relevant Net::Stripe::? objects
While this makes Net::Stripe the Rolls Royce of Stripe libraries, achieving compatibility across different versions will be hard to achieve without diluting some of these design principles.
If someone wants to experiment with implementing the latest version on top of this I look forward to seeing a pull request. I think the main requirement is that the existing unit tests are kept to ensure backward compatibility hasn't been lost.
I'll be working on this in the next week or so.
@andrewsolomon, based on some needs i have at the present, i have started identifying the changes required to make all of the unit tests pass under the post-2015-02-18 API, and it does seem that allowing switching between API versions and keeping the code clean and maintainable will be a challenge in some places. in others, it may simply be a matter of deciding on an approach and coding style.
for example, i am not familiar enough with Moose to know which of these is preferable, or even will/won't work
if ( ! $stripe->api_version() || $stripe->api_version() ge '2015-02-18' ) {
has 'source' => (is => 'ro', isa => 'Maybe[Net::Stripe::Token|Net::Stripe::Card|Str]');
} else {
has 'card' => (is => 'ro', isa => 'Maybe[Net::Stripe::Token|Net::Stripe::Card|Str]');
}
vs
my $source_field;
if ( ! $stripe->api_version() || $stripe->api_version() ge '2015-02-18' ) {
$source_field = 'source';
} else {
$source_field = 'card';
}
has $source_field => (is => 'ro', isa => 'Maybe[Net::Stripe::Token|Net::Stripe::Card|Str]');
and are the conditions better expressed as
if ( ! $stripe->api_version() || $stripe->api_version() ge '2015-02-18' ) {
or
if ( ! $API_VERSION_GTE_20150218 ) {
etc.
any input or direction that you can provide will be greatly appreciated.
Starting with the easy bit, I like:
$stripe->api_version()
Now I'm trying to understand the if-statement. My understanding is that this is saying "if I'm on the new version"
if ( ! $stripe->api_version() || $stripe->api_version() ge '2015-02-18' )
so that seems to suggest that if api_version
isn't set we're on the latest version?
I think that's going to cause problems for everyone on an older version. I would prefer keeping the existing functionality unless you declare a particular version to ensure that things keep running as they are if people install the latest Net::Stripe for their old code. In other words:
if ( $stripe->api_version() && $stripe->api_version() ge '2015-02-18' )
Regarding having different attributes depending upon the version, I think one approach would be to encapsulate each version in a role and apply the appropriate role when an object is instantiated/when the version attribute is called.
This is me working out how to do that:
Let me know how it goes!
so that seems to suggest that if api_version isn't set we're on the latest version?
yes, that is how i wrote the logic, but i am ambivalent about it. the stripe docs indicate that version is set for an individual account the first time an API request is made, https://stripe.com/docs/upgrades#what-is-my-api-version. and as far as i can tell, we cannot easily determine programtically what that value is. so it seems that with api_version unspecified, there is potential for breakage for
- accounts that are relatively new, such as mine
- accounts where the API upgrade has been manually triggered through the dashboard
- accounts still using an older API version.
depending on how we define the default behavior.
one solution is to force users to specify an api_version with new installs. that way they are both in direct control of the setting, and are acutely aware of its existence for when/if they decide to upgrade.
WTY(what think ye)?
i'll try to absorb the recommended reading and get back to you with any questions. thanks.
here is an interesting data point related to the conversation about handling unspecified api_version: after making all the changes necessary for the unit tests to pass using the post-2015-02-18 API, the tests also do not fail when forcing a older API version. now given that i am presently at the end of a very long day, i fully reserve the right to be completely wrong, or to have overlooked something obvious, but given
- the indication in the API upgrade docs that Older API versions return both the card attribute and the source attribute and that card was simply being deprecated
- my interpretation of Stripe's approach to backwards compatibility
it is entirely possible that what i am seeing is the expected behavior, at least as it regards this particular API change. while searching for a sanity check, i was able to make an unrelated feature fail by selecting the 2011-06-21 API.
so, if indeed what i am seeing is correct, how does this affect your position on how to appropriately maintain versioning, or even whether versioning even needs to be addressed as it relates to 2015-02-18.
of course #84 is still useful as it allows developers to proactively test API versions newer than their default. but it looks like this issue could be resolved with a commit of my current changes, once they have been prettied up.
@andrewsolomon, how do i update the inline POD in Stripe.pm. ie, is it edited manually, is there some script/method to update it automatically based on the Moose constructs, etc?
here's one of my testing idioms, in case it proves helpful to someone else working on API compatibility:
#~$ lynx -dump -nolist -width=1000 https://stripe.com/docs/upgrades | grep -E -e '^[0-9]{4}-[0-9]{2}-[0-9]{2}$' -e '^ \* Major' | grep -E -B1 '^ \* Major' | grep -E -e '^[0-9]{4}-[0-9]{2}-[0-9]{2}$' > /tmp/major-versions.txt
#~$ while read i; do echo $i; STRIPE_API_VERSION="$i" prove -flq t/live.t; echo; echo; done < /tmp/major-versions.txt
@sherrardb Go ahead and edit the pod in Stripe.pm and dzil will update README.pod with those changes.
@sherrardb regarding your observation that the newer versions return both card and source attributes - that's helpful - but I don't believe this is the standard pattern with all Stripe's version changes.
Although not backward compatible, the idea of forcing the caller to provide a version has its merits.
@andrewsolomon, can you take a look at the latest commit fd6638c to api_gte_20150218 and assess whether your suggested role-based approach appears to be a good fit, based on the required changes to object attributes, object method arguments and the unit tests?
thanks
I understand your questions to be:
- Can we apply roles in order to make this change version dependent?
- Would it be a sensible way to go about it based on the changes this entails?
In answer to both, I've got to say I'm not sure because:
- I have a very limited understanding of Kavorka (but I'm sure we'll learn a lot)
- I don't know how much has to change to make Net::Stripe agree with the version you're targeting.
If you're willing to give it a go though, it will at least be a good learning experience! :-)
If the changes are only tiny, you might want to put "if (version) {} " in various places, but I think a safer and more robust approach would be to add roles like
Net::Stripe::Version::V20150218::Stripe
Net::Stripe::Version::V20120518::Stripe
Net::Stripe::Version::V20120518::Stripe::Invoice
where all the changeable attributes and methods exist.
I understand your questions to be:
- Can we apply roles in order to make this change version dependent?
- Would it be a sensible way to go about it based on the changes this entails?
exactly
I don't know how much has to change to make Net::Stripe agree with the version you're targeting.
that was the intention behind the "quick commit" on my api_gte_20150218 branch. i wanted to outline the scope of the changes by doing "the minimum" to get the tests passing in 2015-02-18 and newer. and it is from this that i was hoping you could asses how to approach the necessary changes to:
- t/live.t
- object attributes, per 'has'
- object method arguments
i think i've hit a bit of a chicken-and-egg situation with dynamic roles. in Stripe.pm, we have
$customer = Net::Stripe::Customer->new(account_balance => $account_balance,
source => $source,
coupon => $coupon,
description => $description,
email => $email,
metadata => $metadata,
plan => $plan,
quantity => $quantity,
trial_end => $trial_end);
while we can easily pass the api_version => "2015-02-18" along to Customer->new() so that it can determine that it needs to apply the role Net::Stripe::Version::V20120518::Stripe::Customer vs Net::Stripe::Version::V20120516::Stripe::Customer, one reason for that role is to supply
has 'source' => (is => 'rw', isa => 'Maybe[Net::Stripe::Token|Net::Stripe::Card|Str|ArrayRef]');
but by the time we are applying the role, the instance has already been created and the source attribute has been dropped since it is not in the base attribute list for the class.
so, unless my approach is way off base, and my search-foo is weak, it's not clear to me how Moose attributes can be applied "conditionally".
#!/usr/bin/perl
use strict;
use warnings;
{
package Methods;
use strict;
use warnings;
use Moose;
use Moose::Util;
sub extened { my $self = shift(); print "called default method\n"; }
sub print_all {
my $self = shift();
my %attr = %{ Moose::Util::get_all_attribute_values($self->meta, $self) || {} };
print "object attributes\n";
foreach my $k ( sort( keys( %attr ) ) ) {
printf " '%s' => '%s'\n", $k, $attr{$k} || '';
}
}
1;
}
{
package WithC;
use strict;
use warnings;
use Moose;
extends 'Methods';
has 'a' => (is => 'rw', isa => 'Maybe[Str]');
has 'b' => (is => 'rw', isa => 'Maybe[Str]');
has 'c' => (is => 'rw', isa => 'Maybe[Str]');
1;
}
{
package WithoutC;
use strict;
use warnings;
use Moose;
use Moose::Util;
extends 'Methods';
has 'a' => (is => 'rw', isa => 'Maybe[Str]');
has 'b' => (is => 'rw', isa => 'Maybe[Str]');
sub BUILD {
my $self = shift();
my %args = @_ == 1 ? %{ $_[0] } : @_;
Moose::Util::apply_all_roles( $self, 'AddC' );
return $self;
}
1;
}
{
package AddC;
use strict;
use warnings;
use Moose::Role;
has 'c' => (is => 'rw', isa => 'Maybe[Str]');
sub extened { my $self = shift(); print "called role method\n"; }
1;
}
foreach my $class ( qw/WithC WithoutC/ ) {
print "class: $class\n";
my $instance = $class->new(
a=> 1,
b=> 2,
c=> 3,
);
$instance->extened();
$instance->print_all();
print "\n";
}
produces
class: WithC
called default method
object attributes
'a' => '1'
'b' => '2'
'c' => '3'
class: WithoutC
called role method
object attributes
'a' => '1'
'b' => '2'
nevermind, i think.
after doing some more searching, and reading with fresh eyes, i see that apply_all_roles can be called not only on instances, but on classes as well.
#!/usr/bin/perl
use strict;
use warnings;
{
package Methods;
use strict;
use warnings;
use Moose;
use Moose::Util;
sub extened { my $self = shift(); print "called default method\n"; }
sub print_all {
my $self = shift();
my %attr = %{ Moose::Util::get_all_attribute_values($self->meta, $self) || {} };
print "object attributes\n";
foreach my $k ( sort( keys( %attr ) ) ) {
printf " '%s' => '%s'\n", $k, $attr{$k} || '';
}
}
1;
}
{
package WithC;
use strict;
use warnings;
use Moose;
extends 'Methods';
has 'a' => (is => 'rw', isa => 'Maybe[Str]');
has 'b' => (is => 'rw', isa => 'Maybe[Str]');
has 'c' => (is => 'rw', isa => 'Maybe[Str]');
1;
}
{
package WithoutC;
use strict;
use warnings;
use Moose;
use Moose::Util;
extends 'Methods';
has 'a' => (is => 'rw', isa => 'Maybe[Str]');
has 'b' => (is => 'rw', isa => 'Maybe[Str]');
after 'BUILDARGS' => sub {
my $class = shift();
my %args = @_ == 1 ? %{ $_[0] } : @_;
Moose::Util::apply_all_roles( $class->meta, 'AddC' );
};
1;
}
{
package AddC;
use strict;
use warnings;
use Moose::Role;
has 'c' => (is => 'rw', isa => 'Maybe[Str]');
sub extened { my $self = shift(); print "called role method\n"; }
1;
}
foreach my $class ( qw/WithC WithoutC/ ) {
print "class: $class\n";
my $instance = $class->new(
a=> 1,
b=> 2,
c=> 3,
);
$instance->extened();
$instance->print_all();
print "\n";
}
produces
class: WithC
called default method
object attributes
'a' => '1'
'b' => '2'
'c' => '3'
class: WithoutC
called role method
object attributes
'a' => '1'
'b' => '2'
'c' => '3'
as desired. now pivoting back to apply the proof-of-concept to the actual code.
ok, no dice. in short, since we are applying the roles to the class, and not an instance, the roles "stay applied", and so the next instance you create in that class, has any roles previously applied.
i suspect that this is a non-starter for any kind of long-running/shared memory process where one might have need to use different API versions in different contexts. the fact that i tripped across it in my test script is probably as good an indicator as any.
I'm a bit confused in trying to map the examples above to what you're doing in Net::Stripe, but I suspect the problem is that all the methods in Net::Stripe
are creating new objects like Net::Stripe::Customer
, Net::Stripe::Refund
and not applying the version role to them.
I've had further thoughts on how to make this simpler. Instead of implementing a whole version as a role, it may be simpler to identify each "change" as a role and the version class determines which "change roles" to apply to each object. In this way we don't implement a change for each of the subsequent version roles.
If you remind me of exactly what the version and changes are you need, I'll experiment to see whether what I'm saying makes any sense at all.
i'm sure that the fault is mine for not giving enough context. the problem is actually not what you describe, as a actually started by applying the changes in Customer, which is a good, concrete example, given that it needs to switch between card -> source. the problem is (actually was) that since the roles also provide attribute constraints, they cannot be applied to an existing instance, since that instance has already now dropped the arguments for which it does not have attributes defined in the main class.
Customer.pm
# no 'card' or 'source' in the attribute section
...
after 'BUILDARGS' => sub {
my $class = shift();
my %args = @_ == 1 ? %{ $_[0] } : @_;
if ( $args{api_version} ) {
my @roles;
if ( $args{api_version} ge '2015-02-18' ) {
push @roles, 'Net::Stripe::Version::V20150218::Stripe::Customer';
} else {
push @roles, 'Net::Stripe::Version::DEFAULT::Stripe::Customer';
}
warn "applying roles (".join( ', ', @roles).") to $class\n";
$class->meta->make_mutable;
Moose::Util::apply_all_roles( $class->meta, @roles );
$class->meta->make_immutable;
}
foreach my $role ( 'Net::Stripe::Version::DEFAULT::Stripe::Customer', 'Net::Stripe::Version::V20150218::Stripe::Customer' ) {
if ( Moose::Util::does_role( $class, $role ) ) {
warn "$class does role $role\n";
} else {
warn "$class does not do role $role\n";
}
}
};
...
{
package Net::Stripe::Version::V20150218::Stripe::Customer;
use Moose::Role;
use Kavorka;
# Customer creation args
has 'source' => (is => 'rw', isa => 'Maybe[Net::Stripe::Token|Net::Stripe::Card|Str]');
# API object args
has 'default_source' => (is => 'ro', isa => 'Maybe[Net::Stripe::Token|Net::Stripe::Card|Str]');
1;
}
{
package Net::Stripe::Version::DEFAULT::Stripe::Customer;
use Moose::Role;
use Kavorka;
# Customer creation args
has 'card' => (is => 'rw', isa => 'Maybe[Net::Stripe::Token|Net::Stripe::Card|Str]');
# API object args
has 'default_card' => (is => 'ro', isa => 'Maybe[Net::Stripe::Token|Net::Stripe::Card|Str]');
1;
}
t/customer.t
...
my @test_versions = qw( 2017-04-06 2015-02-16 );
foreach my $api_version ( @test_versions ) {
note( "start Net::Stripe tests for api_version '$api_version'" );
...
my $source_field_name = $api_version ge '2015-02-18' ? 'source' : 'card';
my $customer = $stripe->post_customer( $source_field_name => $fake_card );
...
}
produces
start Net::Stripe tests for api_version '2017-04-06'
applying roles (Net::Stripe::Version::V20150218::Stripe::Customer) to Net::Stripe::Customer
Net::Stripe::Customer does not do role Net::Stripe::Version::DEFAULT::Stripe::Customer
Net::Stripe::Customer does role Net::Stripe::Version::V20150218::Stripe::Customer
start Net::Stripe tests for api_version '2015-02-16'
applying roles (Net::Stripe::Version::DEFAULT::Stripe::Customer) to Net::Stripe::Customer
Net::Stripe::Customer does role Net::Stripe::Version::DEFAULT::Stripe::Customer
Net::Stripe::Customer does role Net::Stripe::Version::V20150218::Stripe::Customer
because the role is being applied to the class, and therefore is still applied the next time you instantiate an object from that class.
an, AFAICT, applying the roles to an instance, while it avoids the above, also means going full-on abstraction, with something along the lines of:
sub create {
my $class = shift();
my %args = @_ == 1 ? %{ $_[0] } : @_;
my $api_version = $args{api_version} || die Net::Stripe::Error->new(
type => "Net::Stripe object creation error",
message => "required argument api_version not passed",
);
my @roles;
if ( $args{api_version} ge '2015-02-18' ) {
push @roles, 'Net::Stripe::Version::V20150218::Stripe::Customer';
} else {
push @roles, 'Net::Stripe::Version::DEFAULT::Stripe::Customer';
}
my $instance = $class->new(%args);
Moose::Util::apply_all_roles( $instance, @roles );
return $instance;
}
and never calling $class->new() directly.
ps--please excuse the inline examples. my working copy, as i'm sure you can imagine, is in quite a messy state at the time, so i'm trying to avoid committing and pushing until i've at least settled on an approach and had some time for cleanup.
@andrewsolomon, not sure if you want a pull request for what is clearly only a proof-of-concept, but i have committed api_version_roles_w, which will hopefully clarify all of the above.
I'm just wondering - instead of using BUILDARGS why not use BUILD which acts on the object?
for two reasons, as i currently understand it.
a) by the time the object has been created, the non-defined attributes have already been omitted. per my convoluted example above, and in which i think i originally mis-pasted the wrong output, if you have a class whose base definition does not include a 'has' statement for a given attribute, that attribute is omitted from the object at creation time by Moose/MOP, regardless of whether you consume a role later on that provides for that attribute.
my $instance = $class->new(
a=> 1,
b=> 2,
c=> 3,
);
class: WithoutC
called role method
object attributes
'a' => '1'
'b' => '2'
specifically, in the case of N::S::Customer, since the card/source attributes are applied to the consumed role,
N::S:Customer->new( card=> $stripe_returned_card_data )
ends up with no value in the card attribute.
i'm 99% sure on this one, but i have also constructed the code so that you can test and probe in order to convince yourself, or correct my misstep. just change the value of $apply_to
in Stripe.pm and test the behavior for 2015-02-18.
b) even when you apply a role to an instance, you are not applying it to that instance, per-se, but rather to the class that that instance is a member of. see my comment above about why that results in all future objects created from that class also having that role applied. my testing, and copious reading, most of which seems to be replies from tobyink to that same type of question, have left me to conclude that this behavior is by design for a role-based system, and that the only dependable design approach is to use an object factory, and never call Class->new() directly.
but, by all means, i stand to be corrected. i have come at this a number of different ways, and have always run up against the same problem, in different forms. but that doesn't mean that i haven't made some simple misstep. that is why i structured the latest commits to be able to easily switch between the two methods of role application. both so that i could confirm the failure modes with minimal context switching, and so that others without my priors can interrogate the procedure as well. so in Stripe.pm you can apply to the instance, and confirm the arguments being passed to N::S::Customer->new(), and confirm the structure of the resulting object.
Thanks @sherrardb for explaining this with a good example to work off. I've now understood and - as you've said - it's not looking good. :-(
well i don't have a major problem with having to implement a non-new() object constructor. but my position may be based on a mis-assumption about how often users need to create objects directly, vs having them returned natively. thankfully most of the object creation happens in _hash_to_object().
of course, if you prefer not to change the API from the user perspective, i'm pretty sure you could extract all behavior away into roles. ie N::S::Customer->new() would create an object from a meta-class that consumes N::S::Version::BASE::N::S::Customer and N::S::Version::V20120518::N::S::Customer, or something like that. major change in the code, little change for the user.
but, this seems to be contrary to the style of architecture implied by use of Moose, basically seeking to shove a role-based peg into an inheritance-type hole. (disclaimer: working on this code, and the necessary reading, has been my first foray into role-based architecture).
so i'm guessing that you will have to do some individual introspection, and codify what you see as the long-term direction for this module.
if it helps, i can gather a list of the specific stackoverflow threads that have heavily informed my understanding of all the above. otherwise, here are some decent starting points:
I thought I'd chime in with this article from Stripe, as to how they implement the API changes under the hood:
https://stripe.com/blog/api-versioning
If we follow a similar approach here, the basic idea is that the SDK should target the most current version, and then we create change modules to target older versions, but only need to do this as far back as we want to support. The change modules are chained together, starting with latest to next-latest, then next-latest to next-next-latest, until reaching the target version.
At this point it's not clear which version of the API is supported by Net::Stripe. However, we do have a pretty good set of unit tests. So perhaps the best way might be to create an initial change module that targets the current set of unit tests, and go from there.
Being somewhat of a Moose newbie I don't have a clear sense how to implement this, but a starting point might be after and/or before function?
Just my 2c...
I plan to get actively involved in this project starting in the next few weeks, as I'd like to extend this SDK to include Stripe's Platform/Connect endpoints, and I am concerned about being API-version compliant.
What you're proposing is a big change but on reading the article it sounds very sensible.
At the back of my mind though is this issue: #28 Now to me, Kavorka is more a proof of concept and I like to think it's part of the community's effort at pushing this forward https://www.effectiveperlprogramming.com/2015/04/use-v5-20-subroutine-signatures/ https://perldoc.perl.org/perlsub.html#Signatures However I don't feel it's appropriate for use in a module like this where compatibility and stability are paramount.
For this reason I think the first step toward future-proofing Net::Stripe is to remove the dependency on Kavorka. Before jumping into that though, I'd like to hear other people's thoughts.
I guess the first place to look is here https://metacpan.org/pod/distribution/Moose/lib/Moose/Manual/MethodModifiers.pod with the "around" method modifier.
Something else to consider though - given that big changes are afoot - is Moo instead of Moose. It's much quicker than Moose in terms of compile time and there are no XS dependencies so it's easy to install. For the last few years all the new projects I've been involved in have been implemented with Moo.
https://metacpan.org/pod/Moo
https://perlmaven.com/moo
That said, you might want to stick with Moose because one thing Moo doesn't offer is the MOP which may make chaining really smooth.
https://metacpan.org/pod/distribution/Moose/lib/Moose/Manual/MOP.pod
https://metacpan.org/pod/distribution/Moose/lib/Moose/Cookbook/Meta/WhyMeta.pod
Sorry, I know I'm opening up more questions than answers but I just want to make sure we tread carefully! :-)
@andrewsolomon, @yahermann, sorry for the long silence. i was traveling abroad when i started working on this and i've been playing major catchup since getting back home.
my reading of the details of the Stripe API versioning approach doesn't seem to have any direct parallel to the challenges faced here. in a nutshell, the combination of subroutine signatures and role-based (vs inheritance-based) architecture means that we cannot easily apply dynamic adaptations to the subroutine signatures and by extension to the structures returned by Stripe from which the various objects are created. as such, that would mean that the issue is not a Moose thing or a Kavorka thing, but is related to the interplay between and the current architectural approach.
but, as i mentioned above, i would love to have someone else interrogate my commits and my testing methodology in order to either refute or validate my assessment.
i'm going to try to approach this with fresh(ish) eyes and see if there is some approach that is apparent now that was not apparent at the time. barring that, i will probably create a new branch and see what it looks like to address this problem with a "hard-versioning" approach. ie, skipping the attempts to apply roles dynamically, and actually hard-coding the changes required in order to be compatible with a given version of the Stripe API. my (vague) plan would be to then use git release tagging to identify the version that one intends to install. of course i don't have any idea as to whether this plays well with the CPAN packaging scheme, or what other problems there may be.
just to document a recent discussion between myself and @andrewsolomon , the likely path going forth, with regard to maintaining compatibility with different versions of the Stripe API, is to always target the latest version (that we are known to support) with the master branch and therefore the CPAN package, but to create branches that target older API versions. that way, if a given user needs to specify a particular API version, based on their account settings, out-of-date web stack, etc, they can download that particular branch from git and use dzil to install it. this approach also makes it possible to add new, version-agnostic features across all branches, while still maintaining the functionality of the non-backward-compatible API changes.