diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..2307ba4 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/AltaSoft.Choice.Generator/Executor.cs b/src/AltaSoft.Choice.Generator/Executor.cs index 924f829..985bab1 100644 --- a/src/AltaSoft.Choice.Generator/Executor.cs +++ b/src/AltaSoft.Choice.Generator/Executor.cs @@ -44,7 +44,16 @@ internal static void Execute(in ImmutableArray typesToGenerat DeclaredAccessibility: Accessibility.Public }).ToList(); - var sb = Process(typeSymbol, partialProperties); + // Get ordinary (non-partial) required properties + var ordinaryRequiredProperties = typeSymbol.GetMembersOfType().Where(x + => x is + { + IsStatic: false, IsWriteOnly: false, CanBeReferencedByName: true, IsPartialDefinition: false, + DeclaredAccessibility: Accessibility.Public, + IsRequired: true + }).ToList(); + + var sb = Process(typeSymbol, partialProperties, ordinaryRequiredProperties); context.AddSource($"{typeSymbol.Name}.g.cs", sb.ToString()); } @@ -55,7 +64,7 @@ internal static void Execute(in ImmutableArray typesToGenerat } } - private static SourceCodeBuilder Process(INamedTypeSymbol typeSymbol, List properties) + private static SourceCodeBuilder Process(INamedTypeSymbol typeSymbol, List properties, List ordinaryRequiredProperties) { var processedProperties = properties.ConvertAll(ProcessProperty); var usingStatements = processedProperties.Select(x => x.Namespace).Concat(s_baseNamespaces); @@ -67,6 +76,10 @@ private static SourceCodeBuilder Process(INamedTypeSymbol typeSymbol, List instance and sets its value using the specified {prop.TypeSymbol.GetCrefForType()}."); - sb.AppendParamDescription("value", "The value to assign to the created choice instance."); - sb.Append($"public static {typeFullName} CreateAs").Append(prop.Name).Append("(") - .Append(prop.TypeName).Append(" value) => new () { ").Append(prop.Name).AppendLine(" = value };"); + var requiredParamNames = new HashSet( + ordinaryRequiredProperties.Select(x => x.Name.ToCamelCase()), + StringComparer.Ordinal); + + var choiceParamName = "value"; + if (requiredParamNames.Contains(choiceParamName)) + { + choiceParamName = "choiceValue"; + var suffix = 1; + while (requiredParamNames.Contains(choiceParamName)) + { + choiceParamName = $"choiceValue{suffix}"; + suffix++; + } + } + + // Add parameter descriptions for required ordinary properties + foreach (var reqProp in ordinaryRequiredProperties) + { + var paramName = reqProp.Name.ToCamelCase(); + sb.AppendParamDescription(paramName, $"The value for the required property {reqProp.Name}."); + } + + sb.AppendParamDescription(choiceParamName, "The value to assign to the created choice instance."); + + // Build method signature with required property parameters + sb.Append($"public static {typeFullName} CreateAs").Append(prop.Name).Append("("); + + // Add required ordinary properties as parameters first + for (var i = 0; i < ordinaryRequiredProperties.Count; i++) + { + var reqProp = ordinaryRequiredProperties[i]; + var paramName = reqProp.Name.ToCamelCase(); + var propType = reqProp.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + sb.Append(propType).Append(" ").Append(paramName).Append(", "); + } + + // Add the choice property parameter + sb.Append(prop.TypeName).Append(" ").Append(choiceParamName).Append(") => new () { "); + + // Initialize required ordinary properties + foreach (var reqProp in ordinaryRequiredProperties) + { + var paramName = reqProp.Name.ToCamelCase(); + sb.Append(reqProp.Name).Append(" = ").Append(paramName).Append(", "); + } + + // Initialize the choice property + sb.Append(prop.Name).Append(" = ").Append(choiceParamName).AppendLine(" };"); sb.NewLine(); } @@ -191,8 +251,17 @@ private static SourceCodeBuilder Process(INamedTypeSymbol typeSymbol, List /// Returns the C# keyword representation of a property's level, /// optionally omitting the keyword for public if is true. diff --git a/tests/AltaSoft.Choice.Generator.SnapshotTests/ChoiceGeneratorTest.cs b/tests/AltaSoft.Choice.Generator.SnapshotTests/ChoiceGeneratorTest.cs index 099ef2e..7052900 100644 --- a/tests/AltaSoft.Choice.Generator.SnapshotTests/ChoiceGeneratorTest.cs +++ b/tests/AltaSoft.Choice.Generator.SnapshotTests/ChoiceGeneratorTest.cs @@ -226,6 +226,112 @@ public sealed partial class MixedAttributeChoice }); } + [Fact] + public Task ChoiceTypeShouldGenerateWithRequiredOrdinaryProperties() + { + const string source = + """ + using System; + using AltaSoft.Choice; + + namespace TestNamespace + { + [Choice] + public sealed partial class OrderChoice + { + /// + /// Required ordinary property - Order ID + /// + public required string OrderId { get; set; } + + /// + /// Required ordinary property - Customer Name + /// + public required string CustomerName { get; set; } + + /// + /// Choice property - Express delivery + /// + public partial ExpressDelivery? Express { get; set; } + + /// + /// Choice property - Standard delivery + /// + public partial StandardDelivery? Standard { get; set; } + } + + public sealed class ExpressDelivery + { + public DateTime DeliveryDate { get; set; } + public decimal SurchargeAmount { get; set; } + } + + public sealed class StandardDelivery + { + public int DeliveryDays { get; set; } + public decimal ShippingCost { get; set; } + } + } + """; + + return TestHelper.Verify(source, (_, x, _) => + { + Assert.Single(x); + }); + } + + [Fact] + public Task ChoiceTypeShouldGenerateWithRequiredPropertyNamedValue() + { + const string source = + """ + using System; + using AltaSoft.Choice; + + namespace TestNamespace + { + [Choice] + public sealed partial class ConfigChoice + { + /// + /// Required property named "Value" - tests parameter conflict resolution + /// + public required string Value { get; set; } + + /// + /// Required property named "ChoiceValue" - tests fallback parameter conflict resolution + /// + public required string ChoiceValue { get; set; } + + /// + /// Choice property - Option A + /// + public partial OptionA? OptionA { get; set; } + + /// + /// Choice property - Option B + /// + public partial OptionB? OptionB { get; set; } + } + + public sealed class OptionA + { + public string Data { get; set; } + } + + public sealed class OptionB + { + public int Count { get; set; } + } + } + """; + + return TestHelper.Verify(source, (_, x, _) => + { + Assert.Single(x); + }); + } + public static class TestHelper { internal static Task Verify(string source, Action, List, GeneratorDriver>? additionalChecks = null) diff --git a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateAllMethodsAndCompileCorrectly#Authorisation1Choice.g.verified.cs b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateAllMethodsAndCompileCorrectly#Authorisation1Choice.g.verified.cs index 0b26c26..a09218c 100644 --- a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateAllMethodsAndCompileCorrectly#Authorisation1Choice.g.verified.cs +++ b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateAllMethodsAndCompileCorrectly#Authorisation1Choice.g.verified.cs @@ -24,6 +24,10 @@ namespace TestNamespace; #pragma warning disable CS8774 // Member must have a non-null value when exiting. #pragma warning disable CS0628 // New protected member declared in sealed type +#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable IDE0290 // Use primary constructor public sealed partial class Authorisation1Choice { diff --git a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateDocumentationCorrectly_ForArrayInChoice#ArrayInTypeChoice.g.verified.cs b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateDocumentationCorrectly_ForArrayInChoice#ArrayInTypeChoice.g.verified.cs index 909e2d9..660f141 100644 --- a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateDocumentationCorrectly_ForArrayInChoice#ArrayInTypeChoice.g.verified.cs +++ b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateDocumentationCorrectly_ForArrayInChoice#ArrayInTypeChoice.g.verified.cs @@ -24,6 +24,10 @@ namespace TestNamespace; #pragma warning disable CS8774 // Member must have a non-null value when exiting. #pragma warning disable CS0628 // New protected member declared in sealed type +#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable IDE0290 // Use primary constructor public sealed partial class ArrayInTypeChoice { diff --git a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithMixedXmlTagAndXmlElement#MixedAttributeChoice.g.verified.cs b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithMixedXmlTagAndXmlElement#MixedAttributeChoice.g.verified.cs index 8574cc6..98a7ce1 100644 --- a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithMixedXmlTagAndXmlElement#MixedAttributeChoice.g.verified.cs +++ b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithMixedXmlTagAndXmlElement#MixedAttributeChoice.g.verified.cs @@ -24,6 +24,10 @@ namespace TestNamespace; #pragma warning disable CS8774 // Member must have a non-null value when exiting. #pragma warning disable CS0628 // New protected member declared in sealed type +#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable IDE0290 // Use primary constructor public sealed partial class MixedAttributeChoice { diff --git a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithRequiredOrdinaryProperties#OrderChoice.g.verified.cs b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithRequiredOrdinaryProperties#OrderChoice.g.verified.cs new file mode 100644 index 0000000..8d8a29c --- /dev/null +++ b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithRequiredOrdinaryProperties#OrderChoice.g.verified.cs @@ -0,0 +1,181 @@ +//HintName: OrderChoice.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by 'AltaSoft Choice.Generator'. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using TestNamespace; +using AltaSoft.Choice; +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Xml; +using System.Xml.Serialization; +using System.Xml.Schema; + +namespace TestNamespace; + +#pragma warning disable CS8774 // Member must have a non-null value when exiting. +#pragma warning disable CS0628 // New protected member declared in sealed type +#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable IDE0290 // Use primary constructor + +public sealed partial class OrderChoice +{ + /// + /// Constructor for Serialization/Deserialization + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public OrderChoice() + { + } + + /// + /// Choice enum + /// + [JsonIgnore] + [XmlIgnore] + [ChoiceTypeProperty] + public ChoiceOf ChoiceType { get; private set; } + + private TestNamespace.ExpressDelivery? _express; + + /// + /// Choice property - Express delivery + /// + [DisallowNull] + [XmlElement("Express")] + [ChoiceProperty] + public partial TestNamespace.ExpressDelivery? Express + { + get => _express; + set + { + _express = value ?? throw new InvalidOperationException("Choice value cannot be null"); + _standard = null; + ChoiceType = ChoiceOf.Express; + } + } + + private TestNamespace.StandardDelivery? _standard; + + /// + /// Choice property - Standard delivery + /// + [DisallowNull] + [XmlElement("Standard")] + [ChoiceProperty] + public partial TestNamespace.StandardDelivery? Standard + { + get => _standard; + set + { + _standard = value ?? throw new InvalidOperationException("Choice value cannot be null"); + _express = null; + ChoiceType = ChoiceOf.Standard; + } + } + + + /// + /// Creates a new instance and sets its value using the specified . + /// + /// The value for the required property OrderId. + /// The value for the required property CustomerName. + /// The value to assign to the created choice instance. + public static TestNamespace.OrderChoice CreateAsExpress(string orderId, string customerName, TestNamespace.ExpressDelivery value) => new () { OrderId = orderId, CustomerName = customerName, Express = value }; + + /// + /// Creates a new instance and sets its value using the specified . + /// + /// The value for the required property OrderId. + /// The value for the required property CustomerName. + /// The value to assign to the created choice instance. + public static TestNamespace.OrderChoice CreateAsStandard(string orderId, string customerName, TestNamespace.StandardDelivery value) => new () { OrderId = orderId, CustomerName = customerName, Standard = value }; + + /// + /// Applies the appropriate function based on the current choice type + /// + /// The return type of the provided match functions + /// Function to invoke if the choice is a value + /// Function to invoke if the choice is a value + public TResult Match( + Func matchExpress, + Func matchStandard) + { + return ChoiceType switch + { + ChoiceOf.Express => matchExpress(Express!), + ChoiceOf.Standard => matchStandard(Standard!), + _ => throw new InvalidOperationException($"Invalid ChoiceType. '{ChoiceType}'") + }; + } + + /// + /// Applies the appropriate Action based on the current choice type + /// + /// Action to invoke if the choice is a value + /// Action to invoke if the choice is a value + public void Switch( + Action matchExpress, + Action matchStandard) + { + switch (ChoiceType) + { + case ChoiceOf.Express: + matchExpress(Express!); + return; + + case ChoiceOf.Standard: + matchStandard(Standard!); + return; + + default: + throw new XmlException($"Invalid ChoiceType. '{ChoiceType}'"); + } + } + + + /// + /// Determines whether the property should be serialized. + /// + /// + /// true if is the active choice; otherwise, false. + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeExpress() => ChoiceType == ChoiceOf.Express; + + /// + /// Determines whether the property should be serialized. + /// + /// + /// true if is the active choice; otherwise, false. + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeStandard() => ChoiceType == ChoiceOf.Standard; + + /// + /// Choice enumeration + /// + [XmlType("ChoiceOf.OrderChoice")] + public enum ChoiceOf + { + /// + /// Choice property - Express delivery + /// + Express, + /// + /// Choice property - Standard delivery + /// + Standard, + } +} diff --git a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithRequiredPropertyNamedValue#ConfigChoice.g.verified.cs b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithRequiredPropertyNamedValue#ConfigChoice.g.verified.cs new file mode 100644 index 0000000..450cda4 --- /dev/null +++ b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithRequiredPropertyNamedValue#ConfigChoice.g.verified.cs @@ -0,0 +1,181 @@ +//HintName: ConfigChoice.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by 'AltaSoft Choice.Generator'. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +using TestNamespace; +using AltaSoft.Choice; +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Xml; +using System.Xml.Serialization; +using System.Xml.Schema; + +namespace TestNamespace; + +#pragma warning disable CS8774 // Member must have a non-null value when exiting. +#pragma warning disable CS0628 // New protected member declared in sealed type +#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable IDE0290 // Use primary constructor + +public sealed partial class ConfigChoice +{ + /// + /// Constructor for Serialization/Deserialization + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public ConfigChoice() + { + } + + /// + /// Choice enum + /// + [JsonIgnore] + [XmlIgnore] + [ChoiceTypeProperty] + public ChoiceOf ChoiceType { get; private set; } + + private TestNamespace.OptionA? _optionA; + + /// + /// Choice property - Option A + /// + [DisallowNull] + [XmlElement("OptionA")] + [ChoiceProperty] + public partial TestNamespace.OptionA? OptionA + { + get => _optionA; + set + { + _optionA = value ?? throw new InvalidOperationException("Choice value cannot be null"); + _optionB = null; + ChoiceType = ChoiceOf.OptionA; + } + } + + private TestNamespace.OptionB? _optionB; + + /// + /// Choice property - Option B + /// + [DisallowNull] + [XmlElement("OptionB")] + [ChoiceProperty] + public partial TestNamespace.OptionB? OptionB + { + get => _optionB; + set + { + _optionB = value ?? throw new InvalidOperationException("Choice value cannot be null"); + _optionA = null; + ChoiceType = ChoiceOf.OptionB; + } + } + + + /// + /// Creates a new instance and sets its value using the specified . + /// + /// The value for the required property Value. + /// The value for the required property ChoiceValue. + /// The value to assign to the created choice instance. + public static TestNamespace.ConfigChoice CreateAsOptionA(string value, string choiceValue, TestNamespace.OptionA choiceValue1) => new () { Value = value, ChoiceValue = choiceValue, OptionA = choiceValue1 }; + + /// + /// Creates a new instance and sets its value using the specified . + /// + /// The value for the required property Value. + /// The value for the required property ChoiceValue. + /// The value to assign to the created choice instance. + public static TestNamespace.ConfigChoice CreateAsOptionB(string value, string choiceValue, TestNamespace.OptionB choiceValue1) => new () { Value = value, ChoiceValue = choiceValue, OptionB = choiceValue1 }; + + /// + /// Applies the appropriate function based on the current choice type + /// + /// The return type of the provided match functions + /// Function to invoke if the choice is a value + /// Function to invoke if the choice is a value + public TResult Match( + Func matchOptionA, + Func matchOptionB) + { + return ChoiceType switch + { + ChoiceOf.OptionA => matchOptionA(OptionA!), + ChoiceOf.OptionB => matchOptionB(OptionB!), + _ => throw new InvalidOperationException($"Invalid ChoiceType. '{ChoiceType}'") + }; + } + + /// + /// Applies the appropriate Action based on the current choice type + /// + /// Action to invoke if the choice is a value + /// Action to invoke if the choice is a value + public void Switch( + Action matchOptionA, + Action matchOptionB) + { + switch (ChoiceType) + { + case ChoiceOf.OptionA: + matchOptionA(OptionA!); + return; + + case ChoiceOf.OptionB: + matchOptionB(OptionB!); + return; + + default: + throw new XmlException($"Invalid ChoiceType. '{ChoiceType}'"); + } + } + + + /// + /// Determines whether the property should be serialized. + /// + /// + /// true if is the active choice; otherwise, false. + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeOptionA() => ChoiceType == ChoiceOf.OptionA; + + /// + /// Determines whether the property should be serialized. + /// + /// + /// true if is the active choice; otherwise, false. + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeOptionB() => ChoiceType == ChoiceOf.OptionB; + + /// + /// Choice enumeration + /// + [XmlType("ChoiceOf.ConfigChoice")] + public enum ChoiceOf + { + /// + /// Choice property - Option A + /// + OptionA, + /// + /// Choice property - Option B + /// + OptionB, + } +} diff --git a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithXmlTagNamespace#XmlNamespaceChoice.g.verified.cs b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithXmlTagNamespace#XmlNamespaceChoice.g.verified.cs index 268ab64..1f3017f 100644 --- a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithXmlTagNamespace#XmlNamespaceChoice.g.verified.cs +++ b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithXmlTagNamespace#XmlNamespaceChoice.g.verified.cs @@ -24,6 +24,10 @@ namespace TestNamespace; #pragma warning disable CS8774 // Member must have a non-null value when exiting. #pragma warning disable CS0628 // New protected member declared in sealed type +#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable IDE0290 // Use primary constructor public sealed partial class XmlNamespaceChoice { diff --git a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldNotGenerateImplicitMethodsAndCompileCorrectly#Authorisation1Choice.g.verified.cs b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldNotGenerateImplicitMethodsAndCompileCorrectly#Authorisation1Choice.g.verified.cs index 35cb294..f4c2e9d 100644 --- a/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldNotGenerateImplicitMethodsAndCompileCorrectly#Authorisation1Choice.g.verified.cs +++ b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldNotGenerateImplicitMethodsAndCompileCorrectly#Authorisation1Choice.g.verified.cs @@ -24,6 +24,10 @@ namespace TestNamespace; #pragma warning disable CS8774 // Member must have a non-null value when exiting. #pragma warning disable CS0628 // New protected member declared in sealed type +#pragma warning disable CS0618 // Type or member is obsolete +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#pragma warning disable IDE0290 // Use primary constructor public sealed partial class Authorisation1Choice { diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceSerializationJsonTests.cs b/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceSerializationJsonTests.cs new file mode 100644 index 0000000..ae0c632 --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceSerializationJsonTests.cs @@ -0,0 +1,292 @@ +using System; +using AltaSoft.ChoiceGenerator.Tests.TestHelpers; +using AltaSoft.ChoiceGenerator.Tests.TestModels; +using Xunit; + +namespace AltaSoft.ChoiceGenerator.Tests; + +/// +/// Tests for JSON serialization and deserialization of choice types +/// +public class ChoiceSerializationJsonTests +{ + #region Serialization Tests + + [Fact] + public void JsonSerialization_PaymentMethodWithCreditCard_ShouldProduceCorrectJson() + { + // Arrange + var cardPayment = new CreditCardPayment("4111111111111111", "John Doe", "12/25", "123"); + var paymentMethod = PaymentMethod.CreateAsCreditCard(cardPayment); + + // Act + var json = JsonSerializationHelper.SerializeToJson(paymentMethod); + + // Assert + Assert.Contains("\"creditCard\"", json); + Assert.Contains("\"CardNumber\": \"4111111111111111\"", json); + Assert.Contains("\"CardHolderName\": \"John Doe\"", json); + Assert.DoesNotContain("bankTransfer", json); + Assert.DoesNotContain("payPal", json); + } + + [Fact] + public void JsonSerialization_PaymentMethodWithBankTransfer_ShouldProduceCorrectJson() + { + // Arrange + var bankTransfer = new BankTransferPayment("123456789", "987654321", "Bank of America"); + var paymentMethod = PaymentMethod.CreateAsBankTransfer(bankTransfer); + + // Act + var json = JsonSerializationHelper.SerializeToJson(paymentMethod); + + // Assert + Assert.Contains("\"bankTransfer\"", json); + Assert.Contains("\"AccountNumber\": \"123456789\"", json); + Assert.Contains("\"BankName\": \"Bank of America\"", json); + Assert.DoesNotContain("creditCard", json); + Assert.DoesNotContain("payPal", json); + } + + [Fact] + public void JsonSerialization_ShippingOption_ShouldSerializeCorrectly() + { + // Arrange + var expressShipping = new ShippingDetails(15.99m, 2, "FedEx"); + var shippingOption = ShippingOption.CreateAsExpress(expressShipping); + + // Act + var json = JsonSerializationHelper.SerializeToJson(shippingOption); + + // Assert + Assert.Contains("\"express\"", json); + Assert.Contains("\"Cost\": 15.99", json); + Assert.Contains("\"EstimatedDays\": 2", json); + Assert.Contains("\"Carrier\": \"FedEx\"", json); + } + + [Fact] + public void JsonSerialization_SearchCriteriaWithKeyword_ShouldSerializeCorrectly() + { + // Arrange + var searchCriteria = SearchCriteria.CreateAsKeyword("laptop"); + + // Act + var json = JsonSerializationHelper.SerializeToJson(searchCriteria); + + // Assert + Assert.Contains("\"keyword\": \"laptop\"", json); + Assert.DoesNotContain("categoryId", json); + Assert.DoesNotContain("dateRange", json); + Assert.DoesNotContain("priceRange", json); + } + + [Fact] + public void JsonSerialization_NotificationChannel_ShouldSerializeEnum() + { + // Arrange + var channel = NotificationChannel.CreateAsChannel(NotificationChannelType.Email); + + // Act + var json = JsonSerializationHelper.SerializeToJson(channel); + + // Assert + Assert.Contains("\"channel\": \"Email\"", json); + } + + #endregion + + #region Deserialization Tests + + [Fact] + public void JsonDeserialization_CreditCardPayment_ShouldDeserializeCorrectly() + { + // Arrange + const string json = """ + { + "creditCard": { + "CardNumber": "4111111111111111", + "CardHolderName": "Jane Smith", + "ExpiryDate": "06/26", + "Cvv": "456" + } + } + """; + + // Act + var paymentMethod = JsonSerializationHelper.DeserializeFromJson(json); + + // Assert + Assert.NotNull(paymentMethod); + Assert.Equal(PaymentMethod.ChoiceOf.CreditCard, paymentMethod.ChoiceType); + Assert.NotNull(paymentMethod.CreditCard); + Assert.Equal("4111111111111111", paymentMethod.CreditCard.CardNumber); + Assert.Equal("Jane Smith", paymentMethod.CreditCard.CardHolderName); + Assert.Null(paymentMethod.BankTransfer); + Assert.Null(paymentMethod.PayPal); + } + + [Fact] + public void JsonDeserialization_PayPalPayment_ShouldDeserializeCorrectly() + { + // Arrange + const string json = """ + { + "payPal": { + "Email": "test@example.com", + "TransactionId": "TXN-12345" + } + } + """; + + // Act + var paymentMethod = JsonSerializationHelper.DeserializeFromJson(json); + + // Assert + Assert.NotNull(paymentMethod); + Assert.Equal(PaymentMethod.ChoiceOf.PayPal, paymentMethod.ChoiceType); + Assert.NotNull(paymentMethod.PayPal); + Assert.Equal("test@example.com", paymentMethod.PayPal.Email); + Assert.Equal("TXN-12345", paymentMethod.PayPal.TransactionId); + } + + [Fact] + public void JsonDeserialization_ShippingOption_ShouldDeserializeCorrectly() + { + // Arrange + const string json = """ + { + "overnight": { + "Cost": 29.99, + "EstimatedDays": 1, + "Carrier": "DHL" + } + } + """; + + // Act + var shippingOption = JsonSerializationHelper.DeserializeFromJson(json); + + // Assert + Assert.NotNull(shippingOption); + Assert.Equal(ShippingOption.ChoiceOf.Overnight, shippingOption.ChoiceType); + Assert.NotNull(shippingOption.Overnight); + Assert.Equal(29.99m, shippingOption.Overnight.Cost); + Assert.Equal(1, shippingOption.Overnight.EstimatedDays); + } + + [Fact] + public void JsonDeserialization_SearchCriteriaWithCategoryId_ShouldDeserializeCorrectly() + { + // Arrange + const string json = """ + { + "categoryId": 42 + } + """; + + // Act + var searchCriteria = JsonSerializationHelper.DeserializeFromJson(json); + + // Assert + Assert.NotNull(searchCriteria); + Assert.Equal(SearchCriteria.ChoiceOf.CategoryId, searchCriteria.ChoiceType); + Assert.Equal(42, searchCriteria.CategoryId); + } + + [Fact] + public void JsonDeserialization_NotificationChannel_ShouldDeserializeEnum() + { + // Arrange + const string json = """ + { + "channel": "SMS" + } + """; + + // Act + var channel = JsonSerializationHelper.DeserializeFromJson(json); + + // Assert + Assert.NotNull(channel); + Assert.Equal(NotificationChannelType.SMS, channel.Channel); + } + + #endregion + + #region Round-Trip Tests + + [Fact] + public void JsonRoundTrip_PaymentMethod_ShouldPreserveData() + { + // Arrange + var original = PaymentMethod.CreateAsCreditCard( + new CreditCardPayment("4111111111111111", "Alice Johnson", "12/25", "123") + ); + + // Act + var restored = JsonSerializationHelper.RoundTrip(original); + + // Assert + Assert.NotNull(restored); + Assert.Equal(PaymentMethod.ChoiceOf.CreditCard, restored.ChoiceType); + Assert.Equal("Alice Johnson", restored.CreditCard?.CardHolderName); + Assert.Equal("4111111111111111", restored.CreditCard?.CardNumber); + } + + [Fact] + public void JsonRoundTrip_ShippingOption_ShouldPreserveData() + { + // Arrange + var original = ShippingOption.CreateAsStandard( + new ShippingDetails(5.99m, 7, "USPS") + ); + + // Act + var restored = JsonSerializationHelper.RoundTrip(original); + + // Assert + Assert.NotNull(restored); + Assert.Equal(ShippingOption.ChoiceOf.Standard, restored.ChoiceType); + Assert.Equal(5.99m, restored.Standard?.Cost); + Assert.Equal(7, restored.Standard?.EstimatedDays); + } + + [Fact] + public void JsonRoundTrip_SearchCriteria_WithPriceRange_ShouldPreserveStruct() + { + // Arrange + var original = SearchCriteria.CreateAsPriceRange(new PriceRange(100m, 500m)); + + // Act + var restored = JsonSerializationHelper.RoundTrip(original); + + // Assert + Assert.NotNull(restored); + Assert.Equal(SearchCriteria.ChoiceOf.PriceRange, restored.ChoiceType); + Assert.NotNull(restored.PriceRange); + Assert.Equal(100m, restored.PriceRange.Value.MinPrice); + Assert.Equal(500m, restored.PriceRange.Value.MaxPrice); + } + + [Fact] + public void JsonRoundTrip_SearchCriteria_WithDateRange_ShouldPreserveDates() + { + // Arrange + var startDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var endDate = new DateTime(2024, 12, 31, 23, 59, 59, DateTimeKind.Utc); + var original = SearchCriteria.CreateAsDateRange(new DateRange(startDate, endDate)); + + // Act + var restored = JsonSerializationHelper.RoundTrip(original); + + // Assert + Assert.NotNull(restored); + Assert.Equal(SearchCriteria.ChoiceOf.DateRange, restored.ChoiceType); + Assert.NotNull(restored.DateRange); + Assert.Equal(startDate, restored.DateRange.StartDate); + Assert.Equal(endDate, restored.DateRange.EndDate); + } + + #endregion +} diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceSerializationXmlTests.cs b/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceSerializationXmlTests.cs new file mode 100644 index 0000000..e2a8556 --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceSerializationXmlTests.cs @@ -0,0 +1,332 @@ +using System; +using AltaSoft.ChoiceGenerator.Tests.TestHelpers; +using AltaSoft.ChoiceGenerator.Tests.TestModels; +using Xunit; + +namespace AltaSoft.ChoiceGenerator.Tests; + +/// +/// Tests for XML serialization and deserialization of choice types +/// +public class ChoiceSerializationXmlTests +{ + #region Serialization Tests + + [Fact] + public void XmlSerialization_PaymentMethodWithCreditCard_ShouldProduceCorrectXml() + { + // Arrange + var cardPayment = new CreditCardPayment("4111111111111111", "John Doe", "12/25", "123"); + var paymentMethod = PaymentMethod.CreateAsCreditCard(cardPayment); + + // Act + var xml = XmlSerializationHelper.SerializeToXml(paymentMethod); + + // Assert + Assert.Contains("", xml); + Assert.Contains("4111111111111111", xml); + Assert.Contains("John Doe", xml); + Assert.DoesNotContain("", xml); + Assert.Contains("123456789", xml); + Assert.Contains("Bank of America", xml); + Assert.DoesNotContain("", xml); + Assert.Contains("15.99", xml); + Assert.Contains("2", xml); + Assert.Contains("FedEx", xml); + Assert.DoesNotContain("laptop", xml); + Assert.DoesNotContain("Push", xml); + } + + [Fact] + public void XmlSerialization_ShouldNotIncludeInactiveChoices() + { + // Arrange + var payPal = new PayPalPayment("test@example.com", "TXN-999"); + var paymentMethod = PaymentMethod.CreateAsPayPal(payPal); + + // Act + var xml = XmlSerializationHelper.SerializeToXml(paymentMethod); + + // Assert + Assert.Contains("", xml); + // Inactive choices should not be serialized (no xsi:nil elements) + Assert.DoesNotContain("nil=", xml); + Assert.DoesNotContain(" + + 4111111111111111 + Jane Smith + 06/26 + 456 + + + """; + + // Act + var paymentMethod = XmlSerializationHelper.DeserializeFromXml(xml); + + // Assert + Assert.NotNull(paymentMethod); + Assert.Equal(PaymentMethod.ChoiceOf.CreditCard, paymentMethod.ChoiceType); + Assert.NotNull(paymentMethod.CreditCard); + Assert.Equal("4111111111111111", paymentMethod.CreditCard.CardNumber); + Assert.Equal("Jane Smith", paymentMethod.CreditCard.CardHolderName); + Assert.Null(paymentMethod.BankTransfer); + Assert.Null(paymentMethod.PayPal); + } + + [Fact] + public void XmlDeserialization_PayPalPayment_ShouldDeserializeCorrectly() + { + // Arrange + const string xml = """ + + + user@paypal.com + TXN-ABC123 + + + """; + + // Act + var paymentMethod = XmlSerializationHelper.DeserializeFromXml(xml); + + // Assert + Assert.NotNull(paymentMethod); + Assert.Equal(PaymentMethod.ChoiceOf.PayPal, paymentMethod.ChoiceType); + Assert.NotNull(paymentMethod.PayPal); + Assert.Equal("user@paypal.com", paymentMethod.PayPal.Email); + Assert.Equal("TXN-ABC123", paymentMethod.PayPal.TransactionId); + } + + [Fact] + public void XmlDeserialization_ShippingOption_ShouldDeserializeCorrectly() + { + // Arrange + const string xml = """ + + + 29.99 + 1 + DHL + + + """; + + // Act + var shippingOption = XmlSerializationHelper.DeserializeFromXml(xml); + + // Assert + Assert.NotNull(shippingOption); + Assert.Equal(ShippingOption.ChoiceOf.Overnight, shippingOption.ChoiceType); + Assert.NotNull(shippingOption.Overnight); + Assert.Equal(29.99m, shippingOption.Overnight.Cost); + Assert.Equal(1, shippingOption.Overnight.EstimatedDays); + Assert.Equal("DHL", shippingOption.Overnight.Carrier); + } + + [Fact] + public void XmlDeserialization_SearchCriteriaWithCategoryId_ShouldDeserializeCorrectly() + { + // Arrange + const string xml = """ + + 42 + + """; + + // Act + var searchCriteria = XmlSerializationHelper.DeserializeFromXml(xml); + + // Assert + Assert.NotNull(searchCriteria); + Assert.Equal(SearchCriteria.ChoiceOf.CategoryId, searchCriteria.ChoiceType); + Assert.Equal(42, searchCriteria.CategoryId); + } + + [Fact] + public void XmlDeserialization_NotificationChannel_ShouldDeserializeEnum() + { + // Arrange + const string xml = """ + + Email + + """; + + // Act + var channel = XmlSerializationHelper.DeserializeFromXml(xml); + + // Assert + Assert.NotNull(channel); + Assert.Equal(NotificationChannelType.Email, channel.Channel); + } + + #endregion + + #region Round-Trip Tests + + [Fact] + public void XmlRoundTrip_PaymentMethod_ShouldPreserveData() + { + // Arrange + var original = PaymentMethod.CreateAsCreditCard( + new CreditCardPayment("4111111111111111", "Alice Johnson", "12/25", "123") + ); + + // Act + var restored = XmlSerializationHelper.RoundTrip(original); + + // Assert + Assert.NotNull(restored); + Assert.Equal(PaymentMethod.ChoiceOf.CreditCard, restored.ChoiceType); + Assert.Equal("Alice Johnson", restored.CreditCard?.CardHolderName); + Assert.Equal("4111111111111111", restored.CreditCard?.CardNumber); + } + + [Fact] + public void XmlRoundTrip_ShippingOption_ShouldPreserveData() + { + // Arrange + var original = ShippingOption.CreateAsStandard( + new ShippingDetails(5.99m, 7, "USPS") + ); + + // Act + var restored = XmlSerializationHelper.RoundTrip(original); + + // Assert + Assert.NotNull(restored); + Assert.Equal(ShippingOption.ChoiceOf.Standard, restored.ChoiceType); + Assert.Equal(5.99m, restored.Standard?.Cost); + Assert.Equal(7, restored.Standard?.EstimatedDays); + Assert.Equal("USPS", restored.Standard?.Carrier); + } + + [Fact] + public void XmlRoundTrip_SearchCriteria_WithPriceRange_ShouldPreserveStruct() + { + // Arrange + var original = SearchCriteria.CreateAsPriceRange(new PriceRange(100m, 500m)); + + // Act + var restored = XmlSerializationHelper.RoundTrip(original); + + // Assert + Assert.NotNull(restored); + Assert.Equal(SearchCriteria.ChoiceOf.PriceRange, restored.ChoiceType); + Assert.NotNull(restored.PriceRange); + Assert.Equal(100m, restored.PriceRange.Value.MinPrice); + Assert.Equal(500m, restored.PriceRange.Value.MaxPrice); + } + + [Fact] + public void XmlRoundTrip_SearchCriteria_WithDateRange_ShouldPreserveDates() + { + // Arrange + var startDate = new DateTime(2024, 1, 1); + var endDate = new DateTime(2024, 12, 31); + var original = SearchCriteria.CreateAsDateRange(new DateRange(startDate, endDate)); + + // Act + var restored = XmlSerializationHelper.RoundTrip(original); + + // Assert + Assert.NotNull(restored); + Assert.Equal(SearchCriteria.ChoiceOf.DateRange, restored.ChoiceType); + Assert.NotNull(restored.DateRange); + Assert.Equal(startDate, restored.DateRange.StartDate.Date); + Assert.Equal(endDate, restored.DateRange.EndDate.Date); + } + + [Fact] + public void XmlRoundTrip_MultipleDifferentChoices_ShouldAllWork() + { + // Arrange + var payments = new[] + { + PaymentMethod.CreateAsCreditCard(new CreditCardPayment("4111", "Alice", "12/25", "123")), + PaymentMethod.CreateAsPayPal(new PayPalPayment("bob@test.com")), + PaymentMethod.CreateAsBankTransfer(new BankTransferPayment("123", "456", "Chase")) + }; + + // Act & Assert + foreach (var original in payments) + { + var restored = XmlSerializationHelper.RoundTrip(original); + Assert.NotNull(restored); + Assert.Equal(original.ChoiceType, restored.ChoiceType); + } + } + + #endregion +} diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceTypeCreationTests.cs b/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceTypeCreationTests.cs new file mode 100644 index 0000000..f20c1dc --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceTypeCreationTests.cs @@ -0,0 +1,220 @@ +using AltaSoft.ChoiceGenerator.Tests.TestModels; +using Xunit; + +namespace AltaSoft.ChoiceGenerator.Tests; + +/// +/// Tests for choice type creation using factory methods and implicit operators +/// +public class ChoiceTypeCreationTests +{ + #region Factory Method Tests + + [Fact] + public void CreateAsCreditCard_WithValidData_ShouldCreateCorrectChoice() + { + // Arrange + var cardPayment = new CreditCardPayment("4111111111111111", "John Doe", "12/25", "123"); + + // Act + var paymentMethod = PaymentMethod.CreateAsCreditCard(cardPayment); + + // Assert + Assert.NotNull(paymentMethod); + Assert.Equal(PaymentMethod.ChoiceOf.CreditCard, paymentMethod.ChoiceType); + Assert.NotNull(paymentMethod.CreditCard); + Assert.Equal("4111111111111111", paymentMethod.CreditCard.CardNumber); + Assert.Equal("John Doe", paymentMethod.CreditCard.CardHolderName); + Assert.Null(paymentMethod.BankTransfer); + Assert.Null(paymentMethod.PayPal); + } + + [Fact] + public void CreateAsBankTransfer_WithValidData_ShouldCreateCorrectChoice() + { + // Arrange + var bankTransfer = new BankTransferPayment("123456789", "987654321", "Bank of America"); + + // Act + var paymentMethod = PaymentMethod.CreateAsBankTransfer(bankTransfer); + + // Assert + Assert.NotNull(paymentMethod); + Assert.Equal(PaymentMethod.ChoiceOf.BankTransfer, paymentMethod.ChoiceType); + Assert.NotNull(paymentMethod.BankTransfer); + Assert.Equal("123456789", paymentMethod.BankTransfer.AccountNumber); + Assert.Null(paymentMethod.CreditCard); + Assert.Null(paymentMethod.PayPal); + } + + [Fact] + public void CreateAsPayPal_WithValidData_ShouldCreateCorrectChoice() + { + // Arrange + var payPal = new PayPalPayment("john.doe@example.com", "TXN-12345"); + + // Act + var paymentMethod = PaymentMethod.CreateAsPayPal(payPal); + + // Assert + Assert.NotNull(paymentMethod); + Assert.Equal(PaymentMethod.ChoiceOf.PayPal, paymentMethod.ChoiceType); + Assert.NotNull(paymentMethod.PayPal); + Assert.Equal("john.doe@example.com", paymentMethod.PayPal.Email); + Assert.Null(paymentMethod.CreditCard); + Assert.Null(paymentMethod.BankTransfer); + } + + [Fact] + public void CreateAsStandard_WithShippingDetails_ShouldCreateCorrectChoice() + { + // Arrange + var shipping = new ShippingDetails(5.99m, 7, "USPS"); + + // Act + var shippingOption = ShippingOption.CreateAsStandard(shipping); + + // Assert + Assert.NotNull(shippingOption); + Assert.Equal(ShippingOption.ChoiceOf.Standard, shippingOption.ChoiceType); + Assert.NotNull(shippingOption.Standard); + Assert.Equal(5.99m, shippingOption.Standard.Cost); + Assert.Equal(7, shippingOption.Standard.EstimatedDays); + Assert.Null(shippingOption.Express); + Assert.Null(shippingOption.Overnight); + } + + [Fact] + public void CreateAsKeyword_WithString_ShouldCreateCorrectChoice() + { + // Arrange + const string keyword = "laptop"; + + // Act + var searchCriteria = SearchCriteria.CreateAsKeyword(keyword); + + // Assert + Assert.NotNull(searchCriteria); + Assert.Equal(SearchCriteria.ChoiceOf.Keyword, searchCriteria.ChoiceType); + Assert.Equal("laptop", searchCriteria.Keyword); + Assert.Null(searchCriteria.CategoryId); + Assert.Null(searchCriteria.DateRange); + Assert.Null(searchCriteria.PriceRange); + } + + [Fact] + public void CreateAsCategoryId_WithInt_ShouldCreateCorrectChoice() + { + // Arrange + const int categoryId = 42; + + // Act + var searchCriteria = SearchCriteria.CreateAsCategoryId(categoryId); + + // Assert + Assert.NotNull(searchCriteria); + Assert.Equal(SearchCriteria.ChoiceOf.CategoryId, searchCriteria.ChoiceType); + Assert.Equal(42, searchCriteria.CategoryId); + Assert.Null(searchCriteria.Keyword); + Assert.Null(searchCriteria.DateRange); + Assert.Null(searchCriteria.PriceRange); + } + + #endregion + + #region Implicit Operator Tests + + [Fact] + public void ImplicitOperator_FromCreditCardPayment_ShouldCreateChoice() + { + // Arrange + var cardPayment = new CreditCardPayment("4111111111111111", "Jane Smith", "06/26", "456"); + + // Act + PaymentMethod paymentMethod = cardPayment; + + // Assert + Assert.NotNull(paymentMethod); + Assert.Equal(PaymentMethod.ChoiceOf.CreditCard, paymentMethod.ChoiceType); + Assert.NotNull(paymentMethod.CreditCard); + Assert.Equal("Jane Smith", paymentMethod.CreditCard.CardHolderName); + } + + [Fact] + public void ImplicitOperator_FromBankTransferPayment_ShouldCreateChoice() + { + // Arrange + var bankTransfer = new BankTransferPayment("987654321", "123456789", "Chase Bank"); + + // Act + PaymentMethod paymentMethod = bankTransfer; + + // Assert + Assert.NotNull(paymentMethod); + Assert.Equal(PaymentMethod.ChoiceOf.BankTransfer, paymentMethod.ChoiceType); + Assert.NotNull(paymentMethod.BankTransfer); + Assert.Equal("Chase Bank", paymentMethod.BankTransfer.BankName); + } + + [Fact] + public void ImplicitOperator_FromString_ShouldCreateSearchCriteria() + { + // Arrange + const string keyword = "smartphone"; + + // Act + SearchCriteria criteria = keyword; + + // Assert + Assert.NotNull(criteria); + Assert.Equal(SearchCriteria.ChoiceOf.Keyword, criteria.ChoiceType); + Assert.Equal("smartphone", criteria.Keyword); + } + + [Fact] + public void ImplicitOperator_FromInt_ShouldCreateSearchCriteria() + { + // Arrange + const int categoryId = 100; + + // Act + SearchCriteria criteria = categoryId; + + // Assert + Assert.NotNull(criteria); + Assert.Equal(SearchCriteria.ChoiceOf.CategoryId, criteria.ChoiceType); + Assert.Equal(100, criteria.CategoryId); + } + + #endregion + + #region Single Property Choice Tests + + [Fact] + public void NotificationChannel_SingleProperty_ShouldCreateCorrectly() + { + // Arrange & Act + var channel = NotificationChannel.CreateAsChannel(NotificationChannelType.Email); + + // Assert + Assert.NotNull(channel); + Assert.Equal(NotificationChannel.ChoiceOf.Channel, channel.ChoiceType); + Assert.Equal(NotificationChannelType.Email, channel.Channel); + } + + [Fact] + public void NotificationChannel_ImplicitOperator_ShouldWork() + { + // Arrange + const NotificationChannelType type = NotificationChannelType.SMS; + + // Act + NotificationChannel channel = type; + + // Assert + Assert.NotNull(channel); + Assert.Equal(NotificationChannelType.SMS, channel.Channel); + } + + #endregion +} diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceTypeSwitchMatchTests.cs b/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceTypeSwitchMatchTests.cs new file mode 100644 index 0000000..ece6ca2 --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceTypeSwitchMatchTests.cs @@ -0,0 +1,262 @@ +using System; +using System.Linq; +using AltaSoft.ChoiceGenerator.Tests.TestModels; +using Xunit; + +namespace AltaSoft.ChoiceGenerator.Tests; + +/// +/// Tests for Switch and Match pattern matching on choice types +/// +public class ChoiceTypeSwitchMatchTests +{ + #region Match Pattern Tests + + [Fact] + public void Match_WithCreditCardPayment_ShouldExecuteCorrectBranch() + { + // Arrange + var cardPayment = new CreditCardPayment("4111111111111111", "Alice", "12/25", "123"); + var paymentMethod = PaymentMethod.CreateAsCreditCard(cardPayment); + + // Act + var result = paymentMethod.Match( + creditCard => $"Card: {creditCard.CardHolderName}", + bankTransfer => $"Bank: {bankTransfer.BankName}", + payPal => $"PayPal: {payPal.Email}" + ); + + // Assert + Assert.Equal("Card: Alice", result); + } + + [Fact] + public void Match_WithBankTransferPayment_ShouldExecuteCorrectBranch() + { + // Arrange + var bankTransfer = new BankTransferPayment("123456", "654321", "Wells Fargo"); + var paymentMethod = PaymentMethod.CreateAsBankTransfer(bankTransfer); + + // Act + var result = paymentMethod.Match( + creditCard => $"Card: {creditCard.CardHolderName}", + bankTransfer => $"Bank: {bankTransfer.BankName}", + payPal => $"PayPal: {payPal.Email}" + ); + + // Assert + Assert.Equal("Bank: Wells Fargo", result); + } + + [Fact] + public void Match_WithPayPalPayment_ShouldExecuteCorrectBranch() + { + // Arrange + var payPal = new PayPalPayment("bob@example.com", "TXN-789"); + var paymentMethod = PaymentMethod.CreateAsPayPal(payPal); + + // Act + var result = paymentMethod.Match( + creditCard => $"Card: {creditCard.CardHolderName}", + bankTransfer => $"Bank: {bankTransfer.BankName}", + payPal => $"PayPal: {payPal.Email}" + ); + + // Assert + Assert.Equal("PayPal: bob@example.com", result); + } + + [Fact] + public void Match_WithShippingOptions_ShouldCalculateTotalCost() + { + // Arrange + var expressShipping = new ShippingDetails(15.99m, 2, "FedEx"); + var shippingOption = ShippingOption.CreateAsExpress(expressShipping); + + // Act + var totalCost = shippingOption.Match( + standard => standard.Cost, + express => express.Cost * 1.1m, // 10% processing fee for express + overnight => overnight.Cost * 1.2m // 20% processing fee for overnight + ); + + // Assert + Assert.Equal(17.589m, totalCost); + } + + [Fact] + public void Match_WithSearchCriteria_ShouldFormatQuery() + { + // Arrange + var searchCriteria = SearchCriteria.CreateAsKeyword("laptop"); + + // Act + var query = searchCriteria.Match( + keyword => $"keyword={keyword}", + categoryId => $"category={categoryId}", + dateRange => $"from={dateRange.StartDate:yyyy-MM-dd}&to={dateRange.EndDate:yyyy-MM-dd}", + priceRange => $"min={priceRange.MinPrice}&max={priceRange.MaxPrice}" + ); + + // Assert + Assert.Equal("keyword=laptop", query); + } + + #endregion + + #region Switch Pattern Tests + + [Fact] + public void Switch_WithCreditCardPayment_ShouldExecuteCorrectAction() + { + // Arrange + var cardPayment = new CreditCardPayment("4111111111111111", "Charlie", "12/25", "123"); + var paymentMethod = PaymentMethod.CreateAsCreditCard(cardPayment); + var processedType = string.Empty; + + // Act + paymentMethod.Switch( + creditCard => processedType = "credit_card", + bankTransfer => processedType = "bank_transfer", + payPal => processedType = "paypal" + ); + + // Assert + Assert.Equal("credit_card", processedType); + } + + [Fact] + public void Switch_WithBankTransferPayment_ShouldExecuteCorrectAction() + { + // Arrange + var bankTransfer = new BankTransferPayment("999888", "777666", "Bank of America"); + var paymentMethod = PaymentMethod.CreateAsBankTransfer(bankTransfer); + var processedType = string.Empty; + + // Act + paymentMethod.Switch( + creditCard => processedType = "credit_card", + bankTransfer => processedType = "bank_transfer", + payPal => processedType = "paypal" + ); + + // Assert + Assert.Equal("bank_transfer", processedType); + } + + [Fact] + public void Switch_WithPayPalPayment_ShouldExecuteCorrectAction() + { + // Arrange + var payPal = new PayPalPayment("dana@test.com"); + var paymentMethod = PaymentMethod.CreateAsPayPal(payPal); + var processedType = string.Empty; + + // Act + paymentMethod.Switch( + creditCard => processedType = "credit_card", + bankTransfer => processedType = "bank_transfer", + payPal => processedType = "paypal" + ); + + // Assert + Assert.Equal("paypal", processedType); + } + + [Fact] + public void Switch_WithShippingOptions_ShouldAccumulateData() + { + // Arrange + var overnightShipping = new ShippingDetails(29.99m, 1, "DHL"); + var shippingOption = ShippingOption.CreateAsOvernight(overnightShipping); + var deliveryInfo = new { Days = 0, Cost = 0m, Type = "" }; + + // Act + shippingOption.Switch( + standard => deliveryInfo = new { Days = standard.EstimatedDays, Cost = standard.Cost, Type = "Standard" }, + express => deliveryInfo = new { Days = express.EstimatedDays, Cost = express.Cost, Type = "Express" }, + overnight => deliveryInfo = new { Days = overnight.EstimatedDays, Cost = overnight.Cost, Type = "Overnight" } + ); + + // Assert + Assert.Equal(1, deliveryInfo.Days); + Assert.Equal(29.99m, deliveryInfo.Cost); + Assert.Equal("Overnight", deliveryInfo.Type); + } + + [Fact] + public void Switch_WithSearchCriteria_ShouldModifyExternalState() + { + // Arrange + var priceRange = new PriceRange(100m, 500m); + var searchCriteria = SearchCriteria.CreateAsPriceRange(priceRange); + var filterApplied = false; + var filterType = ""; + + // Act + searchCriteria.Switch( + keyword => { filterApplied = true; filterType = "text"; }, + categoryId => { filterApplied = true; filterType = "category"; }, + dateRange => { filterApplied = true; filterType = "date"; }, + priceRange => { filterApplied = true; filterType = "price"; } + ); + + // Assert + Assert.True(filterApplied); + Assert.Equal("price", filterType); + } + + #endregion + + #region Complex Scenarios + + [Fact] + public void Match_ChainedWithSwitch_ShouldWorkCorrectly() + { + // Arrange + var standardShipping = new ShippingDetails(5.99m, 7, "USPS"); + var shippingOption = ShippingOption.CreateAsStandard(standardShipping); + + // Act - Use Match to calculate discount + var discount = shippingOption.Match( + standard => 0m, + express => 2m, + overnight => 5m + ); + + // Switch to apply discount + var finalCost = 0m; + shippingOption.Switch( + standard => finalCost = standard.Cost - discount, + express => finalCost = express.Cost - discount, + overnight => finalCost = overnight.Cost - discount + ); + + // Assert + Assert.Equal(5.99m, finalCost); + } + + [Fact] + public void Match_UsedInLinqQuery_ShouldWork() + { + // Arrange + var payments = new[] + { + PaymentMethod.CreateAsCreditCard(new CreditCardPayment("4111", "Alice", "12/25", "123")), + PaymentMethod.CreateAsPayPal(new PayPalPayment("bob@test.com")), + PaymentMethod.CreateAsBankTransfer(new BankTransferPayment("123", "456", "Chase")) + }; + + // Act + var paymentTypes = payments.Select(p => p.Match( + cc => "Card", + bt => "Transfer", + pp => "PayPal" + )).ToArray(); + + // Assert + Assert.Equal(new[] { "Card", "PayPal", "Transfer" }, paymentTypes); + } + + #endregion +} diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/MixedPropertiesChoiceTests.cs b/tests/AltaSoft.ChoiceGenerator.Tests/MixedPropertiesChoiceTests.cs new file mode 100644 index 0000000..ee89a20 --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/MixedPropertiesChoiceTests.cs @@ -0,0 +1,560 @@ +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; +using AltaSoft.Choice; +using Xunit; + +namespace AltaSoft.ChoiceGenerator.Tests; + +#region Test Model Definitions + +/// +/// Choice type with one ordinary property and two choice properties +/// +[Choice] +public sealed partial class OrderChoice +{ + /// + /// Ordinary property - Order ID (not part of the choice) + /// + [XmlElement("OrderId")] + public string? OrderId { get; set; } + + /// + /// Choice property - Express delivery + /// + [XmlTag("Express")] + public partial ExpressDelivery? Express { get; set; } + + /// + /// Choice property - Standard delivery + /// + [XmlTag("Standard")] + public partial StandardDelivery? Standard { get; set; } +} + +public sealed class ExpressDelivery +{ + public DateTime DeliveryDate { get; set; } + public decimal SurchargeAmount { get; set; } + + public ExpressDelivery() { } + public ExpressDelivery(DateTime deliveryDate, decimal surchargeAmount) + { + DeliveryDate = deliveryDate; + SurchargeAmount = surchargeAmount; + } +} + +public sealed class StandardDelivery +{ + public int DeliveryDays { get; set; } + public decimal ShippingCost { get; set; } + + public StandardDelivery() { } + public StandardDelivery(int deliveryDays, decimal shippingCost) + { + DeliveryDays = deliveryDays; + ShippingCost = shippingCost; + } +} + +/// +/// Choice type with multiple ordinary properties and choice properties +/// +[Choice] +public sealed partial class PaymentRequest +{ + /// + /// Ordinary property - Transaction ID + /// + [XmlElement("TxnId")] + public string? TransactionId { get; set; } + + /// + /// Ordinary property - Amount + /// + [XmlElement("Amt")] + public decimal Amount { get; set; } + + /// + /// Ordinary property - Currency + /// + [XmlElement("Ccy")] + public string? Currency { get; set; } + + /// + /// Choice property - Card payment + /// + [XmlTag("Card")] + public partial CardPayment? Card { get; set; } + + /// + /// Choice property - Bank transfer + /// + [XmlTag("BankTrf")] + public partial BankTransfer? BankTransfer { get; set; } + + /// + /// Choice property - Cash payment + /// + [XmlTag("Cash")] + public partial CashPayment? Cash { get; set; } +} + +public sealed class CardPayment +{ + public string? CardNumber { get; set; } + public string? ExpiryDate { get; set; } + + public CardPayment() { } + public CardPayment(string cardNumber, string expiryDate) + { + CardNumber = cardNumber; + ExpiryDate = expiryDate; + } +} + +public sealed class BankTransfer +{ + public string? IBAN { get; set; } + public string? BIC { get; set; } + + public BankTransfer() { } + public BankTransfer(string iban, string bic) + { + IBAN = iban; + BIC = bic; + } +} + +public sealed class CashPayment +{ + public string? ReceiptNumber { get; set; } + public DateTime ReceivedDate { get; set; } + + public CashPayment() { } + public CashPayment(string receiptNumber, DateTime receivedDate) + { + ReceiptNumber = receiptNumber; + ReceivedDate = receivedDate; + } +} + +#endregion + +/// +/// Tests for Choice types that contain both Choice properties (partial) and ordinary properties +/// +public class MixedPropertiesChoiceTests +{ + private static readonly XmlWriterSettings s_xmlWriterSettings = new() + { + OmitXmlDeclaration = true, + Indent = true + }; + + [Fact] + public void MixedProperties_OrdinaryPropertyShouldNotAffectChoice() + { + var order = new OrderChoice + { + OrderId = "ORD-12345", + Express = new ExpressDelivery(new DateTime(2024, 12, 25), 25.00m) + }; + + Assert.Equal("ORD-12345", order.OrderId); + Assert.Equal(OrderChoice.ChoiceOf.Express, order.ChoiceType); + Assert.NotNull(order.Express); + Assert.Null(order.Standard); + } + + [Fact] + public void MixedProperties_ChangingChoiceShouldNotAffectOrdinaryProperty() + { + var order = new OrderChoice + { + OrderId = "ORD-99999", + Express = new ExpressDelivery(new DateTime(2024, 12, 25), 25.00m) + }; + + // Change the choice + order.Standard = new StandardDelivery(5, 10.00m); + + // Ordinary property should remain unchanged + Assert.Equal("ORD-99999", order.OrderId); + Assert.Equal(OrderChoice.ChoiceOf.Standard, order.ChoiceType); + Assert.Null(order.Express); + Assert.NotNull(order.Standard); + } + + [Fact] + public void MixedProperties_MultipleOrdinaryProperties_ShouldNotAffectChoice() + { + var payment = new PaymentRequest + { + TransactionId = "TXN-001", + Amount = 150.50m, + Currency = "USD", + Card = new CardPayment("4111111111111111", "12/25") + }; + + Assert.Equal("TXN-001", payment.TransactionId); + Assert.Equal(150.50m, payment.Amount); + Assert.Equal("USD", payment.Currency); + Assert.Equal(PaymentRequest.ChoiceOf.Card, payment.ChoiceType); + Assert.NotNull(payment.Card); + Assert.Null(payment.BankTransfer); + Assert.Null(payment.Cash); + } + + [Fact] + public void MixedProperties_SwitchChoice_OrdinaryPropertiesRemainIntact() + { + var payment = new PaymentRequest + { + TransactionId = "TXN-002", + Amount = 500.00m, + Currency = "EUR", + Card = new CardPayment("5500000000000004", "06/26") + }; + + // Switch to BankTransfer + payment.BankTransfer = new BankTransfer("GB82WEST12345698765432", "WESTGB22"); + + // Ordinary properties should remain unchanged + Assert.Equal("TXN-002", payment.TransactionId); + Assert.Equal(500.00m, payment.Amount); + Assert.Equal("EUR", payment.Currency); + Assert.Equal(PaymentRequest.ChoiceOf.BankTransfer, payment.ChoiceType); + Assert.Null(payment.Card); + Assert.NotNull(payment.BankTransfer); + Assert.Null(payment.Cash); + + // Switch to Cash + payment.Cash = new CashPayment("RCPT-999", new DateTime(2024, 12, 20)); + + // Ordinary properties should still remain unchanged + Assert.Equal("TXN-002", payment.TransactionId); + Assert.Equal(500.00m, payment.Amount); + Assert.Equal("EUR", payment.Currency); + Assert.Equal(PaymentRequest.ChoiceOf.Cash, payment.ChoiceType); + Assert.Null(payment.Card); + Assert.Null(payment.BankTransfer); + Assert.NotNull(payment.Cash); + } + + #region Factory Method Tests + + [Fact] + public void MixedProperties_CreateAsFactory_ShouldSetChoiceOnly() + { + var order = OrderChoice.CreateAsExpress( + new ExpressDelivery(new DateTime(2024, 12, 31), 30.00m) + ); + + Assert.Null(order.OrderId); // Ordinary property not set by factory + Assert.Equal(OrderChoice.ChoiceOf.Express, order.ChoiceType); + Assert.NotNull(order.Express); + Assert.Equal(30.00m, order.Express.SurchargeAmount); + } + + [Fact] + public void MixedProperties_CreateAsFactory_ThenSetOrdinaryProperties() + { + var payment = PaymentRequest.CreateAsCard( + new CardPayment("4012888888881881", "03/27") + ); + + // Set ordinary properties after creation + payment.TransactionId = "TXN-FACTORY"; + payment.Amount = 299.99m; + payment.Currency = "GBP"; + + Assert.Equal("TXN-FACTORY", payment.TransactionId); + Assert.Equal(299.99m, payment.Amount); + Assert.Equal("GBP", payment.Currency); + Assert.Equal(PaymentRequest.ChoiceOf.Card, payment.ChoiceType); + Assert.NotNull(payment.Card); + } + + #endregion + + #region Match and Switch Tests + + [Fact] + public void MixedProperties_Match_ShouldAccessChoiceAndOrdinaryProperties() + { + var payment = new PaymentRequest + { + TransactionId = "TXN-MATCH", + Amount = 100.00m, + Currency = "USD", + Card = new CardPayment("4111111111111111", "01/26") + }; + + var result = payment.Match( + matchCard: card => $"Card payment: {card.CardNumber} for {payment.Amount} {payment.Currency}", + matchBankTransfer: bank => $"Bank transfer to {bank.IBAN}", + matchCash: cash => $"Cash payment receipt {cash.ReceiptNumber}" + ); + + Assert.Equal("Card payment: 4111111111111111 for 100.00 USD", result); + } + + [Fact] + public void MixedProperties_Switch_ShouldAccessChoiceAndOrdinaryProperties() + { + var payment = new PaymentRequest + { + TransactionId = "TXN-SWITCH", + Amount = 250.00m, + Currency = "EUR", + BankTransfer = new BankTransfer("DE89370400440532013000", "COBADEFFXXX") + }; + + string output = string.Empty; + + payment.Switch( + matchCard: card => output = $"Processing card {payment.TransactionId}", + matchBankTransfer: bank => output = $"Processing bank transfer {payment.TransactionId} to {bank.IBAN}", + matchCash: cash => output = $"Processing cash {payment.TransactionId}" + ); + + Assert.Equal("Processing bank transfer TXN-SWITCH to DE89370400440532013000", output); + } + + #endregion + + #region XML Serialization Tests + + [Fact] + public void MixedProperties_XmlSerialization_OrderChoice_ShouldRoundTrip() + { + var order = new OrderChoice + { + OrderId = "ORD-XML-001", + Express = new ExpressDelivery(new DateTime(2024, 12, 25), 25.00m) + }; + + var serializer = new XmlSerializer(typeof(OrderChoice)); + + // Serialize + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, order, XmlNamespaceHelper.EmptyNamespace); + + var xml = sw.ToString(); + + // Verify structure + Assert.Contains("ORD-XML-001", xml); + Assert.Contains("", xml); + Assert.DoesNotContain("TXN-XML-001", xml); + Assert.Contains("150.00", xml); + Assert.Contains("USD", xml); + Assert.Contains("", xml); + Assert.DoesNotContain("TXN-XML-002", xml); + Assert.Contains("500.00", xml); + Assert.Contains("EUR", xml); + Assert.Contains("", xml); + Assert.DoesNotContain("", xml); + Assert.DoesNotContain("TXN-NO-CHOICE", xml); + Assert.Contains("99.99", xml); + Assert.Contains("GBP", xml); + Assert.DoesNotContain("", xml); + Assert.DoesNotContain("", xml); + Assert.DoesNotContain("", xml); + Assert.DoesNotContain("xsi:nil", xml); + } + + #endregion + + #region Edge Cases + + [Fact] + public void MixedProperties_ModifyOrdinaryPropertyMultipleTimes_ChoiceShouldRemainStable() + { + var order = new OrderChoice + { + OrderId = "ORD-001", + Express = new ExpressDelivery(new DateTime(2024, 12, 25), 25.00m) + }; + + // Modify ordinary property multiple times + order.OrderId = "ORD-002"; + order.OrderId = "ORD-003"; + order.OrderId = "ORD-004"; + + Assert.Equal("ORD-004", order.OrderId); + Assert.Equal(OrderChoice.ChoiceOf.Express, order.ChoiceType); + Assert.NotNull(order.Express); + } + + [Fact] + public void MixedProperties_SetOrdinaryPropertyToNull_ChoiceShouldRemainUnaffected() + { + var order = new OrderChoice + { + OrderId = "ORD-NULL", + Standard = new StandardDelivery(3, 5.00m) + }; + + order.OrderId = null; + + Assert.Null(order.OrderId); + Assert.Equal(OrderChoice.ChoiceOf.Standard, order.ChoiceType); + Assert.NotNull(order.Standard); + } + + [Fact] + public void MixedProperties_ComplexScenario_AllPropertiesModified() + { + var payment = new PaymentRequest + { + TransactionId = "INIT", + Amount = 100m, + Currency = "USD", + Card = new CardPayment("1234", "01/25") + }; + + // Modify ordinary properties + payment.TransactionId = "UPDATED-1"; + payment.Amount = 200m; + + // Switch choice + payment.BankTransfer = new BankTransfer("IBAN123", "BIC456"); + + // Modify ordinary properties again + payment.Currency = "EUR"; + payment.TransactionId = "UPDATED-2"; + + // Switch choice again + payment.Cash = new CashPayment("RCPT-001", DateTime.Now); + + // Modify ordinary properties one more time + payment.Amount = 300m; + + // Verify final state + Assert.Equal("UPDATED-2", payment.TransactionId); + Assert.Equal(300m, payment.Amount); + Assert.Equal("EUR", payment.Currency); + Assert.Equal(PaymentRequest.ChoiceOf.Cash, payment.ChoiceType); + Assert.Null(payment.Card); + Assert.Null(payment.BankTransfer); + Assert.NotNull(payment.Cash); + } + + #endregion +} diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/ObsoleteTypeChoiceTests.cs b/tests/AltaSoft.ChoiceGenerator.Tests/ObsoleteTypeChoiceTests.cs new file mode 100644 index 0000000..26fc03b --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/ObsoleteTypeChoiceTests.cs @@ -0,0 +1,85 @@ +using AltaSoft.Choice; +using System; +using Xunit; + +namespace AltaSoft.ChoiceGenerator.Tests; + +/// +/// Choice type using obsolete types - verifies warning suppression +/// +[Choice] +public sealed partial class PaymentMethodChoice +{ + /// + /// Legacy payment method - obsolete but still supported + /// + public partial LegacyPayment? Legacy { get; set; } + + /// + /// Modern payment method + /// + public partial ModernPayment? Modern { get; set; } +} + +/// +/// Obsolete payment type - still used in some legacy systems +/// +[Obsolete("Use ModernPayment instead", false)] +public sealed class LegacyPayment +{ + public string CardNumber { get; set; } = string.Empty; + public LegacyPayment() { } +} + +/// +/// Modern payment type +/// +public sealed class ModernPayment +{ + public string Token { get; set; } = string.Empty; + public ModernPayment() { } +} + +/// +/// Tests for Choice types using obsolete types - verifies no warnings in generated code +/// +public class ObsoleteTypeChoiceTests +{ + [Fact] + public void ObsoleteType_CanBeUsedInChoice_WithoutWarnings() + { + // The generated code should have CS0618 suppressed, so using obsolete types works + var payment = PaymentMethodChoice.CreateAsLegacy(new LegacyPayment { CardNumber = "1234" }); + + Assert.Equal(PaymentMethodChoice.ChoiceOf.Legacy, payment.ChoiceType); + Assert.NotNull(payment.Legacy); + Assert.Equal("1234", payment.Legacy.CardNumber); + } + + [Fact] + public void ObsoleteType_Switch_ShouldWork() + { + var payment = PaymentMethodChoice.CreateAsLegacy(new LegacyPayment { CardNumber = "5678" }); + + var result = string.Empty; + payment.Switch( + legacy => result = $"Legacy: {legacy.CardNumber}", + modern => result = $"Modern: {modern.Token}" + ); + + Assert.Equal("Legacy: 5678", result); + } + + [Fact] + public void ObsoleteType_Match_ShouldWork() + { + var payment = PaymentMethodChoice.CreateAsLegacy(new LegacyPayment { CardNumber = "9012" }); + + var result = payment.Match( + legacy => $"Legacy: {legacy.CardNumber}", + modern => $"Modern: {modern.Token}" + ); + + Assert.Equal("Legacy: 9012", result); + } +} diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/RequiredPropertiesChoiceTests.cs b/tests/AltaSoft.ChoiceGenerator.Tests/RequiredPropertiesChoiceTests.cs new file mode 100644 index 0000000..02c9673 --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/RequiredPropertiesChoiceTests.cs @@ -0,0 +1,126 @@ +using AltaSoft.Choice; +using Xunit; + +namespace AltaSoft.ChoiceGenerator.Tests; + +/// +/// Choice type with required ordinary properties - demonstrates generator enhancement +/// +[Choice] +public sealed partial class OrderWithRequiredProperties +{ + /// + /// Required ordinary property - Order ID + /// + public required string OrderId { get; set; } + + /// + /// Required ordinary property - Customer Name + /// + public required string CustomerName { get; set; } + + /// + /// Choice property - Express delivery + /// + public partial ExpressDelivery? Express { get; set; } + + /// + /// Choice property - Standard delivery + /// + public partial StandardDelivery? Standard { get; set; } +} + +/// +/// Tests for Choice types with required ordinary properties +/// +public class RequiredPropertiesChoiceTests +{ + [Fact] + public void RequiredProperties_CreateAsExpress_ShouldRequireOrdinaryProperties() + { + // This should work after the fix - note camelCase parameter names + var order = OrderWithRequiredProperties.CreateAsExpress( + "ORD-001", // orderId + "John Doe", // customerName + new ExpressDelivery(new System.DateTime(2024, 12, 25), 25.00m) // express (value) + ); + + Assert.Equal("ORD-001", order.OrderId); + Assert.Equal("John Doe", order.CustomerName); + Assert.Equal(OrderWithRequiredProperties.ChoiceOf.Express, order.ChoiceType); + Assert.NotNull(order.Express); + } + + [Fact] + public void RequiredProperties_CreateAsStandard_ShouldRequireOrdinaryProperties() + { + // This should work after the fix - note camelCase parameter names + var order = OrderWithRequiredProperties.CreateAsStandard( + "ORD-002", // orderId + "Jane Smith", // customerName + new StandardDelivery(5, 10.00m) // standard (value) + ); + + Assert.Equal("ORD-002", order.OrderId); + Assert.Equal("Jane Smith", order.CustomerName); + Assert.Equal(OrderWithRequiredProperties.ChoiceOf.Standard, order.ChoiceType); + Assert.NotNull(order.Standard); + } + + [Fact] + public void RequiredPropertyNamedValue_ShouldNotConflictWithChoiceParameter() + { + // This tests the edge case where required ordinary properties would collide + // with both the default choice parameter name and its first fallback. + var config = ConfigWithValueProperty.CreateAsOptionA( + "CONFIG-123", // value (required property) + "CONFIG-ALT", // choiceValue (required property) + new OptionA("Option A Data") // choiceValue1 (choice parameter - uniquely renamed) + ); + + Assert.Equal("CONFIG-123", config.Value); + Assert.Equal("CONFIG-ALT", config.ChoiceValue); + Assert.Equal(ConfigWithValueProperty.ChoiceOf.OptionA, config.ChoiceType); + Assert.NotNull(config.OptionA); + Assert.Equal("Option A Data", config.OptionA.Data); + } +} + +/// +/// Choice type with required properties named "Value" and "ChoiceValue" to test parameter conflict resolution +/// +[Choice] +public sealed partial class ConfigWithValueProperty +{ + /// + /// Required property named "Value" - this would conflict with the default choice parameter name + /// + public required string Value { get; set; } + + /// + /// Required property named "ChoiceValue" - this would conflict with the first fallback choice parameter name + /// + public required string ChoiceValue { get; set; } + + /// + /// Choice property - Option A + /// + public partial OptionA? OptionA { get; set; } + + /// + /// Choice property - Option B + /// + public partial OptionB? OptionB { get; set; } +} + +public sealed class OptionA +{ + public string Data { get; set; } + public OptionA(string data) => Data = data; +} + +public sealed class OptionB +{ + public int Count { get; set; } + public OptionB(int count) => Count = count; +} diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/TestHelpers/JsonSerializationHelper.cs b/tests/AltaSoft.ChoiceGenerator.Tests/TestHelpers/JsonSerializationHelper.cs new file mode 100644 index 0000000..5d2f141 --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/TestHelpers/JsonSerializationHelper.cs @@ -0,0 +1,52 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace AltaSoft.ChoiceGenerator.Tests.TestHelpers; + +/// +/// Helper methods for JSON serialization in tests +/// +public static class JsonSerializationHelper +{ + private static readonly JsonSerializerOptions s_options = new() + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new JsonStringEnumConverter() } + }; + + /// + /// Serializes an object to JSON string + /// + public static string SerializeToJson(T obj) + { + return JsonSerializer.Serialize(obj, s_options); + } + + /// + /// Deserializes an object from JSON string + /// + public static T? DeserializeFromJson(string json) + { + return JsonSerializer.Deserialize(json, s_options); + } + + /// + /// Performs a round-trip serialization test: object -> JSON -> object + /// + public static T? RoundTrip(T original) + { + var json = SerializeToJson(original); + return DeserializeFromJson(json); + } + + /// + /// Normalizes JSON string for comparison (removes formatting differences) + /// + public static string NormalizeJson(string json) + { + // Parse and re-serialize to normalize formatting + using var doc = JsonDocument.Parse(json); + return JsonSerializer.Serialize(doc.RootElement, s_options); + } +} diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/TestHelpers/XmlSerializationHelper.cs b/tests/AltaSoft.ChoiceGenerator.Tests/TestHelpers/XmlSerializationHelper.cs new file mode 100644 index 0000000..666da87 --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/TestHelpers/XmlSerializationHelper.cs @@ -0,0 +1,67 @@ +using System; +using System.IO; +using System.Text; +using System.Xml; +using System.Xml.Serialization; +using AltaSoft.Choice; + +namespace AltaSoft.ChoiceGenerator.Tests.TestHelpers; + +/// +/// Helper methods for XML serialization in tests +/// +public static class XmlSerializationHelper +{ + private static readonly XmlWriterSettings s_writerSettings = new() + { + OmitXmlDeclaration = true, + Indent = true, + Encoding = Encoding.UTF8 + }; + + /// + /// Serializes an object to XML string + /// + public static string SerializeToXml(T obj) where T : class + { + var serializer = new XmlSerializer(typeof(T)); + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_writerSettings); + + serializer.Serialize(writer, obj, XmlNamespaceHelper.EmptyNamespace); + + return sw.ToString(); + } + + /// + /// Deserializes an object from XML string + /// + public static T DeserializeFromXml(string xml) where T : class + { + var serializer = new XmlSerializer(typeof(T)); + using var reader = new StringReader(xml); + + var result = serializer.Deserialize(reader) as T; + if (result == null) + throw new InvalidOperationException($"Failed to deserialize XML to type {typeof(T).Name}"); + + return result; + } + + /// + /// Performs a round-trip serialization test: object -> XML -> object + /// + public static T RoundTrip(T original) where T : class + { + var xml = SerializeToXml(original); + return DeserializeFromXml(xml); + } + + /// + /// Normalizes XML string for comparison (removes formatting differences) + /// + public static string NormalizeXml(string xml) + { + return xml.Trim().Replace("\r\n", "\n"); + } +} diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/NotificationChannel.cs b/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/NotificationChannel.cs new file mode 100644 index 0000000..17772f0 --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/NotificationChannel.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; +using AltaSoft.Choice; + +namespace AltaSoft.ChoiceGenerator.Tests.TestModels; + +/// +/// Represents a notification delivery channel (single property choice for testing) +/// +[Choice] +public sealed partial class NotificationChannel +{ + /// + /// The notification channel identifier + /// + [XmlTag("Channel")] + [JsonPropertyName("channel")] + public required partial NotificationChannelType Channel { get; set; } +} + +/// +/// Types of notification channels +/// +public enum NotificationChannelType +{ + Email, + SMS, + Push, + InApp +} diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/PaymentMethod.cs b/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/PaymentMethod.cs new file mode 100644 index 0000000..8e857f0 --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/PaymentMethod.cs @@ -0,0 +1,89 @@ +using System.Text.Json.Serialization; +using AltaSoft.Choice; + +namespace AltaSoft.ChoiceGenerator.Tests.TestModels; + +/// +/// Represents a payment method choice for e-commerce scenarios +/// +[Choice] +public sealed partial class PaymentMethod +{ + /// + /// Payment via credit or debit card + /// + [XmlTag("CreditCard")] + [JsonPropertyName("creditCard")] + public partial CreditCardPayment? CreditCard { get; set; } + + /// + /// Payment via bank transfer + /// + [XmlTag("BankTransfer")] + [JsonPropertyName("bankTransfer")] + public partial BankTransferPayment? BankTransfer { get; set; } + + /// + /// Payment via PayPal + /// + [XmlTag("PayPal")] + [JsonPropertyName("payPal")] + public partial PayPalPayment? PayPal { get; set; } +} + +/// +/// Credit card payment details +/// +public sealed class CreditCardPayment +{ + public string CardNumber { get; set; } = string.Empty; + public string CardHolderName { get; set; } = string.Empty; + public string ExpiryDate { get; set; } = string.Empty; + public string Cvv { get; set; } = string.Empty; + + public CreditCardPayment() { } + + public CreditCardPayment(string cardNumber, string cardHolderName, string expiryDate, string cvv) + { + CardNumber = cardNumber; + CardHolderName = cardHolderName; + ExpiryDate = expiryDate; + Cvv = cvv; + } +} + +/// +/// Bank transfer payment details +/// +public sealed class BankTransferPayment +{ + public string AccountNumber { get; set; } = string.Empty; + public string RoutingNumber { get; set; } = string.Empty; + public string BankName { get; set; } = string.Empty; + + public BankTransferPayment() { } + + public BankTransferPayment(string accountNumber, string routingNumber, string bankName) + { + AccountNumber = accountNumber; + RoutingNumber = routingNumber; + BankName = bankName; + } +} + +/// +/// PayPal payment details +/// +public sealed class PayPalPayment +{ + public string Email { get; set; } = string.Empty; + public string TransactionId { get; set; } = string.Empty; + + public PayPalPayment() { } + + public PayPalPayment(string email, string transactionId = "") + { + Email = email; + TransactionId = transactionId; + } +} diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/README.md b/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/README.md new file mode 100644 index 0000000..acb91f0 --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/README.md @@ -0,0 +1,142 @@ +# Test Models + +Realistic domain models for testing the AltaSoft Choice Generator. + +## Overview + +These models represent real-world scenarios to make tests more professional, maintainable, and understandable. They replace generic test types like `TwoValueTypeChoice` and `SinglePropertyChoice` with domain-specific models. + +## Models + +### PaymentMethod + +E-commerce payment choice supporting multiple payment types. + +**Choice Properties:** +- `CreditCard` (CreditCardPayment) - Credit/debit card payment +- `BankTransfer` (BankTransferPayment) - Direct bank transfer +- `PayPal` (PayPalPayment) - PayPal payment + +**Helper Classes:** +- `CreditCardPayment` - CardNumber, CardHolderName, ExpiryDate, Cvv +- `BankTransferPayment` - AccountNumber, RoutingNumber, BankName +- `PayPalPayment` - Email, TransactionId + +**Usage:** +```csharp +var card = new CreditCardPayment("4111111111111111", "John Doe", "12/25", "123"); +var payment = PaymentMethod.CreateAsCreditCard(card); + +payment.Match( + creditCard => ProcessCard(creditCard), + bankTransfer => ProcessBankTransfer(bankTransfer), + payPal => ProcessPayPal(payPal) +); +``` + +**Tests:** Creation, Switch/Match, JSON serialization, XML serialization + +--- + +### ShippingOption + +Shipping method choice for e-commerce orders. + +**Choice Properties:** +- `Standard` (ShippingDetails) - Standard shipping +- `Express` (ShippingDetails) - Express shipping +- `Overnight` (ShippingDetails) - Overnight shipping + +**Helper Classes:** +- `ShippingDetails` - Cost, EstimatedDays, Carrier + +**Usage:** +```csharp +var express = new ShippingDetails(15.99m, 2, "FedEx"); +var shipping = ShippingOption.CreateAsExpress(express); + +var totalCost = shipping.Match( + standard => standard.Cost, + express => express.Cost, + overnight => overnight.Cost +); +``` + +**Tests:** Creation, Switch/Match with cost calculation, JSON/XML round-trips + +--- + +### SearchCriteria + +Product catalog search choice supporting different search types. + +**Choice Properties:** +- `Keyword` (string) - Text search +- `CategoryId` (int) - Category filter +- `DateRange` (DateRange class) - Date range filter +- `PriceRange` (PriceRange struct) - Price range filter + +**Helper Classes:** +- `DateRange` - StartDate, EndDate +- `PriceRange` (struct) - MinPrice, MaxPrice + +**Usage:** +```csharp +var search = SearchCriteria.CreateAsKeyword("laptop"); +// OR +var search = SearchCriteria.CreateAsCategoryId(42); +// OR +var search = SearchCriteria.CreateAsDateRange(new DateRange(start, end)); + +var query = search.Match( + keyword => $"WHERE Name LIKE '%{keyword}%'", + categoryId => $"WHERE CategoryId = {categoryId}", + dateRange => $"WHERE Date BETWEEN '{dateRange.StartDate}' AND '{dateRange.EndDate}'", + priceRange => $"WHERE Price BETWEEN {priceRange.MinPrice} AND {priceRange.MaxPrice}" +); +``` + +**Tests:** Mixed value types (string, int, class, struct), LINQ integration, JSON/XML serialization + +--- + +### NotificationChannel + +Simple single-property choice for notification delivery. + +**Choice Properties:** +- `Channel` (NotificationChannelType enum, required) - Email, SMS, Push, InApp + +**Usage:** +```csharp +var channel = NotificationChannel.CreateAsChannel(NotificationChannelType.Email); + +// Implicit conversion +NotificationChannel smsChannel = NotificationChannelType.SMS; +``` + +**Tests:** Single-property choice, enum handling, implicit operators, required property + +--- + +## Design Principles + +✅ **Realistic** - Models represent actual domain concepts (payments, shipping, search) +✅ **Varied** - Different patterns (multi-choice, single-property, mixed types, structs) +✅ **Documented** - XML doc comments on all types and properties +✅ **Professional** - Follows C# naming conventions and patterns + +## JSON/XML Attributes + +Models use appropriate attributes for serialization control: +- `[JsonPropertyName("camelCase")]` for JSON +- `[XmlTag("PascalCase")]` for XML +- Both respect Choice Generator conventions + +## Test Coverage + +These models are used across multiple test classes: +- **ChoiceTypeCreationTests** - Factory methods and implicit operators +- **ChoiceTypeSwitchMatchTests** - Match/Switch behavior +- **ChoiceSerializationJsonTests** - JSON serialization round-trips +- **ChoiceSerializationXmlTests** - XML serialization round-trips diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/SearchCriteria.cs b/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/SearchCriteria.cs new file mode 100644 index 0000000..8fddc84 --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/SearchCriteria.cs @@ -0,0 +1,73 @@ +using System; +using System.Text.Json.Serialization; +using AltaSoft.Choice; + +namespace AltaSoft.ChoiceGenerator.Tests.TestModels; + +/// +/// Represents search criteria for a product catalog +/// Tests choices with different value types +/// +[Choice] +public sealed partial class SearchCriteria +{ + /// + /// Search by keyword (string) + /// + [XmlTag("Keyword")] + [JsonPropertyName("keyword")] + public partial string? Keyword { get; set; } + + /// + /// Search by category ID (int) + /// + [XmlTag("CategoryId")] + [JsonPropertyName("categoryId")] + public partial int? CategoryId { get; set; } + + /// + /// Search by date range + /// + [XmlTag("DateRange")] + [JsonPropertyName("dateRange")] + public partial DateRange? DateRange { get; set; } + + /// + /// Search by price range + /// + [XmlTag("PriceRange")] + [JsonPropertyName("priceRange")] + public partial PriceRange? PriceRange { get; set; } +} + +/// +/// Date range for filtering +/// +public sealed class DateRange +{ + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + + public DateRange() { } + + public DateRange(DateTime startDate, DateTime endDate) + { + StartDate = startDate; + EndDate = endDate; + } +} + +/// +/// Price range for filtering (struct to test value type behavior) +/// +public struct PriceRange +{ + public decimal MinPrice { get; set; } + public decimal MaxPrice { get; set; } + + public PriceRange(decimal minPrice, decimal maxPrice) + { + MinPrice = minPrice; + MaxPrice = maxPrice; + } +} diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/ShippingOption.cs b/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/ShippingOption.cs new file mode 100644 index 0000000..144710e --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/TestModels/ShippingOption.cs @@ -0,0 +1,53 @@ +using System; +using System.Text.Json.Serialization; +using AltaSoft.Choice; + +namespace AltaSoft.ChoiceGenerator.Tests.TestModels; + +/// +/// Represents shipping options for order delivery +/// Tests multiple choices of the same type (ShippingDetails) +/// +[Choice] +public sealed partial class ShippingOption +{ + /// + /// Standard shipping (5-7 business days) + /// + [XmlTag("Standard")] + [JsonPropertyName("standard")] + public partial ShippingDetails? Standard { get; set; } + + /// + /// Express shipping (2-3 business days) + /// + [XmlTag("Express")] + [JsonPropertyName("express")] + public partial ShippingDetails? Express { get; set; } + + /// + /// Overnight shipping (next business day) + /// + [XmlTag("Overnight")] + [JsonPropertyName("overnight")] + public partial ShippingDetails? Overnight { get; set; } +} + +/// +/// Shipping details including cost and estimated delivery +/// +public sealed class ShippingDetails +{ + public decimal Cost { get; set; } + public int EstimatedDays { get; set; } + public string Carrier { get; set; } = string.Empty; + + public ShippingDetails() { } + + public ShippingDetails(decimal cost, int estimatedDays, string carrier) + { + Cost = cost; + EstimatedDays = estimatedDays; + Carrier = carrier; + } +}