From 7679b81bab0a5d262bb5ea4055aa260b7bd4a2d3 Mon Sep 17 00:00:00 2001 From: Olivier Bellone Date: Tue, 29 Jan 2019 23:55:03 +0100 Subject: [PATCH] AnyOf<> generic class to handle polymorphic parameters --- .../FormEncoding/FormEncoder.cs | 4 + .../JsonConverters/AnyOfConverter.cs | 106 ++++++++ .../Services/Account/AccountSharedOptions.cs | 23 +- .../BalanceTransactionListOptions.cs | 11 +- .../BankAccounts/BankAccountCreateOptions.cs | 13 +- .../Services/Cards/CardCreateOptions.cs | 14 +- .../Services/Charges/ChargeCreateOptions.cs | 13 +- .../Customers/CustomerCreateOptions.cs | 20 +- .../Customers/CustomerUpdateOptions.cs | 17 +- .../ExternalAccountCreateOptions.cs | 30 ++- .../Services/Invoices/InvoiceListOptions.cs | 32 ++- .../Orders/OrderStatusTransitionsOptions.cs | 41 ++-- .../Services/Payouts/PayoutListOptions.cs | 11 +- .../Services/Plans/PlanCreateOptions.cs | 12 +- .../Services/Sources/SourceCreateOptions.cs | 7 +- .../SubscriptionScheduleListOptions.cs | 31 ++- .../Subscriptions/SubscriptionListOptions.cs | 25 +- .../Services/Tokens/TokenCreateOptions.cs | 33 ++- src/Stripe.net/Services/_base/AnyOf.cs | 227 ++++++++++++++++++ .../Services/_base/ListOptionsWithCreated.cs | 11 +- src/Stripe.net/Services/_interfaces/IAnyOf.cs | 12 + .../FormEncoding/FormEncoderTest.cs | 18 ++ .../JsonConverters/AnyOfConverterTest.cs | 110 +++++++++ .../Infrastructure/Public/StripeClientTest.cs | 2 +- .../Infrastructure/TestData/TestOptions.cs | 5 + .../Services/Accounts/AccountServiceTest.cs | 2 +- .../BankAccounts/BankAccountServiceTest.cs | 2 +- .../Services/Cards/CardServiceTest.cs | 2 +- .../Services/Charges/ChargeServiceTest.cs | 2 +- .../Services/Customers/CustomerServiceTest.cs | 2 +- .../ExternalAccountServiceTest.cs | 2 +- .../_base/ListOptionsWithCreatedTest.cs | 60 +++++ .../NoDuplicateJsonPropertyValues.cs | 60 +++++ 33 files changed, 834 insertions(+), 126 deletions(-) create mode 100644 src/Stripe.net/Infrastructure/JsonConverters/AnyOfConverter.cs create mode 100644 src/Stripe.net/Services/_base/AnyOf.cs create mode 100644 src/Stripe.net/Services/_interfaces/IAnyOf.cs create mode 100644 src/StripeTests/Infrastructure/JsonConverters/AnyOfConverterTest.cs create mode 100644 src/StripeTests/Services/_base/ListOptionsWithCreatedTest.cs create mode 100644 src/StripeTests/Wholesome/NoDuplicateJsonPropertyValues.cs diff --git a/src/Stripe.net/Infrastructure/FormEncoding/FormEncoder.cs b/src/Stripe.net/Infrastructure/FormEncoding/FormEncoder.cs index a13744d62b..fa67fa6cf6 100644 --- a/src/Stripe.net/Infrastructure/FormEncoding/FormEncoder.cs +++ b/src/Stripe.net/Infrastructure/FormEncoding/FormEncoder.cs @@ -100,6 +100,10 @@ private static List> FlattenParamsValue(object valu flatParams = SingleParam(keyPrefix, string.Empty); break; + case IAnyOf anyOf: + flatParams = FlattenParamsValue(anyOf.GetValue(), keyPrefix); + break; + case INestedOptions options: flatParams = FlattenParamsOptions(options, keyPrefix); break; diff --git a/src/Stripe.net/Infrastructure/JsonConverters/AnyOfConverter.cs b/src/Stripe.net/Infrastructure/JsonConverters/AnyOfConverter.cs new file mode 100644 index 0000000000..e326973038 --- /dev/null +++ b/src/Stripe.net/Infrastructure/JsonConverters/AnyOfConverter.cs @@ -0,0 +1,106 @@ +namespace Stripe.Infrastructure +{ + using System; + using System.Linq; + using System.Reflection; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + + /// + /// Converts a to and from JSON. + /// + public class AnyOfConverter : JsonConverter + { + /// + /// Gets a value indicating whether this can write JSON. + /// + /// + /// true if this can write JSON; otherwise, false. + /// + public override bool CanWrite => true; + + /// + /// Writes the JSON representation of the object. + /// + /// The to write to. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + switch (value) + { + case null: + serializer.Serialize(writer, null); + break; + + case IAnyOf anyOf: + serializer.Serialize(writer, anyOf.GetValue()); + break; + + default: + throw new JsonSerializationException(string.Format( + "Unexpected value when converting AnyOf. Expected IAnyOf, got {0}.", + value.GetType())); + } + } + + /// + /// Reads the JSON representation of the object. + /// + /// The to read from. + /// Type of the object. + /// The existing value of object being read. + /// The calling serializer. + /// The object value. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + object o = null; + + // Try to deserialize with each possible type + var jToken = JToken.Load(reader); + foreach (var type in objectType.GenericTypeArguments) + { + try + { + using (var subReader = jToken.CreateReader()) + { + o = serializer.Deserialize(subReader, type); + } + + // If deserialization succeeds, stop + break; + } + catch (JsonException) + { + // Do nothing, just try the next type + } + } + + if (o == null) + { + throw new JsonSerializationException(string.Format( + "Cannot deserialize the current JSON object into any of the expected types ({0}).", + string.Join(", ", objectType.GenericTypeArguments.Select(t => t.FullName)))); + } + + return Activator.CreateInstance(objectType, o); + } + + /// + /// Determines whether this instance can convert the specified object type. + /// + /// Type of the object. + /// + /// true if this instance can convert the specified object type; otherwise, false. + /// + public override bool CanConvert(Type objectType) + { + return typeof(IAnyOf).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()); + } + } +} diff --git a/src/Stripe.net/Services/Account/AccountSharedOptions.cs b/src/Stripe.net/Services/Account/AccountSharedOptions.cs index 65ac34bb11..77f1c120de 100644 --- a/src/Stripe.net/Services/Account/AccountSharedOptions.cs +++ b/src/Stripe.net/Services/Account/AccountSharedOptions.cs @@ -1,6 +1,5 @@ namespace Stripe { - using System; using System.Collections.Generic; using Newtonsoft.Json; using Stripe.Infrastructure; @@ -25,14 +24,22 @@ public abstract class AccountSharedOptions : BaseOptions [JsonProperty("email")] public string Email { get; set; } + /// + /// + /// A card or bank account to attach to the account. You can provide either a token, like + /// the ones returned by Stripe.js, or a + /// or instance. + /// + /// + /// By default, providing an external account sets it as the new default external account + /// for its currency, and deletes the old default if one exists. To add additional external + /// accounts without replacing the existing default for the currency, use the bank account + /// or card creation API. + /// + /// [JsonProperty("external_account")] - public string ExternalAccountId { get; set; } - - [JsonProperty("external_account")] - public AccountCardOptions ExternalCardAccount { get; set; } - - [JsonProperty("external_account")] - public AccountBankAccountOptions ExternalBankAccount { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf ExternalAccount { get; set; } [JsonProperty("metadata")] public Dictionary Metadata { get; set; } diff --git a/src/Stripe.net/Services/BalanceTransactions/BalanceTransactionListOptions.cs b/src/Stripe.net/Services/BalanceTransactions/BalanceTransactionListOptions.cs index 91712b1b8b..1ff18bcb7d 100644 --- a/src/Stripe.net/Services/BalanceTransactions/BalanceTransactionListOptions.cs +++ b/src/Stripe.net/Services/BalanceTransactions/BalanceTransactionListOptions.cs @@ -2,14 +2,17 @@ namespace Stripe { using System; using Newtonsoft.Json; + using Stripe.Infrastructure; public class BalanceTransactionListOptions : ListOptionsWithCreated { + /// + /// A filter on the list based on the object available_on field. The value can be a + /// or a . + /// [JsonProperty("available_on")] - public DateTime? AvailableOn { get; set; } - - [JsonProperty("available_on")] - public DateRangeOptions AvailableOnRange { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf AvailableOn { get; set; } [JsonProperty("currency")] public string Currency { get; set; } diff --git a/src/Stripe.net/Services/BankAccounts/BankAccountCreateOptions.cs b/src/Stripe.net/Services/BankAccounts/BankAccountCreateOptions.cs index 8a70143b5e..8e516ca2e9 100644 --- a/src/Stripe.net/Services/BankAccounts/BankAccountCreateOptions.cs +++ b/src/Stripe.net/Services/BankAccounts/BankAccountCreateOptions.cs @@ -1,13 +1,18 @@ namespace Stripe { using Newtonsoft.Json; + using Stripe.Infrastructure; public class BankAccountCreateOptions : BaseOptions { + /// + /// REQUIRED. Either a token, like the ones returned by + /// Stripe.js, or a + /// instance containing a user’s bank account + /// details. + /// [JsonProperty("source")] - public string SourceToken { get; set; } - - [JsonProperty("source")] - public SourceBankAccount SourceBankAccount { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf Source { get; set; } } } diff --git a/src/Stripe.net/Services/Cards/CardCreateOptions.cs b/src/Stripe.net/Services/Cards/CardCreateOptions.cs index 8043e08df3..712cbb6801 100644 --- a/src/Stripe.net/Services/Cards/CardCreateOptions.cs +++ b/src/Stripe.net/Services/Cards/CardCreateOptions.cs @@ -1,8 +1,8 @@ namespace Stripe { - using System; using System.Collections.Generic; using Newtonsoft.Json; + using Stripe.Infrastructure; public class CardCreateOptions : BaseOptions { @@ -12,11 +12,15 @@ public class CardCreateOptions : BaseOptions [JsonProperty("metadata")] public Dictionary Metadata { get; set; } + /// + /// REQUIRED. Either a token, like the ones returned by + /// Stripe.js, or a + /// instance containing a user’s card + /// details. + /// [JsonProperty("source")] - public string SourceToken { get; set; } - - [JsonProperty("source")] - public CardCreateNestedOptions SourceCard { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf Source { get; set; } [JsonProperty("validate")] public bool? Validate { get; set; } diff --git a/src/Stripe.net/Services/Charges/ChargeCreateOptions.cs b/src/Stripe.net/Services/Charges/ChargeCreateOptions.cs index ffbdd6dc7f..aa2ed54d5c 100644 --- a/src/Stripe.net/Services/Charges/ChargeCreateOptions.cs +++ b/src/Stripe.net/Services/Charges/ChargeCreateOptions.cs @@ -3,6 +3,7 @@ namespace Stripe using System; using System.Collections.Generic; using Newtonsoft.Json; + using Stripe.Infrastructure; public class ChargeCreateOptions : BaseOptions { @@ -92,11 +93,15 @@ public class ChargeCreateOptions : BaseOptions [JsonProperty("customer")] public string CustomerId { get; set; } + /// + /// A payment source to be charged. This can be the ID of a card (i.e., credit or debit + /// card), a bank account, a source, a token, or a connected account. For certain + /// sources—namely, cards, bank accounts, and attached sources—you must also pass the ID of + /// the associated customer. + /// [JsonProperty("source")] - public string SourceId { get; set; } - - [JsonProperty("source")] - public CardCreateNestedOptions SourceCard { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf Source { get; set; } /// /// An arbitrary string to be displayed on your customer's credit card statement. This may diff --git a/src/Stripe.net/Services/Customers/CustomerCreateOptions.cs b/src/Stripe.net/Services/Customers/CustomerCreateOptions.cs index 8209324f59..d49188d0f4 100644 --- a/src/Stripe.net/Services/Customers/CustomerCreateOptions.cs +++ b/src/Stripe.net/Services/Customers/CustomerCreateOptions.cs @@ -37,11 +37,23 @@ public class CustomerCreateOptions : BaseOptions [JsonProperty("shipping")] public ShippingOptions Shipping { get; set; } + /// + /// The source can either be a Token or a Source, as returned by + /// Elements, or a + /// containing a user’s credit card details. You must + /// provide a source if the customer does not already have a valid source attached, and you + /// are subscribing the customer to be charged automatically for a plan that is not free. + /// Passing source will create a new source object, make it the customer default + /// source, and delete the old customer default if one exists. If you want to add an + /// additional source, instead use + /// to add the + /// card and then + /// to set it as the default. Whenever you attach a card to a customer, Stripe will + /// automatically validate the card. + /// [JsonProperty("source")] - public string SourceToken { get; set; } - - [JsonProperty("source")] - public CardCreateNestedOptions SourceCard { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf Source { get; set; } [JsonProperty("tax_info")] public CustomerTaxInfoOptions TaxInfo { get; set; } diff --git a/src/Stripe.net/Services/Customers/CustomerUpdateOptions.cs b/src/Stripe.net/Services/Customers/CustomerUpdateOptions.cs index 85e02db1fd..d2f807975b 100644 --- a/src/Stripe.net/Services/Customers/CustomerUpdateOptions.cs +++ b/src/Stripe.net/Services/Customers/CustomerUpdateOptions.cs @@ -1,8 +1,8 @@ namespace Stripe { - using System; using System.Collections.Generic; using Newtonsoft.Json; + using Stripe.Infrastructure; public class CustomerUpdateOptions : BaseOptions { @@ -33,11 +33,18 @@ public class CustomerUpdateOptions : BaseOptions [JsonProperty("shipping")] public ShippingOptions Shipping { get; set; } + /// + /// A Token’s or a Source’s ID, as returned by + /// Elements. Passing source will + /// create a new source object, make it the new customer default source, and delete the old\ + /// customer default if one exists. If you want to add additional sources instead of + /// replacing the existing default, use + /// . Whenever + /// you attach a card to a customer, Stripe will automatically validate the card. + /// [JsonProperty("source")] - public string SourceToken { get; set; } - - [JsonProperty("source")] - public CardCreateNestedOptions SourceCard { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf Source { get; set; } [JsonProperty("tax_info")] public CustomerTaxInfoOptions TaxInfo { get; set; } diff --git a/src/Stripe.net/Services/ExternalAccounts/ExternalAccountCreateOptions.cs b/src/Stripe.net/Services/ExternalAccounts/ExternalAccountCreateOptions.cs index 64127ace15..34d8d0e6a8 100644 --- a/src/Stripe.net/Services/ExternalAccounts/ExternalAccountCreateOptions.cs +++ b/src/Stripe.net/Services/ExternalAccounts/ExternalAccountCreateOptions.cs @@ -2,19 +2,33 @@ namespace Stripe { using System.Collections.Generic; using Newtonsoft.Json; + using Stripe.Infrastructure; public class ExternalAccountCreateOptions : BaseOptions { + /// + /// REQUIRED. Either a token, like the ones returned by + /// Stripe.js, or a + /// instance containing a user’s bank account + /// details. + /// [JsonProperty("external_account")] - public string ExternalAccountTokenId { get; set; } - - [JsonProperty("external_account")] - public AccountBankAccountOptions ExternalAccountBankAccount { get; set; } - - [JsonProperty("metadata")] - public Dictionary Metadata { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf ExternalAccount { get; set; } + /// + /// When set to true, or if this is the first external account added in this + /// currency, this account becomes the default external account for its currency. + /// [JsonProperty("default_for_currency")] public bool? DefaultForCurrency { get; set; } - } + + /// + /// A set of key-value pairs that you can attach to an external account object. It can be + /// useful for storing additional information about the external account in a structured + /// format. + /// + [JsonProperty("metadata")] + public Dictionary Metadata { get; set; } + } } diff --git a/src/Stripe.net/Services/Invoices/InvoiceListOptions.cs b/src/Stripe.net/Services/Invoices/InvoiceListOptions.cs index a0b0ed5adc..e5240eea1e 100644 --- a/src/Stripe.net/Services/Invoices/InvoiceListOptions.cs +++ b/src/Stripe.net/Services/Invoices/InvoiceListOptions.cs @@ -2,6 +2,7 @@ namespace Stripe { using System; using Newtonsoft.Json; + using Stripe.Infrastructure; public class InvoiceListOptions : ListOptions { @@ -11,26 +12,36 @@ public class InvoiceListOptions : ListOptions [JsonProperty("billing")] public Billing? Billing { get; set; } - [JsonProperty("created")] - public DateTime? Created { get; set; } - /// - /// A filter on the list based on the object created field. + /// A filter on the list based on the object created field. The value can be a + /// or a . /// [JsonProperty("created")] - public DateRangeOptions CreatedRange { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf Created { get; set; } + /// + /// Only return invoices for the customer specified by this customer ID. + /// [JsonProperty("customer")] public string CustomerId { get; set; } - [JsonProperty("due_date")] - public DateTime? DueDate { get; set; } + /// + /// A filter on the list based on the object date field. The value can be a + /// or a . + /// + [Obsolete("Use Created instead")] + [JsonProperty("date")] + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf Date { get; set; } /// - /// A filter on the list based on the object due_date field. + /// A filter on the list based on the object due_date field. The value can be a + /// or a . /// [JsonProperty("due_date")] - public DateRangeOptions DueDateRange { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf DueDate { get; set; } /// /// A filter on the list based on the object paid field. @@ -38,6 +49,9 @@ public class InvoiceListOptions : ListOptions [JsonProperty("paid")] public bool? Paid { get; set; } + /// + /// Only return invoices for the subscription specified by this subscription ID. + /// [JsonProperty("subscription")] public string SubscriptionId { get; set; } } diff --git a/src/Stripe.net/Services/Orders/OrderStatusTransitionsOptions.cs b/src/Stripe.net/Services/Orders/OrderStatusTransitionsOptions.cs index 6d8735fe04..dea9e007f0 100644 --- a/src/Stripe.net/Services/Orders/OrderStatusTransitionsOptions.cs +++ b/src/Stripe.net/Services/Orders/OrderStatusTransitionsOptions.cs @@ -2,31 +2,40 @@ namespace Stripe { using System; using Newtonsoft.Json; + using Stripe.Infrastructure; public class OrderStatusTransitionsOptions : INestedOptions { + /// + /// A filter on the list based on the object canceled field. The value can be a + /// or a . + /// [JsonProperty("canceled")] - public DateTime? Canceled { get; set; } - - [JsonProperty("canceled")] - public DateRangeOptions CanceledRange { get; set; } - - [JsonProperty("fulfilled")] - public DateTime? Fulfilled { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf Canceled { get; set; } + /// + /// A filter on the list based on the object fulfilled field. The value can be a + /// or a . + /// [JsonProperty("fulfilled")] - public DateRangeOptions FulfilledRange { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf Fulfilled { get; set; } + /// + /// A filter on the list based on the object paid field. The value can be a + /// or a . + /// [JsonProperty("paid")] - public DateTime? Paid { get; set; } - - [JsonProperty("paid")] - public DateRangeOptions PaidRange { get; set; } - - [JsonProperty("returned")] - public DateTime? Returned { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf Paid { get; set; } + /// + /// A filter on the list based on the object returned field. The value can be a + /// or a . + /// [JsonProperty("returned")] - public DateRangeOptions ReturnedRange { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf Returned { get; set; } } } diff --git a/src/Stripe.net/Services/Payouts/PayoutListOptions.cs b/src/Stripe.net/Services/Payouts/PayoutListOptions.cs index 189bd32ce1..174150b6a9 100644 --- a/src/Stripe.net/Services/Payouts/PayoutListOptions.cs +++ b/src/Stripe.net/Services/Payouts/PayoutListOptions.cs @@ -2,14 +2,17 @@ namespace Stripe { using System; using Newtonsoft.Json; + using Stripe.Infrastructure; public class PayoutListOptions : ListOptionsWithCreated { + /// + /// A filter on the list based on the object arrival_date field. The value can be a + /// or a . + /// [JsonProperty("arrival_date")] - public DateTime? ArrivalDate { get; set; } - - [JsonProperty("arrival_date")] - public DateRangeOptions ArrivalDateRange { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf ArrivalDate { get; set; } [JsonProperty("destination")] public string Destination { get; set; } diff --git a/src/Stripe.net/Services/Plans/PlanCreateOptions.cs b/src/Stripe.net/Services/Plans/PlanCreateOptions.cs index 0d13ad7e96..26e4a5b82b 100644 --- a/src/Stripe.net/Services/Plans/PlanCreateOptions.cs +++ b/src/Stripe.net/Services/Plans/PlanCreateOptions.cs @@ -2,6 +2,7 @@ namespace Stripe { using System.Collections.Generic; using Newtonsoft.Json; + using Stripe.Infrastructure; public class PlanCreateOptions : BaseOptions { @@ -35,11 +36,14 @@ public class PlanCreateOptions : BaseOptions [JsonProperty("nickname")] public string Nickname { get; set; } + /// + /// The product whose pricing the created plan will represent. This can either be the ID of + /// an existing product, or a instance containing + /// fields used to create a service product. + /// [JsonProperty("product")] - public PlanProductCreateOptions Product { get; set; } - - [JsonProperty("product")] - public string ProductId { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf Product { get; set; } [JsonProperty("tiers")] public List Tiers { get; set; } diff --git a/src/Stripe.net/Services/Sources/SourceCreateOptions.cs b/src/Stripe.net/Services/Sources/SourceCreateOptions.cs index 52ae08a560..777c8335f5 100644 --- a/src/Stripe.net/Services/Sources/SourceCreateOptions.cs +++ b/src/Stripe.net/Services/Sources/SourceCreateOptions.cs @@ -2,6 +2,7 @@ namespace Stripe { using System.Collections.Generic; using Newtonsoft.Json; + using Stripe.Infrastructure; public class SourceCreateOptions : BaseOptions { @@ -39,9 +40,6 @@ public class SourceCreateOptions : BaseOptions [JsonProperty("flow")] public string Flow { get; set; } - [JsonProperty("card")] - public string CardId { get; set; } - /// /// Information about a mandate possiblity attached to a source object (generally for bank debits) as well as its acceptance status. /// @@ -102,7 +100,8 @@ Below we group all Source type specific paramters public SourceBancontactCreateOptions Bancontact { get; set; } [JsonProperty("card")] - public CreditCardOptions Card { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf Card { get; set; } [JsonProperty("ideal")] public SourceIdealCreateOptions Ideal { get; set; } diff --git a/src/Stripe.net/Services/SubscriptionSchedules/SubscriptionScheduleListOptions.cs b/src/Stripe.net/Services/SubscriptionSchedules/SubscriptionScheduleListOptions.cs index f08c4c44fe..120c68ca49 100644 --- a/src/Stripe.net/Services/SubscriptionSchedules/SubscriptionScheduleListOptions.cs +++ b/src/Stripe.net/Services/SubscriptionSchedules/SubscriptionScheduleListOptions.cs @@ -2,20 +2,25 @@ namespace Stripe { using System; using Newtonsoft.Json; + using Stripe.Infrastructure; public class SubscriptionScheduleListOptions : ListOptionsWithCreated { + /// + /// A filter on the list based on the object canceled_at field. The value can be a + /// or a . + /// [JsonProperty("canceled_at")] - public DateTime? CanceledAt { get; set; } - - [JsonProperty("canceled_at")] - public DateRangeOptions CanceledAtRange { get; set; } - - [JsonProperty("completed_at")] - public DateTime? CompletedAt { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf CanceledAt { get; set; } + /// + /// A filter on the list based on the object completed_at field. The value can be a + /// or a . + /// [JsonProperty("completed_at")] - public DateRangeOptions CompletedAtRange { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf CompletedAt { get; set; } /// /// Only return subscription schedules for the given customer. @@ -23,11 +28,13 @@ public class SubscriptionScheduleListOptions : ListOptionsWithCreated [JsonProperty("customer")] public string CustomerId { get; set; } + /// + /// A filter on the list based on the object released_at field. The value can be a + /// or a . + /// [JsonProperty("released_at")] - public DateTime? ReleasedAt { get; set; } - - [JsonProperty("released_at")] - public DateRangeOptions ReleasedAtRange { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf ReleasedAt { get; set; } /// /// Only return subscription schedules that have not started yet. diff --git a/src/Stripe.net/Services/Subscriptions/SubscriptionListOptions.cs b/src/Stripe.net/Services/Subscriptions/SubscriptionListOptions.cs index 36c905f77f..69fcbb4179 100644 --- a/src/Stripe.net/Services/Subscriptions/SubscriptionListOptions.cs +++ b/src/Stripe.net/Services/Subscriptions/SubscriptionListOptions.cs @@ -2,6 +2,7 @@ namespace Stripe { using System; using Newtonsoft.Json; + using Stripe.Infrastructure; public class SubscriptionListOptions : ListOptionsWithCreated { @@ -12,28 +13,20 @@ public class SubscriptionListOptions : ListOptionsWithCreated public Billing? Billing { get; set; } /// - /// A filter on the list based on the object current_period_end field. + /// A filter on the list based on the object current_period_end field. The value can + /// be a or a . /// [JsonProperty("current_period_end")] - public DateTime? CurrentPeriodEnd { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf CurrentPeriodEnd { get; set; } /// - /// A filter on the list based on the object current_period_end field. - /// - [JsonProperty("current_period_end")] - public DateRangeOptions CurrentPeriodEndRange { get; set; } - - /// - /// A filter on the list based on the object current_period_start field. - /// - [JsonProperty("current_period_start")] - public DateTime? CurrentPeriodStart { get; set; } - - /// - /// A filter on the list based on the object current_period_start field. + /// A filter on the list based on the object current_period_start field. The value + /// can be a or a . /// [JsonProperty("current_period_start")] - public DateRangeOptions CurrentPeriodStartRange { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf CurrentPeriodStart { get; set; } /// /// The ID of the customer whose subscriptions will be retrieved. diff --git a/src/Stripe.net/Services/Tokens/TokenCreateOptions.cs b/src/Stripe.net/Services/Tokens/TokenCreateOptions.cs index f02774d1e5..07b76aac81 100644 --- a/src/Stripe.net/Services/Tokens/TokenCreateOptions.cs +++ b/src/Stripe.net/Services/Tokens/TokenCreateOptions.cs @@ -1,22 +1,39 @@ namespace Stripe { using Newtonsoft.Json; + using Stripe.Infrastructure; public class TokenCreateOptions : BaseOptions { + /// + /// The customer (owned by the application's account) for which to create a token. For use + /// only with Stripe Connect. Also, this can + /// be used only with an OAuth + /// access token or + /// Stripe-Account header. For + /// more details, see Shared + /// Customers. + /// [JsonProperty("customer")] public string CustomerId { get; set; } + /// + /// The card this token will represent. If you also pass in a customer, the card must be the + /// ID of a card belonging to the customer. Otherwise, if you do not pass in a customer, + /// this is a instance containing a user's credit card + /// details. + /// [JsonProperty("card")] - public CreditCardOptions Card { get; set; } - - [JsonProperty("card")] - public string CardId { get; set; } - - [JsonProperty("bank_account")] - public BankAccountOptions BankAccount { get; set; } + public AnyOf Card { get; set; } + /// + /// The bank account this token will represent. If you also pass in a customer, the bank + /// account must be the ID of a bank account belonging to the customer. Otherwise, if you do + /// not pass in a customer, this is a instance containing a + /// user's bank account details. + /// [JsonProperty("bank_account")] - public string BankAccountId { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf BankAccount { get; set; } } } diff --git a/src/Stripe.net/Services/_base/AnyOf.cs b/src/Stripe.net/Services/_base/AnyOf.cs new file mode 100644 index 0000000000..03e979c0d5 --- /dev/null +++ b/src/Stripe.net/Services/_base/AnyOf.cs @@ -0,0 +1,227 @@ +namespace Stripe +{ + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + + /// + /// Abstract base class for AnyOf<> generic classes. + /// + public abstract class AnyOf : IAnyOf + { + /// Gets the current value. + /// The current value. + public abstract object GetValue(); + + public override bool Equals(object obj) => this.GetValue().Equals(obj); + + public override int GetHashCode() => this.GetValue().GetHashCode(); + + public override string ToString() => this.GetValue().ToString(); + } + + /// + /// is a generic class that can hold a value of one of two different + /// types. It uses implicit conversion operators to seamlessly accept or return either type. + /// This is used to represent polymorphic request parameters, i.e. parameters that can + /// be different types (typically a string or an options class). + /// + /// The first possible type of the value. + /// The second possible type of the value. + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Generic variant")] + public class AnyOf : AnyOf + { + private readonly T1 value1; + private readonly T2 value2; + + /// + /// Initializes a new instance of the class with type T1. + /// + /// The value to hold. + public AnyOf(T1 value) + { + this.value1 = value; + } + + /// + /// Initializes a new instance of the class with type T2. + /// + /// The value to hold. + public AnyOf(T2 value) + { + this.value2 = value; + } + + /// + /// Converts a value of type T1 to an object. + /// + /// The value to convert. + /// An object that holds the value. + public static implicit operator AnyOf(T1 value) => new AnyOf(value); + + /// + /// Converts a value of type T2 to an object. + /// + /// The value to convert. + /// An object that holds the value. + public static implicit operator AnyOf(T2 value) => new AnyOf(value); + + /// + /// Converts an object to a value of type T1, + /// + /// The object to convert. + /// + /// A value of type T1. If the object currently + /// holds a value of a different type, the default value for type T1 is returned. + /// + public static implicit operator T1(AnyOf anyOf) => anyOf.value1; + + /// + /// Converts a value of type T2 to an object. + /// + /// The object to convert. + /// + /// A value of type T2. If the object currently + /// holds a value of a different type, the default value for type T2 is returned. + /// + public static implicit operator T2(AnyOf anyOf) => anyOf.value2; + + /// Gets the current value. + /// The current value. + public override object GetValue() + { + if (!EqualityComparer.Default.Equals(this.value1, default(T1))) + { + return this.value1; + } + else if (!EqualityComparer.Default.Equals(this.value2, default(T2))) + { + return this.value2; + } + else + { + return null; + } + } + } + + /// + /// is a generic class that can hold a value of one of three + /// different types. It uses implicit conversion operators to seamlessly accept or return any + /// of the possible types. + /// This is used to represent polymorphic request parameters, i.e. parameters that can + /// be different types (typically a string or an options class). + /// + /// The first possible type of the value. + /// The second possible type of the value. + /// The third possible type of the value. + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "Generic variant")] + public class AnyOf : AnyOf + { + private readonly T1 value1; + private readonly T2 value2; + private readonly T3 value3; + + /// + /// Initializes a new instance of the class with type + /// T1. + /// + /// The value to hold. + public AnyOf(T1 value) + { + this.value1 = value; + } + + /// + /// Initializes a new instance of the class with type + /// T2. + /// + /// The value to hold. + public AnyOf(T2 value) + { + this.value2 = value; + } + + /// + /// Initializes a new instance of the class with type + /// T3. + /// + /// The value to hold. + public AnyOf(T3 value) + { + this.value3 = value; + } + + /// + /// Converts a value of type T1 to an object. + /// + /// The value to convert. + /// An object that holds the value. + public static implicit operator AnyOf(T1 value) => new AnyOf(value); + + /// + /// Converts a value of type T2 to an object. + /// + /// The value to convert. + /// An object that holds the value. + public static implicit operator AnyOf(T2 value) => new AnyOf(value); + + /// + /// Converts a value of type T3 to an object. + /// + /// The value to convert. + /// An object that holds the value. + public static implicit operator AnyOf(T3 value) => new AnyOf(value); + + /// + /// Converts an object to a value of type T1, + /// + /// The object to convert. + /// + /// A value of type T1. If the object currently + /// holds a value of a different type, the default value for type T3 is returned. + /// + public static implicit operator T1(AnyOf anyOf) => anyOf.value1; + + /// + /// Converts an object to a value of type T2, + /// + /// The object to convert. + /// + /// A value of type T2. If the object currently + /// holds a value of a different type, the default value for type T3 is returned. + /// + public static implicit operator T2(AnyOf anyOf) => anyOf.value2; + + /// + /// Converts an object to a value of type T3, + /// + /// The object to convert. + /// + /// A value of type T3. If the object currently + /// holds a value of a different type, the default value for type T3 is returned. + /// + public static implicit operator T3(AnyOf anyOf) => anyOf.value3; + + /// Gets the current value. + /// The current value. + public override object GetValue() + { + if (!EqualityComparer.Default.Equals(this.value1, default(T1))) + { + return this.value1; + } + else if (!EqualityComparer.Default.Equals(this.value2, default(T2))) + { + return this.value2; + } + else if (!EqualityComparer.Default.Equals(this.value3, default(T3))) + { + return this.value3; + } + else + { + return null; + } + } + } +} diff --git a/src/Stripe.net/Services/_base/ListOptionsWithCreated.cs b/src/Stripe.net/Services/_base/ListOptionsWithCreated.cs index 7cfd979cf0..0604c01dde 100644 --- a/src/Stripe.net/Services/_base/ListOptionsWithCreated.cs +++ b/src/Stripe.net/Services/_base/ListOptionsWithCreated.cs @@ -2,13 +2,16 @@ namespace Stripe { using System; using Newtonsoft.Json; + using Stripe.Infrastructure; public class ListOptionsWithCreated : ListOptions { + /// + /// A filter on the list based on the object created field. The value can be a + /// or a . + /// [JsonProperty("created")] - public DateTime? Created { get; set; } - - [JsonProperty("created")] - public DateRangeOptions CreatedRange { get; set; } + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf Created { get; set; } } } diff --git a/src/Stripe.net/Services/_interfaces/IAnyOf.cs b/src/Stripe.net/Services/_interfaces/IAnyOf.cs new file mode 100644 index 0000000000..dadeb473f3 --- /dev/null +++ b/src/Stripe.net/Services/_interfaces/IAnyOf.cs @@ -0,0 +1,12 @@ +namespace Stripe +{ + /// + /// Represents a value that may be of one of several different types. + /// + public interface IAnyOf + { + /// Gets the current value. + /// The current value. + object GetValue(); + } +} diff --git a/src/StripeTests/Infrastructure/FormEncoding/FormEncoderTest.cs b/src/StripeTests/Infrastructure/FormEncoding/FormEncoderTest.cs index 07f3b2dda9..99b2f5dc49 100644 --- a/src/StripeTests/Infrastructure/FormEncoding/FormEncoderTest.cs +++ b/src/StripeTests/Infrastructure/FormEncoding/FormEncoderTest.cs @@ -105,6 +105,24 @@ public void CreateQueryString() want = string.Empty }, + // AnyOf + new + { + data = new TestOptions + { + AnyOf = "foo", + }, + want = "any_of=foo" + }, + new + { + data = new TestOptions + { + AnyOf = new Dictionary { { "foo", "bar" } }, + }, + want = "any_of[foo]=bar" + }, + // Array new { diff --git a/src/StripeTests/Infrastructure/JsonConverters/AnyOfConverterTest.cs b/src/StripeTests/Infrastructure/JsonConverters/AnyOfConverterTest.cs new file mode 100644 index 0000000000..eed9fac7e1 --- /dev/null +++ b/src/StripeTests/Infrastructure/JsonConverters/AnyOfConverterTest.cs @@ -0,0 +1,110 @@ +namespace StripeTests +{ + using Newtonsoft.Json; + using Stripe; + using Stripe.Infrastructure; + using Xunit; + + public class AnyOfConverterTest : BaseStripeTest + { + [Fact] + public void DeserializeFirstType() + { + var json = "{\n \"any_of\": \"String!\"\n}"; + var obj = JsonConvert.DeserializeObject(json); + + Assert.NotNull(obj.AnyOf); + Assert.Equal("String!", obj.AnyOf); + } + + [Fact] + public void DeserializeSecondType() + { + var json = "{\n \"any_of\": {\n \"id\": \"id_123\",\n \"bar\": 42\n }\n}"; + var obj = JsonConvert.DeserializeObject(json); + + Assert.NotNull(obj.AnyOf); + Assert.Equal("id_123", ((TestSubObject)obj.AnyOf).Id); + Assert.Equal(42, ((TestSubObject)obj.AnyOf).Bar); + } + + [Fact] + public void DeserializeNull() + { + var json = "{\n \"any_of\": null\n}"; + var obj = JsonConvert.DeserializeObject(json); + + Assert.Null(obj.AnyOf); + } + + [Fact] + public void DeserializeUnexpectedType() + { + var json = "{\n \"any_of\": []\n}"; + + var exception = Assert.Throws(() => + JsonConvert.DeserializeObject(json)); + + Assert.Contains( + "Cannot deserialize the current JSON object into any of the expected types", + exception.Message); + } + + [Fact] + public void SerializeFirstType() + { + var obj = new TestObject + { + AnyOf = "String!", + }; + + var expected = "{\n \"any_of\": \"String!\"\n}"; + Assert.Equal(expected, obj.ToJson().Replace("\r\n", "\n")); + } + + [Fact] + public void SerializeSecondType() + { + var obj = new TestObject + { + AnyOf = new TestSubObject + { + Id = "id_123", + Bar = 42, + } + }; + + var expected = + "{\n \"any_of\": {\n \"id\": \"id_123\",\n \"bar\": 42\n }\n}"; + Assert.Equal(expected, obj.ToJson().Replace("\r\n", "\n")); + } + + [Fact] + public void SerializeNull() + { + var obj = new TestObject + { + AnyOf = null, + }; + + var expected = "{\n \"any_of\": null\n}"; + Assert.Equal(expected, obj.ToJson().Replace("\r\n", "\n")); + } + + private class TestSubObject : StripeEntity, IHasId + { + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("bar")] + public int Bar { get; set; } + } + + private class TestObject : StripeEntity + { + [JsonProperty("any_of")] + [JsonConverter(typeof(AnyOfConverter))] + internal AnyOf AnyOf { get; set; } + } + } +} diff --git a/src/StripeTests/Infrastructure/Public/StripeClientTest.cs b/src/StripeTests/Infrastructure/Public/StripeClientTest.cs index 316baa6d6a..c0107e1c8b 100644 --- a/src/StripeTests/Infrastructure/Public/StripeClientTest.cs +++ b/src/StripeTests/Infrastructure/Public/StripeClientTest.cs @@ -22,7 +22,7 @@ public StripeClientTest() { Amount = 123, Currency = "usd", - SourceId = "tok_visa", + Source = "tok_visa", }; this.requestOptions = new RequestOptions(); } diff --git a/src/StripeTests/Infrastructure/TestData/TestOptions.cs b/src/StripeTests/Infrastructure/TestData/TestOptions.cs index a442d88db2..74ac5f1a49 100644 --- a/src/StripeTests/Infrastructure/TestData/TestOptions.cs +++ b/src/StripeTests/Infrastructure/TestData/TestOptions.cs @@ -7,6 +7,7 @@ namespace StripeTests.Infrastructure.TestData using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Stripe; + using Stripe.Infrastructure; public class TestOptions : BaseOptions { @@ -20,6 +21,10 @@ public enum TestEnum TestTwo, } + [JsonProperty("any_of")] + [JsonConverter(typeof(AnyOfConverter))] + public AnyOf> AnyOf { get; set; } + [JsonProperty("array")] public string[] Array { get; set; } diff --git a/src/StripeTests/Services/Accounts/AccountServiceTest.cs b/src/StripeTests/Services/Accounts/AccountServiceTest.cs index 49b8dad630..3f944640e9 100644 --- a/src/StripeTests/Services/Accounts/AccountServiceTest.cs +++ b/src/StripeTests/Services/Accounts/AccountServiceTest.cs @@ -45,7 +45,7 @@ public AccountServiceTest(MockHttpClientFixture mockHttpClientFixture) }, Name = "Company name", }, - ExternalAccountId = "tok_visa_debit", + ExternalAccount = "tok_visa_debit", RequestedCapabilities = new List { "card_payments", diff --git a/src/StripeTests/Services/BankAccounts/BankAccountServiceTest.cs b/src/StripeTests/Services/BankAccounts/BankAccountServiceTest.cs index 6515e7edb0..7df23ff45b 100644 --- a/src/StripeTests/Services/BankAccounts/BankAccountServiceTest.cs +++ b/src/StripeTests/Services/BankAccounts/BankAccountServiceTest.cs @@ -26,7 +26,7 @@ public BankAccountServiceTest(MockHttpClientFixture mockHttpClientFixture) this.createOptions = new BankAccountCreateOptions { - SourceToken = "btok_123", + Source = "btok_123", }; this.updateOptions = new BankAccountUpdateOptions diff --git a/src/StripeTests/Services/Cards/CardServiceTest.cs b/src/StripeTests/Services/Cards/CardServiceTest.cs index 8de08abd5f..e8dad336e8 100644 --- a/src/StripeTests/Services/Cards/CardServiceTest.cs +++ b/src/StripeTests/Services/Cards/CardServiceTest.cs @@ -29,7 +29,7 @@ public CardServiceTest(MockHttpClientFixture mockHttpClientFixture) this.createOptions = new CardCreateOptions { - SourceToken = "tok_123", + Source = "tok_123", }; this.updateOptions = new CardUpdateOptions diff --git a/src/StripeTests/Services/Charges/ChargeServiceTest.cs b/src/StripeTests/Services/Charges/ChargeServiceTest.cs index 8dc21de671..3190dd4068 100644 --- a/src/StripeTests/Services/Charges/ChargeServiceTest.cs +++ b/src/StripeTests/Services/Charges/ChargeServiceTest.cs @@ -32,7 +32,7 @@ public ChargeServiceTest(MockHttpClientFixture mockHttpClientFixture) { Amount = 123, Currency = "usd", - SourceId = "tok_123", + Source = "tok_123", }; this.updateOptions = new ChargeUpdateOptions diff --git a/src/StripeTests/Services/Customers/CustomerServiceTest.cs b/src/StripeTests/Services/Customers/CustomerServiceTest.cs index 828541970c..3592f7ca66 100644 --- a/src/StripeTests/Services/Customers/CustomerServiceTest.cs +++ b/src/StripeTests/Services/Customers/CustomerServiceTest.cs @@ -25,7 +25,7 @@ public CustomerServiceTest(MockHttpClientFixture mockHttpClientFixture) this.createOptions = new CustomerCreateOptions { Email = "example@example.com", - SourceToken = "tok_123", + Source = "tok_123", }; this.updateOptions = new CustomerUpdateOptions diff --git a/src/StripeTests/Services/ExternalAccounts/ExternalAccountServiceTest.cs b/src/StripeTests/Services/ExternalAccounts/ExternalAccountServiceTest.cs index b6c7719526..e0a1957d36 100644 --- a/src/StripeTests/Services/ExternalAccounts/ExternalAccountServiceTest.cs +++ b/src/StripeTests/Services/ExternalAccounts/ExternalAccountServiceTest.cs @@ -25,7 +25,7 @@ public ExternalAccountServiceTest(MockHttpClientFixture mockHttpClientFixture) this.createOptions = new ExternalAccountCreateOptions { - ExternalAccountTokenId = "btok_123", + ExternalAccount = "btok_123", }; this.updateOptions = new ExternalAccountUpdateOptions diff --git a/src/StripeTests/Services/_base/ListOptionsWithCreatedTest.cs b/src/StripeTests/Services/_base/ListOptionsWithCreatedTest.cs new file mode 100644 index 0000000000..c083c89e21 --- /dev/null +++ b/src/StripeTests/Services/_base/ListOptionsWithCreatedTest.cs @@ -0,0 +1,60 @@ +namespace StripeTests +{ + using System; + using Stripe; + using Stripe.Infrastructure.FormEncoding; + using Xunit; + + public class ListOptionsWithCreatedTest : BaseStripeTest + { + [Fact] + public void SerializeNull() + { + var options = new ListOptionsWithCreated + { + Created = null, + }; + + Assert.Equal(string.Empty, FormEncoder.CreateQueryString(options)); + } + + [Fact] + public void SerializeDateTime() + { + var options = new ListOptionsWithCreated + { + Created = DateTime.Parse("Fri, 13 Feb 2009 23:31:30Z"), + }; + + Assert.Equal("created=1234567890", FormEncoder.CreateQueryString(options)); + } + + [Fact] + public void SerializeDateTimeNull() + { + var options = new ListOptionsWithCreated + { + Created = (DateTime?)null, + }; + + Assert.Equal("created=", FormEncoder.CreateQueryString(options)); + } + + [Fact] + public void SerializeDateRangeOptions() + { + var options = new ListOptionsWithCreated + { + Created = new DateRangeOptions + { + GreaterThanOrEqual = DateTime.Parse("Fri, 13 Feb 2009 23:31:30Z"), + LessThan = DateTime.Parse("Sun, 1 May 2044 01:28:21Z"), + }, + }; + + Assert.Equal( + "created[gte]=1234567890&created[lt]=2345678901", + FormEncoder.CreateQueryString(options)); + } + } +} diff --git a/src/StripeTests/Wholesome/NoDuplicateJsonPropertyValues.cs b/src/StripeTests/Wholesome/NoDuplicateJsonPropertyValues.cs new file mode 100644 index 0000000000..9e3f71891b --- /dev/null +++ b/src/StripeTests/Wholesome/NoDuplicateJsonPropertyValues.cs @@ -0,0 +1,60 @@ +#if NETCOREAPP +namespace StripeTests +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using Newtonsoft.Json; + using Stripe; + using Xunit; + + /// + /// This wholesome test ensures that no entity or options class reuses the same name in + /// different `JsonProperty` attributes. + /// + public class NoDuplicateJsonPropertyValues : WholesomeTest + { + private const string AssertionMessage = + "Found at least one duplicate JsonProperty name."; + + [Fact] + public void Check() + { + List results = new List(); + + // Get all classes that derive from StripeEntity or implement INestedOptions + var stripeClasses = GetSubclassesOf(typeof(StripeEntity)); + stripeClasses.AddRange(GetClassesWithInterface(typeof(INestedOptions))); + + foreach (Type stripeClass in stripeClasses) + { + var jsonPropertyNames = new List(); + + foreach (PropertyInfo property in stripeClass.GetProperties()) + { + var propType = property.PropertyType; + + // Skip properties that don't have a `JsonProperty` attribute + var attribute = property.GetCustomAttribute(); + if (attribute == null) + { + continue; + } + + if (jsonPropertyNames.Contains(attribute.PropertyName)) + { + results.Add($"{stripeClass.Name}.{property.Name}"); + } + else + { + jsonPropertyNames.Add(attribute.PropertyName); + } + } + } + + AssertEmpty(results, AssertionMessage); + } + } +} +#endif