From aa1ce55cc053022ee77d11c22613bafc9b032663 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 +- .../ExternalAccountCreateOptions.cs | 30 ++- .../Services/Sources/SourceCreateOptions.cs | 7 +- .../Services/Tokens/TokenCreateOptions.cs | 33 ++- src/Stripe.net/Services/_base/AnyOf.cs | 217 ++++++++++++++++++ .../Services/_base/ListOptionsWithCreated.cs | 11 +- src/Stripe.net/Services/_interfaces/IAnyOf.cs | 12 + .../FormEncoding/FormEncoderTest.cs | 18 ++ .../JsonConverters/AnyOfConverterTest.cs | 110 +++++++++ .../Infrastructure/TestData/TestOptions.cs | 5 + .../Services/Accounts/AccountServiceTest.cs | 2 +- .../ExternalAccountServiceTest.cs | 2 +- .../_base/ListOptionsWithCreatedTest.cs | 58 +++++ .../NoDuplicateJsonPropertyValues.cs | 61 +++++ 16 files changed, 665 insertions(+), 34 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 2926741b6c..e3608124a2 100644 --- a/src/Stripe.net/Infrastructure/FormEncoding/FormEncoder.cs +++ b/src/Stripe.net/Infrastructure/FormEncoding/FormEncoder.cs @@ -128,6 +128,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..c30fad7164 --- /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 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 a543557739..4ab2373191 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; @@ -34,14 +33,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("legal_entity")] public AccountLegalEntityOptions LegalEntity { 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/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/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..4c8d638a18 --- /dev/null +++ b/src/Stripe.net/Services/_base/AnyOf.cs @@ -0,0 +1,217 @@ +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; + } + } + } + + [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 a858adc51c..4bee8a96b2 100644 --- a/src/StripeTests/Infrastructure/FormEncoding/FormEncoderTest.cs +++ b/src/StripeTests/Infrastructure/FormEncoding/FormEncoderTest.cs @@ -23,6 +23,24 @@ public void EncodeOptions() 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/TestData/TestOptions.cs b/src/StripeTests/Infrastructure/TestData/TestOptions.cs index c6fef6626a..96bd57b24d 100644 --- a/src/StripeTests/Infrastructure/TestData/TestOptions.cs +++ b/src/StripeTests/Infrastructure/TestData/TestOptions.cs @@ -6,6 +6,7 @@ namespace StripeTests.Infrastructure.TestData using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Stripe; + using Stripe.Infrastructure; public class TestOptions : BaseOptions { @@ -19,6 +20,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 f962f69548..82e0b7d9ae 100644 --- a/src/StripeTests/Services/Accounts/AccountServiceTest.cs +++ b/src/StripeTests/Services/Accounts/AccountServiceTest.cs @@ -26,7 +26,7 @@ public AccountServiceTest(MockHttpClientFixture mockHttpClientFixture) this.createOptions = new AccountCreateOptions { Type = AccountType.Custom, - ExternalAccountId = "tok_visa_debit", + ExternalAccount = "tok_visa_debit", LegalEntity = new AccountLegalEntityOptions { AdditionalOwners = new List diff --git a/src/StripeTests/Services/ExternalAccounts/ExternalAccountServiceTest.cs b/src/StripeTests/Services/ExternalAccounts/ExternalAccountServiceTest.cs index c8948c2d72..87d5a52a72 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 { - ExternalAccountBankAccount = new AccountBankAccountOptions + ExternalAccount = new AccountBankAccountOptions { AccountNumber = "000123456789", Country = "US", diff --git a/src/StripeTests/Services/_base/ListOptionsWithCreatedTest.cs b/src/StripeTests/Services/_base/ListOptionsWithCreatedTest.cs new file mode 100644 index 0000000000..6d9779af05 --- /dev/null +++ b/src/StripeTests/Services/_base/ListOptionsWithCreatedTest.cs @@ -0,0 +1,58 @@ +namespace StripeTests +{ + using System; + using Stripe; + using Stripe.Infrastructure.Extensions; + using Xunit; + + public class ListOptionsWithCreatedTest : BaseStripeTest + { + [Fact] + public void SerializeNull() + { + var options = new ListOptionsWithCreated + { + Created = null, + }; + + Assert.Equal(string.Empty, options.ToQueryString()); + } + + [Fact] + public void SerializeDateTime() + { + var options = new ListOptionsWithCreated + { + Created = DateTime.Parse("Fri, 13 Feb 2009 23:31:30Z"), + }; + + Assert.Equal("created=1234567890", options.ToQueryString()); + } + + [Fact] + public void SerializeDateTimeNull() + { + var options = new ListOptionsWithCreated + { + Created = (DateTime?)null, + }; + + Assert.Equal("created=", options.ToQueryString()); + } + + [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", options.ToQueryString()); + } + } +} diff --git a/src/StripeTests/Wholesome/NoDuplicateJsonPropertyValues.cs b/src/StripeTests/Wholesome/NoDuplicateJsonPropertyValues.cs new file mode 100644 index 0000000000..73478457f6 --- /dev/null +++ b/src/StripeTests/Wholesome/NoDuplicateJsonPropertyValues.cs @@ -0,0 +1,61 @@ +#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.Concat(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; + } + + // Skip non-array types + if (jsonPropertyNames.Contains(attribute.PropertyName)) + { + results.Add($"{stripeClass.Name}.{property.Name}"); + } + else + { + jsonPropertyNames.Add(attribute.PropertyName); + } + } + } + + AssertEmpty(results, AssertionMessage); + } + } +} +#endif