-
Notifications
You must be signed in to change notification settings - Fork 572
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
[WIP] [experimental] Interfaces for polymorphic API resources #1313
[WIP] [experimental] Interfaces for polymorphic API resources #1313
Conversation
Make internals visible to Newtonsoft.Json
Cross target netstandard2.0
This ensures that params/class structure matches the API. Also changed DateFilter to match the API and remove custom encoding and renamed it to DateRangeOptions
Move all services to BasicService
Remove +x flag from source files
Also made a few changes to some resources to match the API naming and added support for AddressJapan on the Account resource.
Move tests to their own folder
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.
So this looks really neat in concept. I really like it. I am just a little unsure about removing the Type
property on the ExternalAccount class. I know why you did it but is using GetType
really common enough that people would prefer that over an explicit type check?
EDIT: I realize that my sentence is confusing. GetType
is an explicit type check. I meant is it more common than doing if externalAccount.Type == "bank_account" ...
or whatever?
48462ff
to
e172fd7
Compare
I think calling I also just discovered the |
e172fd7
to
1e48083
Compare
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.
Added a few more comments, mostly for my own education
{ | ||
Id = incoming.SelectToken("id").ToString() | ||
}; | ||
var externalAccount = default(IExternalAccount); |
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.
didn't you say you did not need to use var
?
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.
To clarify: dynamic
can (but it's not a requirement) be used to tell the compiler that the exact type won't be known until runtime. It's not a silver bullet, because it disables compile-type checks. E.g. if I write this:
dynamic foo = account.ExternalAccounts.Data[0];
Console.WriteLine(foo.AccountHolderName)
The compiler has no way of knowing whether AccountHolderName
is a valid call. If it's not (because foo
is a Card
instead of a BankAccount
), a runtime exception will be raised.
A safer but more verbose alternative:
var foo = account.ExternalAccounts.Data[0]; // foo is an `IExternalAccount`
if (foo is BankAccount)
{
Console.WriteLine(((BankAccount)foo).AccountHolderName); // need to manually cast to BankAccount
}
else if (foo is Card)
{
Console.WriteLine(((Card)foo).Name); // need to manually cast to Card
}
else
{
// this should never be reached, unless a new concrete type is introduced for IExternalAccount
}
In the specific place of the code, we can use var
because we don't need to access any of specific properties of the concrete types. We could replace var
with IExternalAccount
an it would be a no-op (it's the type that is inferred by the compiler).
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.
Ack thanks for the thorough explanation. So the logic becomes similar to what we do in stripe-java. More verbose but safer and cleaner.
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.
Sorry for the lurk, but +1 the choice to avoid dynamic
if possible. It's one of these tools that can be useful in a huge variety of situations, but probably should be avoided in almost all of them because of loss of typedness is a very significant drawback (no more compiler checks, Intellisense, etc.).
|
||
public static class Mapper<T> | ||
{ | ||
private static JsonConverter[] converters = { | ||
new ExternalAccountConverter(), |
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.
so this is the new place where we would list all converters?
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.
Yep. Not sure if this is the best way to achieve this, but it seems to work and should be relatively efficient (it's static
, so only initialized once).
updateId(item.Id); | ||
updateObject(item); | ||
} | ||
else if (value is string) | ||
{ | ||
updateId((string)value); | ||
updateObject(null); | ||
updateObject(default(T)); |
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.
can you clarify what this line does/changes?
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 practice, nothing. default(T)
returns the default value for the type T
. For reference types, the default value is null
.
However, since T
is now constrained to be an interface rather than a class, the compiler complains that it cannot convert from null
to T
. This is (I think) because struct
s are value types but can implement interfaces.
{ | ||
public static void Map(object value, Action<string> updateId, Action<T> updateObject) | ||
{ | ||
if (value is JObject) | ||
{ | ||
T item = ((JToken)value).ToObject<T>(); | ||
// T item = ((JToken)value).ToObject<T>(); | ||
T item = Mapper<T>.MapFromJson(value.ToString()); |
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.
Not fully sure what all of this does
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 uses our custom deserialization instead of the default one, to ensure that our registered converters are used.
Assert.Equal("bank_account", externalAccount.BankAccount.Object); | ||
Assert.IsType<BankAccount>(externalAccount); | ||
var bankAccount = (BankAccount)externalAccount; | ||
Assert.Equal("bank_account", bankAccount.Object); |
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 you mentioned you did not need to use var
anymore
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.
I forgot to update this test. But as said above, using dynamic
is optional, it's basically syntactic sugar at the cost of type safety.
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.
Thinking about this a bit more, I think we could make Object
part of the IStripeEntity
interface (or maybe on a new IStripeEntityWithObject
interface) so that we can check its value without using dynamic
.
@@ -20,6 +20,7 @@ | |||
</ItemGroup> | |||
|
|||
<ItemGroup Condition=" '$(TargetFramework)' == 'net452' "> | |||
<Reference Include="Microsoft.CSharp" /> |
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.
What is this exactly?
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.
This is needed to use dynamic
with .NET Framework (at least for version 4.5.2). Note that this is only added in the tests project. Users of the library who use .NET Framework and want to use dynamic
would need to do the same thing, but the library doesn't make this a requirement.
Add support for InvoiceItem separate from InvoiceLineItem
Automatically convert Unix timestamps to DateTimes
1e48083
to
9eaf9b8
Compare
bd39334
to
79e5289
Compare
Closing this one because rebasing is a pain. |
An experiment to see if we could replace classes like
ExternalAccount
andPaymentSource
(which don't really exist in the API) by interfaces.