From f32f8e59b26ab0cf7465faa4ef4705099de8a4ad Mon Sep 17 00:00:00 2001 From: Gregory Nikolaishvili Date: Sun, 2 Aug 2026 01:55:39 +0400 Subject: [PATCH] Add XML namespace support for Choice properties - Support XmlTag.Namespace for per-property XML namespaces - Emit [XmlElement(..., Namespace=...)] for correct XML output - Serialize only the active choice property (no xsi:nil) - Add advanced XML serialization tests with namespaces - Add envelope and document models for test coverage - Update tests for mixed XmlTag/XmlElement and namespaces - Bump version to 2.2.0 --- Directory.Build.props | 4 +- src/AltaSoft.Choice.Generator/Executor.cs | 39 +- .../Models/PropertyDetails.cs | 7 + src/AltaSoft.Choice/XmlTagAttribute.cs | 8 + .../ChoiceGeneratorTest.cs | 92 ++- ...rrectly#Authorisation1Choice.g.verified.cs | 13 +- ...ayInChoice#ArrayInTypeChoice.g.verified.cs | 18 + ...Element#MixedAttributeChoice.g.verified.cs | 223 +++++++ ...Namespace#XmlNamespaceChoice.g.verified.cs | 173 ++++++ ...rrectly#Authorisation1Choice.g.verified.cs | 13 +- .../AdvancedXml/AccountReport.cs | 29 + .../AdvancedXml/ApplicationHeader.cs | 25 + .../AdvancedXml/CustomerData.cs | 33 + .../AdvancedXml/MessageEnvelope.cs | 39 ++ .../AdvancedXml/PaymentInstruction.cs | 33 + .../AdvancedXmlNamespaceTests.cs | 564 ++++++++++++++++++ .../ChoiceGeneratorTests.cs | 66 ++ .../XmlChoiceSerializationTests.cs | 427 +++++++++++++ .../XmlNamespaceChoice.cs | 13 + .../XmlNamespaceNilTests.cs | 139 +++++ 20 files changed, 1939 insertions(+), 19 deletions(-) create mode 100644 tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithMixedXmlTagAndXmlElement#MixedAttributeChoice.g.verified.cs create mode 100644 tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithXmlTagNamespace#XmlNamespaceChoice.g.verified.cs create mode 100644 tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXml/AccountReport.cs create mode 100644 tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXml/ApplicationHeader.cs create mode 100644 tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXml/CustomerData.cs create mode 100644 tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXml/MessageEnvelope.cs create mode 100644 tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXml/PaymentInstruction.cs create mode 100644 tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXmlNamespaceTests.cs create mode 100644 tests/AltaSoft.ChoiceGenerator.Tests/XmlChoiceSerializationTests.cs create mode 100644 tests/AltaSoft.ChoiceGenerator.Tests/XmlNamespaceChoice.cs create mode 100644 tests/AltaSoft.ChoiceGenerator.Tests/XmlNamespaceNilTests.cs diff --git a/Directory.Build.props b/Directory.Build.props index e5035ea..ab22976 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,8 +9,8 @@ ALTA Software llc. Choice generator ALTA Software llc. - Copyright © 2024 ALTA Software llc. - 2.1.5 + Copyright © 2024-2026 ALTA Software llc. + 2.2.0 diff --git a/src/AltaSoft.Choice.Generator/Executor.cs b/src/AltaSoft.Choice.Generator/Executor.cs index 2e82cc1..924f829 100644 --- a/src/AltaSoft.Choice.Generator/Executor.cs +++ b/src/AltaSoft.Choice.Generator/Executor.cs @@ -118,7 +118,14 @@ private static SourceCodeBuilder Process(INamedTypeSymbol typeSymbol, List x.TypeSymbol.IsValueType && !x.IsDateOnly()).Select(x => x.Name)) + // Generate ShouldSerialize methods for all properties to prevent xsi:nil in XML + // This ensures that only the active choice property is serialized + foreach (var p in processedProperties) { - sb.AppendSummary($"Determines whether the property should be serialized.") - .AppendBlock("returns", $"true if has a value; otherwise, false."); + sb.AppendSummary($"Determines whether the property should be serialized.") + .AppendBlock("returns", $"true if is the active choice; otherwise, false."); sb.AppendLine("[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]"); - sb.Append("public bool ShouldSerialize").Append(p).Append("() => "); + sb.Append("public bool ShouldSerialize").Append(p.Name).Append("() => "); + if (isOnly1Property) sb.AppendLine("true;"); else - sb.Append(p).AppendLine(".HasValue;"); + sb.Append("ChoiceType == ChoiceOf.").Append(p.Name).AppendLine(";"); + sb.NewLine(); } @@ -222,6 +238,14 @@ private static PropertyDetails ProcessProperty(IPropertySymbol propertySymbol) var xmlTagAttribute = propertySymbol.GetAttributes().FirstOrDefault(x => x.AttributeClass?.ToDisplayString() == Constants.XmlTagAttributeFullName); var xmlElementName = (string?)xmlTagAttribute?.ConstructorArguments[0].Value ?? propertySymbol.Name; + // Read the Namespace property from XmlTagAttribute if present + string? xmlNamespace = null; + var namespaceProperty = xmlTagAttribute?.NamedArguments.FirstOrDefault(x => x.Key == "Namespace"); + if (namespaceProperty?.Value.Value is string ns) + { + xmlNamespace = ns; + } + var typeFullName = propertySymbol.Type.GetFullName(); var propertyName = propertySymbol.Name; var modifiers = propertySymbol.GetModifiers(); @@ -231,6 +255,7 @@ private static PropertyDetails ProcessProperty(IPropertySymbol propertySymbol) typeName: typeFullName.Replace("?", ""), @namespace: propertySymbol.ContainingNamespace.ToDisplayString(), xmlNameValue: xmlElementName, + xmlNamespace: xmlNamespace, modifiers: modifiers, summary: propertySymbol.GetSummaryText(), getterAccessibility: propertySymbol.GetMethod?.DeclaredAccessibility ?? Accessibility.NotApplicable, diff --git a/src/AltaSoft.Choice.Generator/Models/PropertyDetails.cs b/src/AltaSoft.Choice.Generator/Models/PropertyDetails.cs index ee696a3..740c996 100644 --- a/src/AltaSoft.Choice.Generator/Models/PropertyDetails.cs +++ b/src/AltaSoft.Choice.Generator/Models/PropertyDetails.cs @@ -25,6 +25,11 @@ internal sealed class PropertyDetails /// internal string XmlNameValue { get; private set; } + /// + /// The XML namespace for the property element. + /// + internal string? XmlNamespace { get; private set; } + /// /// The access modifiers for the property, such as "public", "private", etc. /// @@ -66,6 +71,7 @@ public PropertyDetails( string typeName, string @namespace, string xmlNameValue, + string? xmlNamespace, string modifiers, string? summary, Accessibility getterAccessibility, @@ -76,6 +82,7 @@ public PropertyDetails( TypeName = typeName; Namespace = @namespace; XmlNameValue = xmlNameValue; + XmlNamespace = xmlNamespace; Modifiers = modifiers; Summary = summary; GetterAccessibility = getterAccessibility; diff --git a/src/AltaSoft.Choice/XmlTagAttribute.cs b/src/AltaSoft.Choice/XmlTagAttribute.cs index f243614..14c3b40 100644 --- a/src/AltaSoft.Choice/XmlTagAttribute.cs +++ b/src/AltaSoft.Choice/XmlTagAttribute.cs @@ -16,6 +16,14 @@ public sealed class XmlTagAttribute : Attribute /// public string Tag { get; } + /// + /// Gets the XML namespace associated with the property. + /// + /// + /// A representing the XML namespace, or null if no namespace is specified. + /// + public string? Namespace { get; set; } + /// /// Initializes a new instance of the class with the specified XML tag value. /// diff --git a/tests/AltaSoft.Choice.Generator.SnapshotTests/ChoiceGeneratorTest.cs b/tests/AltaSoft.Choice.Generator.SnapshotTests/ChoiceGeneratorTest.cs index 7402982..099ef2e 100644 --- a/tests/AltaSoft.Choice.Generator.SnapshotTests/ChoiceGeneratorTest.cs +++ b/tests/AltaSoft.Choice.Generator.SnapshotTests/ChoiceGeneratorTest.cs @@ -107,29 +107,29 @@ public Task ChoiceTypeShouldNotGenerateImplicitMethodsAndCompileCorrectly() using System.Xml.Serialization; using AltaSoft.Choice; using TestNamespace.OtherNamespace; - + namespace TestNamespace { [Choice] public sealed partial class Authorisation1Choice { - + /// /// Specifies the authorisation, in a coded form. /// [XmlElement("Cd")] - + public partial string? Code { get; set; } - + /// /// Specifies the authorisation, in a free text form. /// [XmlElement("Prtry")] - + public partial Authorisation1Code? Proprietary { get; set; } } } - + namespace TestNamespace.OtherNamespace { public enum Authorisation1Code @@ -146,6 +146,86 @@ public enum Authorisation1Code }); } + [Fact] + public Task ChoiceTypeShouldGenerateWithXmlTagNamespace() + { + const string source = + """ + using System; + using System.Xml; + using System.Xml.Schema; + using System.Xml.Serialization; + using AltaSoft.Choice; + + namespace TestNamespace + { + [Choice] + public sealed partial class XmlNamespaceChoice + { + /// + /// Specifies the code with namespace. + /// + [XmlTag("Cd", Namespace = "urn:test:code")] + public partial string? Code { get; set; } + + /// + /// Specifies the proprietary value with namespace. + /// + [XmlTag("Prtry", Namespace = "urn:test:proprietary")] + public partial string? Proprietary { get; set; } + } + } + """; + + return TestHelper.Verify(source, (_, x, _) => + { + Assert.Single(x); + }); + } + + [Fact] + public Task ChoiceTypeShouldGenerateWithMixedXmlTagAndXmlElement() + { + const string source = + """ + using System; + using System.Xml; + using System.Xml.Schema; + using System.Xml.Serialization; + using AltaSoft.Choice; + + namespace TestNamespace + { + [Choice] + public sealed partial class MixedAttributeChoice + { + /// + /// Code with XmlTag and namespace. + /// + [XmlTag("Cd", Namespace = "urn:test:code")] + public partial string? Code { get; set; } + + /// + /// Proprietary with standard XmlElement. + /// + [XmlElement("Prtry")] + public partial string? Proprietary { get; set; } + + /// + /// Amount with XmlTag but no namespace. + /// + [XmlTag("Amt")] + public partial decimal? Amount { 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 bb31efb..0b26c26 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 @@ -168,10 +168,19 @@ public void Switch( /// Determines whether the property should be serialized. /// /// - /// true if has a value; otherwise, false. + /// true if is the active choice; otherwise, false. /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - public bool ShouldSerializeCode() => Code.HasValue; + public bool ShouldSerializeCode() => ChoiceType == ChoiceOf.Code; + + /// + /// Determines whether the property should be serialized. + /// + /// + /// true if is the active choice; otherwise, false. + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeProprietary() => ChoiceType == ChoiceOf.Proprietary; /// /// Choice enumeration 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 654e6f1..909e2d9 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 @@ -158,6 +158,24 @@ public void Switch( return value is null ? null : CreateAsAccounts(value); } + /// + /// Determines whether the property should be serialized. + /// + /// + /// true if is the active choice; otherwise, false. + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeStringChoice() => ChoiceType == ChoiceOf.StringChoice; + + /// + /// Determines whether the property should be serialized. + /// + /// + /// true if is the active choice; otherwise, false. + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeAccounts() => ChoiceType == ChoiceOf.Accounts; + /// /// Choice enumeration /// 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 new file mode 100644 index 0000000..8574cc6 --- /dev/null +++ b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithMixedXmlTagAndXmlElement#MixedAttributeChoice.g.verified.cs @@ -0,0 +1,223 @@ +//HintName: MixedAttributeChoice.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 + +public sealed partial class MixedAttributeChoice +{ + /// + /// Constructor for Serialization/Deserialization + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public MixedAttributeChoice() + { + } + + /// + /// Choice enum + /// + [JsonIgnore] + [XmlIgnore] + [ChoiceTypeProperty] + public ChoiceOf ChoiceType { get; private set; } + + private string? _code; + + /// + /// Code with XmlTag and namespace. + /// + [DisallowNull] + [XmlElement("Cd", Namespace = "urn:test:code")] + [ChoiceProperty] + public partial string? Code + { + get => _code; + set + { + _code = value ?? throw new InvalidOperationException("Choice value cannot be null"); + _proprietary = null; + _amount = null; + ChoiceType = ChoiceOf.Code; + } + } + + private string? _proprietary; + + /// + /// Proprietary with standard XmlElement. + /// + [DisallowNull] + [XmlElement("Proprietary")] + [ChoiceProperty] + public partial string? Proprietary + { + get => _proprietary; + set + { + _proprietary = value ?? throw new InvalidOperationException("Choice value cannot be null"); + _code = null; + _amount = null; + ChoiceType = ChoiceOf.Proprietary; + } + } + + private decimal? _amount; + + /// + /// Amount with XmlTag but no namespace. + /// + [DisallowNull] + [XmlElement("Amt")] + [ChoiceProperty] + public partial decimal? Amount + { + get => _amount; + set + { + _amount = value ?? throw new InvalidOperationException("Choice value cannot be null"); + _code = null; + _proprietary = null; + ChoiceType = ChoiceOf.Amount; + } + } + + + /// + /// Creates a new instance and sets its value using the specified . + /// + /// The value to assign to the created choice instance. + public static TestNamespace.MixedAttributeChoice CreateAsCode(string value) => new () { Code = value }; + + /// + /// Creates a new instance and sets its value using the specified . + /// + /// The value to assign to the created choice instance. + public static TestNamespace.MixedAttributeChoice CreateAsProprietary(string value) => new () { Proprietary = value }; + + /// + /// Creates a new instance and sets its value using the specified . + /// + /// The value to assign to the created choice instance. + public static TestNamespace.MixedAttributeChoice CreateAsAmount(decimal value) => new () { Amount = 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 + /// Function to invoke if the choice is a value + public TResult Match( + Func matchCode, + Func matchProprietary, + Func matchAmount) + { + return ChoiceType switch + { + ChoiceOf.Code => matchCode(Code!), + ChoiceOf.Proprietary => matchProprietary(Proprietary!), + ChoiceOf.Amount => matchAmount(Amount!.Value), + _ => 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 + /// Action to invoke if the choice is a value + public void Switch( + Action matchCode, + Action matchProprietary, + Action matchAmount) + { + switch (ChoiceType) + { + case ChoiceOf.Code: + matchCode(Code!); + return; + + case ChoiceOf.Proprietary: + matchProprietary(Proprietary!); + return; + + case ChoiceOf.Amount: + matchAmount(Amount!.Value); + 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 ShouldSerializeCode() => ChoiceType == ChoiceOf.Code; + + /// + /// Determines whether the property should be serialized. + /// + /// + /// true if is the active choice; otherwise, false. + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeProprietary() => ChoiceType == ChoiceOf.Proprietary; + + /// + /// Determines whether the property should be serialized. + /// + /// + /// true if is the active choice; otherwise, false. + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeAmount() => ChoiceType == ChoiceOf.Amount; + + /// + /// Choice enumeration + /// + [XmlType("ChoiceOf.MixedAttributeChoice")] + public enum ChoiceOf + { + /// + /// Code with XmlTag and namespace. + /// + Code, + /// + /// Proprietary with standard XmlElement. + /// + Proprietary, + /// + /// Amount with XmlTag but no namespace. + /// + Amount, + } +} 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 new file mode 100644 index 0000000..268ab64 --- /dev/null +++ b/tests/AltaSoft.Choice.Generator.SnapshotTests/Snapshots/ChoiceGeneratorTest.ChoiceTypeShouldGenerateWithXmlTagNamespace#XmlNamespaceChoice.g.verified.cs @@ -0,0 +1,173 @@ +//HintName: XmlNamespaceChoice.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 + +public sealed partial class XmlNamespaceChoice +{ + /// + /// Constructor for Serialization/Deserialization + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public XmlNamespaceChoice() + { + } + + /// + /// Choice enum + /// + [JsonIgnore] + [XmlIgnore] + [ChoiceTypeProperty] + public ChoiceOf ChoiceType { get; private set; } + + private string? _code; + + /// + /// Specifies the code with namespace. + /// + [DisallowNull] + [XmlElement("Cd", Namespace = "urn:test:code")] + [ChoiceProperty] + public partial string? Code + { + get => _code; + set + { + _code = value ?? throw new InvalidOperationException("Choice value cannot be null"); + _proprietary = null; + ChoiceType = ChoiceOf.Code; + } + } + + private string? _proprietary; + + /// + /// Specifies the proprietary value with namespace. + /// + [DisallowNull] + [XmlElement("Prtry", Namespace = "urn:test:proprietary")] + [ChoiceProperty] + public partial string? Proprietary + { + get => _proprietary; + set + { + _proprietary = value ?? throw new InvalidOperationException("Choice value cannot be null"); + _code = null; + ChoiceType = ChoiceOf.Proprietary; + } + } + + + /// + /// Creates a new instance and sets its value using the specified . + /// + /// The value to assign to the created choice instance. + public static TestNamespace.XmlNamespaceChoice CreateAsCode(string value) => new () { Code = value }; + + /// + /// Creates a new instance and sets its value using the specified . + /// + /// The value to assign to the created choice instance. + public static TestNamespace.XmlNamespaceChoice CreateAsProprietary(string value) => new () { Proprietary = 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 matchCode, + Func matchProprietary) + { + return ChoiceType switch + { + ChoiceOf.Code => matchCode(Code!), + ChoiceOf.Proprietary => matchProprietary(Proprietary!), + _ => 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 matchCode, + Action matchProprietary) + { + switch (ChoiceType) + { + case ChoiceOf.Code: + matchCode(Code!); + return; + + case ChoiceOf.Proprietary: + matchProprietary(Proprietary!); + 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 ShouldSerializeCode() => ChoiceType == ChoiceOf.Code; + + /// + /// Determines whether the property should be serialized. + /// + /// + /// true if is the active choice; otherwise, false. + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeProprietary() => ChoiceType == ChoiceOf.Proprietary; + + /// + /// Choice enumeration + /// + [XmlType("ChoiceOf.XmlNamespaceChoice")] + public enum ChoiceOf + { + /// + /// Specifies the code with namespace. + /// + Code, + /// + /// Specifies the proprietary value with namespace. + /// + Proprietary, + } +} 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 4ed06af..35cb294 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 @@ -164,14 +164,23 @@ public void Switch( return value is null ? null : CreateAsProprietary(value.Value); } + /// + /// Determines whether the property should be serialized. + /// + /// + /// true if is the active choice; otherwise, false. + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public bool ShouldSerializeCode() => ChoiceType == ChoiceOf.Code; + /// /// Determines whether the property should be serialized. /// /// - /// true if has a value; otherwise, false. + /// true if is the active choice; otherwise, false. /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] - public bool ShouldSerializeProprietary() => Proprietary.HasValue; + public bool ShouldSerializeProprietary() => ChoiceType == ChoiceOf.Proprietary; /// /// Choice enumeration diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXml/AccountReport.cs b/tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXml/AccountReport.cs new file mode 100644 index 0000000..bcd6ae8 --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXml/AccountReport.cs @@ -0,0 +1,29 @@ +using System; +using System.Text.Json.Serialization; +using System.Xml.Serialization; + +namespace AltaSoft.ChoiceGenerator.Tests.AdvancedXml; + +/// +/// Account report document +/// +[XmlRoot(ElementName = "AcctRpt", Namespace = "urn:test:account")] +[Serializable] +public sealed record AccountReport +{ + [XmlElement("RptId")] + [JsonPropertyName("rpt_id")] + public string? ReportId { get; set; } + + [XmlElement("AcctId")] + [JsonPropertyName("acct_id")] + public string? AccountId { get; set; } + + [XmlElement("Bal")] + [JsonPropertyName("bal")] + public decimal Balance { get; set; } + + [XmlElement("BalDt")] + [JsonPropertyName("bal_dt")] + public DateTime BalanceDate { get; set; } +} diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXml/ApplicationHeader.cs b/tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXml/ApplicationHeader.cs new file mode 100644 index 0000000..1b60518 --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXml/ApplicationHeader.cs @@ -0,0 +1,25 @@ +using System; +using System.Text.Json.Serialization; +using System.Xml.Serialization; + +namespace AltaSoft.ChoiceGenerator.Tests.AdvancedXml; + +/// +/// Application header with custom XML namespace +/// +[XmlRoot(ElementName = "AppHdr", Namespace = "urn:test:head")] +[Serializable] +public sealed record ApplicationHeader +{ + [XmlElement("BizMsgIdr")] + [JsonPropertyName("biz_msg_idr")] + public string? BusinessMessageIdentifier { get; set; } + + [XmlElement("CreDtTm")] + [JsonPropertyName("cre_dt_tm")] + public DateTime CreationDateTime { get; set; } + + [XmlElement("MsgDefIdr")] + [JsonPropertyName("msg_def_idr")] + public string? MessageDefinitionIdentifier { get; set; } +} diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXml/CustomerData.cs b/tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXml/CustomerData.cs new file mode 100644 index 0000000..4bea297 --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXml/CustomerData.cs @@ -0,0 +1,33 @@ +using System; +using System.Text.Json.Serialization; +using System.Xml.Serialization; + +namespace AltaSoft.ChoiceGenerator.Tests.AdvancedXml; + +/// +/// Customer data document +/// +[XmlRoot(ElementName = "CstmrData", Namespace = "urn:test:customer")] +[Serializable] +public sealed record CustomerData +{ + [XmlElement("CstmrId")] + [JsonPropertyName("cstmr_id")] + public string? CustomerId { get; set; } + + [XmlElement("Nm")] + [JsonPropertyName("nm")] + public string? Name { get; set; } + + [XmlElement("Email")] + [JsonPropertyName("email")] + public string? Email { get; set; } + + [XmlElement("PhoneNb")] + [JsonPropertyName("phone_nb")] + public string? PhoneNumber { get; set; } + + [XmlElement("Ctry")] + [JsonPropertyName("ctry")] + public string? Country { get; set; } +} diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXml/MessageEnvelope.cs b/tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXml/MessageEnvelope.cs new file mode 100644 index 0000000..8fc606d --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXml/MessageEnvelope.cs @@ -0,0 +1,39 @@ +using System; +using System.Text.Json.Serialization; +using System.Xml.Serialization; + +namespace AltaSoft.ChoiceGenerator.Tests.AdvancedXml; + +/// +/// Envelope containing header and business document choice +/// +[XmlRoot(ElementName = "Envelope", Namespace = "urn:test:envelope")] +[Serializable] +[Choice.Choice] +public sealed partial record MessageEnvelope +{ + /// + /// Application header + /// + [XmlElement("AppHdr", Namespace = "urn:test:head")] + [JsonPropertyName("app_hdr")] + public ApplicationHeader? Header { get; set; } + + /// + /// Payment instruction document + /// + [Choice.XmlTag("PmtInstr", Namespace = "urn:test:payment")] + public partial PaymentInstruction? Payment { get; set; } + + /// + /// Account report document + /// + [Choice.XmlTag("AcctRpt", Namespace = "urn:test:account")] + public partial AccountReport? Account { get; set; } + + /// + /// Customer data document + /// + [Choice.XmlTag("CstmrData", Namespace = "urn:test:customer")] + public partial CustomerData? Customer { get; set; } +} diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXml/PaymentInstruction.cs b/tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXml/PaymentInstruction.cs new file mode 100644 index 0000000..a00eb6d --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXml/PaymentInstruction.cs @@ -0,0 +1,33 @@ +using System; +using System.Text.Json.Serialization; +using System.Xml.Serialization; + +namespace AltaSoft.ChoiceGenerator.Tests.AdvancedXml; + +/// +/// Payment instruction document +/// +[XmlRoot(ElementName = "PmtInstr", Namespace = "urn:test:payment")] +[Serializable] +public sealed record PaymentInstruction +{ + [XmlElement("InstrId")] + [JsonPropertyName("instr_id")] + public string? InstructionId { get; set; } + + [XmlElement("Amt")] + [JsonPropertyName("amt")] + public decimal Amount { get; set; } + + [XmlElement("Ccy")] + [JsonPropertyName("ccy")] + public string? Currency { get; set; } + + [XmlElement("DbtrNm")] + [JsonPropertyName("dbtr_nm")] + public string? DebtorName { get; set; } + + [XmlElement("CdtrNm")] + [JsonPropertyName("cdtr_nm")] + public string? CreditorName { get; set; } +} diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXmlNamespaceTests.cs b/tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXmlNamespaceTests.cs new file mode 100644 index 0000000..71d8db4 --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/AdvancedXmlNamespaceTests.cs @@ -0,0 +1,564 @@ +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; +using AltaSoft.Choice; +using AltaSoft.ChoiceGenerator.Tests.AdvancedXml; +using Xunit; +using Xunit.Abstractions; + +namespace AltaSoft.ChoiceGenerator.Tests; + +/// +/// Comprehensive tests for XML serialization with namespaces and Choice types in envelope structures. +/// Tests verify that xsi:nil attributes are NOT generated and only the active choice is serialized. +/// +public class AdvancedXmlNamespaceTests +{ + private readonly ITestOutputHelper _output; + + private static readonly XmlWriterSettings s_xmlWriterSettings = new() + { + OmitXmlDeclaration = true, + Indent = true + }; + + public AdvancedXmlNamespaceTests(ITestOutputHelper output) + { + _output = output; + } + + #region Payment Document Tests + + [Fact] + public void XmlSerialization_Envelope_WithPaymentDocument_ShouldRoundTrip() + { + const string expectedXml = """ + + + MSG-12345 + 2024-01-15T10:30:00 + pacs.008.001.08 + + + PMT-001 + 1500.50 + USD + John Doe + Jane Smith + + + """; + + var serializer = new XmlSerializer(typeof(MessageEnvelope)); + + // Deserialize + using var reader = new StringReader(expectedXml); + var envelope = (MessageEnvelope)serializer.Deserialize(reader)!; + + // Validate Header + Assert.NotNull(envelope); + Assert.NotNull(envelope.Header); + Assert.Equal("MSG-12345", envelope.Header.BusinessMessageIdentifier); + Assert.Equal(new DateTime(2024, 1, 15, 10, 30, 0), envelope.Header.CreationDateTime); + Assert.Equal("pacs.008.001.08", envelope.Header.MessageDefinitionIdentifier); + + // Validate Document Choice + Assert.Equal(MessageEnvelope.ChoiceOf.Payment, envelope.ChoiceType); + Assert.NotNull(envelope.Payment); + Assert.Null(envelope.Account); + Assert.Null(envelope.Customer); + + // Validate Payment Details + var payment = envelope.Payment; + Assert.Equal("PMT-001", payment.InstructionId); + Assert.Equal(1500.50m, payment.Amount); + Assert.Equal("USD", payment.Currency); + Assert.Equal("John Doe", payment.DebtorName); + Assert.Equal("Jane Smith", payment.CreditorName); + + // Serialize back + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, envelope); + + var actualXml = sw.ToString(); + Assert.Equal(expectedXml, actualXml); + } + + [Fact] + public void XmlSerialization_Envelope_CreatePaymentDocument_ShouldSerializeCorrectly() + { + var payment = new PaymentInstruction + { + InstructionId = "PMT-999", + Amount = 5000.00m, + Currency = "EUR", + DebtorName = "ACME Corp", + CreditorName = "Supplier Ltd" + }; + + var envelope = MessageEnvelope.CreateAsPayment(payment); + envelope.Header = new ApplicationHeader + { + BusinessMessageIdentifier = "MSG-999", + CreationDateTime = new DateTime(2024, 12, 25, 14, 0, 0), + MessageDefinitionIdentifier = "pacs.008.001.10" + }; + + var serializer = new XmlSerializer(typeof(MessageEnvelope)); + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, envelope); + + var xml = sw.ToString(); + + _output.WriteLine("Generated Payment XML:"); + _output.WriteLine(xml); + + // Validate key elements presence + Assert.Contains("", xml); + Assert.Contains("MSG-999", xml); + Assert.Contains("", xml); + Assert.Contains("PMT-999", xml); + Assert.Contains("5000.00", xml); + Assert.Contains("EUR", xml); + + // KEY ASSERTIONS: No xsi:nil and no other document types + Assert.DoesNotContain("xsi:nil=\"true\"", xml); + Assert.DoesNotContain("", xml); + Assert.Contains("PMT-VERIFY", xml); + + // Verify Account and Customer are NOT present (no xsi:nil) + Assert.DoesNotContain("AcctRpt", xml); + Assert.DoesNotContain("CstmrData", xml); + Assert.DoesNotContain("xsi:nil", xml); + } + + #endregion + + #region Account Document Tests + + [Fact] + public void XmlSerialization_Envelope_WithAccountDocument_ShouldRoundTrip() + { + const string expectedXml = """ + + + MSG-ACCT-001 + 2024-06-15T09:00:00 + camt.053.001.08 + + + RPT-2024-001 + ACC-123456 + 25000.75 + 2024-06-15T00:00:00 + + + """; + + var serializer = new XmlSerializer(typeof(MessageEnvelope)); + + // Deserialize + using var reader = new StringReader(expectedXml); + var envelope = (MessageEnvelope)serializer.Deserialize(reader)!; + + // Validate Header + Assert.NotNull(envelope); + Assert.NotNull(envelope.Header); + Assert.Equal("MSG-ACCT-001", envelope.Header.BusinessMessageIdentifier); + Assert.Equal("camt.053.001.08", envelope.Header.MessageDefinitionIdentifier); + + // Validate Document Choice + Assert.Equal(MessageEnvelope.ChoiceOf.Account, envelope.ChoiceType); + Assert.Null(envelope.Payment); + Assert.NotNull(envelope.Account); + Assert.Null(envelope.Customer); + + // Validate Account Details + var account = envelope.Account; + Assert.Equal("RPT-2024-001", account.ReportId); + Assert.Equal("ACC-123456", account.AccountId); + Assert.Equal(25000.75m, account.Balance); + Assert.Equal(new DateTime(2024, 6, 15), account.BalanceDate); + + // Serialize back + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, envelope); + + var actualXml = sw.ToString(); + Assert.Equal(expectedXml, actualXml); + } + + [Fact] + public void XmlSerialization_Envelope_CreateAccountDocument_ShouldSerializeCorrectly() + { + var account = new AccountReport + { + ReportId = "RPT-TEST", + AccountId = "ACC-999", + Balance = 100000.00m, + BalanceDate = new DateTime(2024, 12, 31) + }; + + var envelope = MessageEnvelope.CreateAsAccount(account); + envelope.Header = new ApplicationHeader + { + BusinessMessageIdentifier = "MSG-TEST", + CreationDateTime = new DateTime(2024, 11, 1, 10, 30, 0), + MessageDefinitionIdentifier = "camt.053.001.10" + }; + + var serializer = new XmlSerializer(typeof(MessageEnvelope)); + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, envelope); + + var xml = sw.ToString(); + + _output.WriteLine("Generated Account XML:"); + _output.WriteLine(xml); + + // Verify Account is present with correct namespace + Assert.Contains("", xml); + Assert.Contains("RPT-TEST", xml); + Assert.Contains("ACC-999", xml); + Assert.Contains("100000.00", xml); + + // KEY ASSERTIONS: No xsi:nil and no other document types + Assert.DoesNotContain("xsi:nil=\"true\"", xml); + Assert.DoesNotContain(" + + MSG-CUST-001 + 2024-03-20T14:30:00 + acmt.001.001.06 + + + CUST-789 + Alice Johnson + alice.johnson@example.com + +1-555-0123 + USA + + + """; + + var serializer = new XmlSerializer(typeof(MessageEnvelope)); + + // Deserialize + using var reader = new StringReader(expectedXml); + var envelope = (MessageEnvelope)serializer.Deserialize(reader)!; + + // Validate Header + Assert.NotNull(envelope); + Assert.NotNull(envelope.Header); + Assert.Equal("MSG-CUST-001", envelope.Header.BusinessMessageIdentifier); + Assert.Equal("acmt.001.001.06", envelope.Header.MessageDefinitionIdentifier); + + // Validate Document Choice + Assert.Equal(MessageEnvelope.ChoiceOf.Customer, envelope.ChoiceType); + Assert.Null(envelope.Payment); + Assert.Null(envelope.Account); + Assert.NotNull(envelope.Customer); + + // Validate Customer Details + var customer = envelope.Customer; + Assert.Equal("CUST-789", customer.CustomerId); + Assert.Equal("Alice Johnson", customer.Name); + Assert.Equal("alice.johnson@example.com", customer.Email); + Assert.Equal("+1-555-0123", customer.PhoneNumber); + Assert.Equal("USA", customer.Country); + + // Serialize back + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, envelope); + + var actualXml = sw.ToString(); + Assert.Equal(expectedXml, actualXml); + } + + [Fact] + public void XmlSerialization_Envelope_CreateCustomerDocument_ShouldSerializeCorrectly() + { + var customer = new CustomerData + { + CustomerId = "CUST-XYZ", + Name = "Bob Williams", + Email = "bob@company.com", + PhoneNumber = "+44-20-1234-5678", + Country = "UK" + }; + + var envelope = MessageEnvelope.CreateAsCustomer(customer); + envelope.Header = new ApplicationHeader + { + BusinessMessageIdentifier = "MSG-BOB", + CreationDateTime = new DateTime(2024, 11, 1, 8, 0, 0), + MessageDefinitionIdentifier = "acmt.002.001.06" + }; + + var serializer = new XmlSerializer(typeof(MessageEnvelope)); + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, envelope); + + var xml = sw.ToString(); + + _output.WriteLine("Generated Customer XML:"); + _output.WriteLine(xml); + + // Verify Customer is present with correct namespace + Assert.Contains("", xml); + Assert.Contains("CUST-XYZ", xml); + Assert.Contains("Bob Williams", xml); + Assert.Contains("bob@company.com", xml); + + // KEY ASSERTIONS: No xsi:nil and no other document types + Assert.DoesNotContain("xsi:nil=\"true\"", xml); + Assert.DoesNotContain(" $"Payment: {payment.InstructionId}", + account => $"Account: {account.AccountId}", + customer => $"Customer: {customer.Name}" + ); + + Assert.Equal("Payment: PMT-1", result); + + var accountEnvelope = MessageEnvelope.CreateAsAccount(new AccountReport + { + AccountId = "ACC-123", + Balance = 1000m + }); + + result = accountEnvelope.Match( + payment => $"Payment: {payment.InstructionId}", + account => $"Account: {account.AccountId}", + customer => $"Customer: {customer.Name}" + ); + + Assert.Equal("Account: ACC-123", result); + + var customerEnvelope = MessageEnvelope.CreateAsCustomer(new CustomerData + { + Name = "Test User" + }); + + result = customerEnvelope.Match( + payment => $"Payment: {payment.InstructionId}", + account => $"Account: {account.AccountId}", + customer => $"Customer: {customer.Name}" + ); + + Assert.Equal("Customer: Test User", result); + } + + #endregion + + #region Namespace Validation Tests + + [Fact] + public void XmlSerialization_Envelope_AllNamespacesShouldBeCorrect() + { + var envelope = MessageEnvelope.CreateAsPayment(new PaymentInstruction + { + InstructionId = "PMT-NS", + Amount = 50m, + Currency = "GBP" + }); + envelope.Header = new ApplicationHeader + { + BusinessMessageIdentifier = "NS-TEST", + CreationDateTime = new DateTime(2024, 1, 1), + MessageDefinitionIdentifier = "ns.test" + }; + + var serializer = new XmlSerializer(typeof(MessageEnvelope)); + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, envelope); + + var xml = sw.ToString(); + + _output.WriteLine("Namespace validation XML:"); + _output.WriteLine(xml); + + // Validate all namespaces are present and correct + Assert.Contains("xmlns=\"urn:test:envelope\"", xml); + Assert.Contains("xmlns=\"urn:test:head\"", xml); + Assert.Contains("xmlns=\"urn:test:payment\"", xml); + + // Validate namespace prefixes are not mixed + Assert.Contains("", xml); + Assert.Contains("", xml); + + // Ensure only Payment namespace is present, not others + Assert.DoesNotContain("urn:test:account", xml); + Assert.DoesNotContain("urn:test:customer", xml); + } + + [Fact] + public void XmlSerialization_Envelope_EachDocumentType_HasCorrectNamespace() + { + // Test Payment namespace + var paymentEnv = MessageEnvelope.CreateAsPayment(new PaymentInstruction { InstructionId = "P1" }); + var xml1 = SerializeToString(paymentEnv); + _output.WriteLine("Payment namespace check:"); + _output.WriteLine(xml1); + Assert.Contains("xmlns=\"urn:test:payment\"", xml1); + Assert.DoesNotContain("urn:test:account", xml1); + Assert.DoesNotContain("urn:test:customer", xml1); + + // Test Account namespace + var accountEnv = MessageEnvelope.CreateAsAccount(new AccountReport { AccountId = "A1" }); + var xml2 = SerializeToString(accountEnv); + _output.WriteLine("\nAccount namespace check:"); + _output.WriteLine(xml2); + Assert.Contains("xmlns=\"urn:test:account\"", xml2); + Assert.DoesNotContain("urn:test:payment", xml2); + Assert.DoesNotContain("urn:test:customer", xml2); + + // Test Customer namespace + var customerEnv = MessageEnvelope.CreateAsCustomer(new CustomerData { CustomerId = "C1" }); + var xml3 = SerializeToString(customerEnv); + _output.WriteLine("\nCustomer namespace check:"); + _output.WriteLine(xml3); + Assert.Contains("xmlns=\"urn:test:customer\"", xml3); + Assert.DoesNotContain("urn:test:payment", xml3); + Assert.DoesNotContain("urn:test:account", xml3); + } + + #endregion + + #region Helper Methods + + private static string SerializeToString(MessageEnvelope envelope) + { + var serializer = new XmlSerializer(typeof(MessageEnvelope)); + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, envelope); + return sw.ToString(); + } + + #endregion +} + diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceGeneratorTests.cs b/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceGeneratorTests.cs index 2049c5c..f9b8601 100644 --- a/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceGeneratorTests.cs +++ b/tests/AltaSoft.ChoiceGenerator.Tests/ChoiceGeneratorTests.cs @@ -314,5 +314,71 @@ public void GenerateSinglePropertyChoice() var switched = choice.Match(x => "matched"); Assert.Equal("matched", switched); } + + [Fact] + public void XmlNamespaceChoice_ShouldRoundTripWithNamespaces() + { + const string expectedXml = """ + + TEST-CODE + + """; + + var serializer = new XmlSerializer(typeof(XmlNamespaceChoice)); + using var reader = new StringReader(expectedXml); + + var value = (XmlNamespaceChoice)serializer.Deserialize(reader)!; + + Assert.NotNull(value); + Assert.Equal(XmlNamespaceChoice.ChoiceOf.Code, value.ChoiceType); + Assert.Equal("TEST-CODE", value.Code); + Assert.Null(value.Proprietary); + + var settings = new XmlWriterSettings + { + OmitXmlDeclaration = true, + Indent = true + }; + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, settings); + + serializer.Serialize(writer, value, XmlNamespaceHelper.EmptyNamespace); + + var serializedXml = sw.ToString(); + Assert.Equal(expectedXml, serializedXml); + } + + [Fact] + public void XmlNamespaceChoice_Proprietary_ShouldSerializeWithNamespace() + { + const string expectedXml = """ + + PROPRIETARY-DATA + + """; + + var serializer = new XmlSerializer(typeof(XmlNamespaceChoice)); + using var reader = new StringReader(expectedXml); + + var value = (XmlNamespaceChoice)serializer.Deserialize(reader)!; + + Assert.NotNull(value); + Assert.Equal(XmlNamespaceChoice.ChoiceOf.Proprietary, value.ChoiceType); + Assert.Null(value.Code); + Assert.Equal("PROPRIETARY-DATA", value.Proprietary); + + var settings = new XmlWriterSettings + { + OmitXmlDeclaration = true, + Indent = true + }; + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, settings); + + serializer.Serialize(writer, value, XmlNamespaceHelper.EmptyNamespace); + + var serializedXml = sw.ToString(); + Assert.Equal(expectedXml, serializedXml); + } } diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/XmlChoiceSerializationTests.cs b/tests/AltaSoft.ChoiceGenerator.Tests/XmlChoiceSerializationTests.cs new file mode 100644 index 0000000..6bc9cf7 --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/XmlChoiceSerializationTests.cs @@ -0,0 +1,427 @@ +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; +using AltaSoft.Choice; +using AltaSoft.ChoiceGenerator.Tests.OtherNamespace; +using Xunit; + +namespace AltaSoft.ChoiceGenerator.Tests; + +/// +/// Comprehensive tests for XML serialization and deserialization of Choice types. +/// +public class XmlChoiceSerializationTests +{ + private static readonly XmlWriterSettings s_xmlWriterSettings = new() + { + OmitXmlDeclaration = true, + Indent = true + }; + + #region TwoDifferentTypeChoice Tests + + [Fact] + public void XmlSerialization_TwoDifferentTypeChoice_StringChoice_ShouldRoundTrip() + { + const string expectedXml = """ + + test value + + """; + + var serializer = new XmlSerializer(typeof(TwoDifferentTypeChoice)); + + // Deserialize + using var reader = new StringReader(expectedXml); + var choice = (TwoDifferentTypeChoice)serializer.Deserialize(reader)!; + + Assert.NotNull(choice); + Assert.Equal(TwoDifferentTypeChoice.ChoiceOf.StringChoice, choice.ChoiceType); + Assert.Equal("test value", choice.StringChoice); + Assert.Null(choice.IntChoice); + + // Serialize back + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, choice, XmlNamespaceHelper.EmptyNamespace); + + var actualXml = sw.ToString(); + Assert.Equal(expectedXml, actualXml); + } + + [Fact] + public void XmlSerialization_TwoDifferentTypeChoice_IntChoice_ShouldRoundTrip() + { + const string expectedXml = """ + + 42 + + """; + + var serializer = new XmlSerializer(typeof(TwoDifferentTypeChoice)); + + // Deserialize + using var reader = new StringReader(expectedXml); + var choice = (TwoDifferentTypeChoice)serializer.Deserialize(reader)!; + + Assert.NotNull(choice); + Assert.Equal(TwoDifferentTypeChoice.ChoiceOf.IntChoice, choice.ChoiceType); + Assert.Equal(42, choice.IntChoice); + Assert.Null(choice.StringChoice); + + // Serialize back + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, choice, XmlNamespaceHelper.EmptyNamespace); + + var actualXml = sw.ToString(); + Assert.Equal(expectedXml, actualXml); + } + + [Fact] + public void XmlSerialization_TwoDifferentTypeChoice_CreateAsStringChoice_ShouldSerializeCorrectly() + { + var choice = TwoDifferentTypeChoice.CreateAsStringChoice("xml test"); + + var serializer = new XmlSerializer(typeof(TwoDifferentTypeChoice)); + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, choice, XmlNamespaceHelper.EmptyNamespace); + + var xml = sw.ToString(); + Assert.Contains("xml test", xml); + Assert.DoesNotContain("", xml); + } + + #endregion + + #region TwoSameTypeChoice Tests + + [Fact] + public void XmlSerialization_TwoSameTypeChoice_StringChoiceOne_ShouldRoundTrip() + { + const string expectedXml = """ + + first + + """; + + var serializer = new XmlSerializer(typeof(TwoSameTypeChoice)); + + // Deserialize + using var reader = new StringReader(expectedXml); + var choice = (TwoSameTypeChoice)serializer.Deserialize(reader)!; + + Assert.NotNull(choice); + Assert.Equal(TwoSameTypeChoice.ChoiceOf.StringChoiceOne, choice.ChoiceType); + Assert.Equal("first", choice.StringChoiceOne); + Assert.Null(choice.StringChoiceTwo); + + // Serialize back + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, choice, XmlNamespaceHelper.EmptyNamespace); + + var actualXml = sw.ToString(); + Assert.Equal(expectedXml, actualXml); + } + + [Fact] + public void XmlSerialization_TwoSameTypeChoice_StringChoiceTwo_ShouldRoundTrip() + { + const string expectedXml = """ + + second + + """; + + var serializer = new XmlSerializer(typeof(TwoSameTypeChoice)); + + // Deserialize + using var reader = new StringReader(expectedXml); + var choice = (TwoSameTypeChoice)serializer.Deserialize(reader)!; + + Assert.NotNull(choice); + Assert.Equal(TwoSameTypeChoice.ChoiceOf.StringChoiceTwo, choice.ChoiceType); + Assert.Null(choice.StringChoiceOne); + Assert.Equal("second", choice.StringChoiceTwo); + + // Serialize back + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, choice, XmlNamespaceHelper.EmptyNamespace); + + var actualXml = sw.ToString(); + Assert.Equal(expectedXml, actualXml); + } + + #endregion + + #region TwoValueTypeChoice Tests + + [Fact] + public void XmlSerialization_TwoValueTypeChoice_Integer_ShouldRoundTrip() + { + const string expectedXml = """ + + 999 + + """; + + var serializer = new XmlSerializer(typeof(TwoValueTypeChoice)); + + // Deserialize + using var reader = new StringReader(expectedXml); + var choice = (TwoValueTypeChoice)serializer.Deserialize(reader)!; + + Assert.NotNull(choice); + Assert.Equal(TwoValueTypeChoice.ChoiceOf.Integer, choice.ChoiceType); + Assert.Equal(999, choice.Integer); + Assert.Null(choice.Code); + + // Serialize back + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, choice, XmlNamespaceHelper.EmptyNamespace); + + var actualXml = sw.ToString(); + Assert.Equal(expectedXml, actualXml); + } + + [Fact] + public void XmlSerialization_TwoValueTypeChoice_EnumCode_ShouldRoundTrip() + { + const string expectedXml = """ + + One + + """; + + var serializer = new XmlSerializer(typeof(TwoValueTypeChoice)); + + // Deserialize + using var reader = new StringReader(expectedXml); + var choice = (TwoValueTypeChoice)serializer.Deserialize(reader)!; + + Assert.NotNull(choice); + Assert.Equal(TwoValueTypeChoice.ChoiceOf.Code, choice.ChoiceType); + Assert.Equal(Authorisation1Code.One, choice.Code); + Assert.Null(choice.Integer); + + // Serialize back + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, choice, XmlNamespaceHelper.EmptyNamespace); + + var actualXml = sw.ToString(); + Assert.Equal(expectedXml, actualXml); + } + + #endregion + + #region Authorisation1Choice with XmlTag Tests + + [Fact] + public void XmlSerialization_Authorisation1Choice_WithCustomXmlTag_Code_ShouldRoundTrip() + { + const string expectedXml = """ + + One + + """; + + var serializer = new XmlSerializer(typeof(Authorisation1Choice)); + + // Deserialize + using var reader = new StringReader(expectedXml); + var choice = (Authorisation1Choice)serializer.Deserialize(reader)!; + + Assert.NotNull(choice); + Assert.Equal(Authorisation1Choice.ChoiceOf.Code, choice.ChoiceType); + Assert.Equal(Authorisation1Code.One, choice.Code); + Assert.Null(choice.Proprietary); + + // Serialize back + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, choice, XmlNamespaceHelper.EmptyNamespace); + + var actualXml = sw.ToString(); + Assert.Equal(expectedXml, actualXml); + } + + [Fact] + public void XmlSerialization_Authorisation1Choice_WithCustomXmlTag_Proprietary_ShouldRoundTrip() + { + const string expectedXml = """ + + + custom data + + + """; + + var serializer = new XmlSerializer(typeof(Authorisation1Choice)); + + // Deserialize + using var reader = new StringReader(expectedXml); + var choice = (Authorisation1Choice)serializer.Deserialize(reader)!; + + Assert.NotNull(choice); + Assert.Equal(Authorisation1Choice.ChoiceOf.Proprietary, choice.ChoiceType); + Assert.Null(choice.Code); + Assert.NotNull(choice.Proprietary); + Assert.Equal("custom data", choice.Proprietary.Other); + + // Serialize back + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, choice, XmlNamespaceHelper.EmptyNamespace); + + var actualXml = sw.ToString(); + Assert.Equal(expectedXml, actualXml); + } + + #endregion + + #region DateTypeChoice with DateOnly Tests + + [Fact] + public void XmlSerialization_DateTypeChoice_OnlyDate_ShouldRoundTrip() + { + const string expectedXml = """ + + 2024-12-25 + + """; + + var serializer = new XmlSerializer(typeof(DateTypeChoice)); + + // Deserialize + using var reader = new StringReader(expectedXml); + var choice = (DateTypeChoice)serializer.Deserialize(reader)!; + + Assert.NotNull(choice); + Assert.Equal(DateTypeChoice.ChoiceOf.OnlyDate, choice.ChoiceType); + Assert.Equal(new DateOnly(2024, 12, 25), choice.OnlyDate); + Assert.Null(choice.DateTimeChoice); + + // Serialize back + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, choice, XmlNamespaceHelper.EmptyNamespace); + + var actualXml = sw.ToString(); + Assert.Equal(expectedXml, actualXml); + } + + [Fact] + public void XmlSerialization_DateTypeChoice_DateTime_ShouldRoundTrip() + { + const string expectedXml = """ + + 2024-12-25T15:30:45 + + """; + + var serializer = new XmlSerializer(typeof(DateTypeChoice)); + + // Deserialize + using var reader = new StringReader(expectedXml); + var choice = (DateTypeChoice)serializer.Deserialize(reader)!; + + Assert.NotNull(choice); + Assert.Equal(DateTypeChoice.ChoiceOf.DateTimeChoice, choice.ChoiceType); + Assert.Null(choice.OnlyDate); + Assert.Equal(new DateTime(2024, 12, 25, 15, 30, 45), choice.DateTimeChoice); + + // Serialize back + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, choice, XmlNamespaceHelper.EmptyNamespace); + + var actualXml = sw.ToString(); + Assert.Equal(expectedXml, actualXml); + } + + #endregion + + #region Edge Cases and Validation + + [Fact] + public void XmlSerialization_EmptyStringValue_ShouldSerializeAndDeserialize() + { + const string expectedXml = """ + + + + """; + + var serializer = new XmlSerializer(typeof(TwoDifferentTypeChoice)); + + // Deserialize + using var reader = new StringReader(expectedXml); + var choice = (TwoDifferentTypeChoice)serializer.Deserialize(reader)!; + + Assert.NotNull(choice); + Assert.Equal(TwoDifferentTypeChoice.ChoiceOf.StringChoice, choice.ChoiceType); + Assert.Equal(string.Empty, choice.StringChoice); + } + + [Fact] + public void XmlSerialization_SpecialCharacters_ShouldBeEscaped() + { + const string expectedXml = """ + + <test> & "quotes" + + """; + + var serializer = new XmlSerializer(typeof(TwoDifferentTypeChoice)); + + // Deserialize + using var reader = new StringReader(expectedXml); + var choice = (TwoDifferentTypeChoice)serializer.Deserialize(reader)!; + + Assert.NotNull(choice); + Assert.Equal(" & \"quotes\"", choice.StringChoice); + + // Serialize back + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, choice, XmlNamespaceHelper.EmptyNamespace); + + var actualXml = sw.ToString(); + Assert.Equal(expectedXml, actualXml); + } + + [Fact] + public void XmlSerialization_NegativeInteger_ShouldRoundTrip() + { + const string expectedXml = """ + + -42 + + """; + + var serializer = new XmlSerializer(typeof(TwoDifferentTypeChoice)); + + // Deserialize + using var reader = new StringReader(expectedXml); + var choice = (TwoDifferentTypeChoice)serializer.Deserialize(reader)!; + + Assert.NotNull(choice); + Assert.Equal(-42, choice.IntChoice); + + // Serialize back + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, choice, XmlNamespaceHelper.EmptyNamespace); + + var actualXml = sw.ToString(); + Assert.Equal(expectedXml, actualXml); + } + + #endregion +} diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/XmlNamespaceChoice.cs b/tests/AltaSoft.ChoiceGenerator.Tests/XmlNamespaceChoice.cs new file mode 100644 index 0000000..66b33f3 --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/XmlNamespaceChoice.cs @@ -0,0 +1,13 @@ +using AltaSoft.Choice; + +namespace AltaSoft.ChoiceGenerator.Tests; + +[Choice] +public sealed partial class XmlNamespaceChoice +{ + [XmlTag("Cd", Namespace = "urn:test:code")] + public partial string? Code { get; set; } + + [XmlTag("Prtry", Namespace = "urn:test:proprietary")] + public partial string? Proprietary { get; set; } +} diff --git a/tests/AltaSoft.ChoiceGenerator.Tests/XmlNamespaceNilTests.cs b/tests/AltaSoft.ChoiceGenerator.Tests/XmlNamespaceNilTests.cs new file mode 100644 index 0000000..a0d99c6 --- /dev/null +++ b/tests/AltaSoft.ChoiceGenerator.Tests/XmlNamespaceNilTests.cs @@ -0,0 +1,139 @@ +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; +using AltaSoft.ChoiceGenerator.Tests.AdvancedXml; +using Xunit; + +namespace AltaSoft.ChoiceGenerator.Tests; + +/// +/// Tests to verify that xsi:nil attributes are NOT generated for null Choice properties +/// +public class XmlNamespaceNilTests +{ + private static readonly XmlWriterSettings s_xmlWriterSettings = new() + { + OmitXmlDeclaration = true, + Indent = true + }; + + [Fact] + public void MessageEnvelope_WithPayment_ShouldNotIncludeNilElements() + { + var payment = new PaymentInstruction + { + InstructionId = "PMT-999", + Amount = 5000.00m, + Currency = "EUR", + DebtorName = "ACME Corp", + CreditorName = "Supplier Ltd" + }; + + var envelope = new MessageEnvelope + { + Header = new ApplicationHeader + { + BusinessMessageIdentifier = "MSG-999", + CreationDateTime = new DateTime(2024, 12, 25, 14, 0, 0), + MessageDefinitionIdentifier = "pacs.008.001.10" + }, + Payment = payment + }; + + var serializer = new XmlSerializer(typeof(MessageEnvelope)); + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, envelope); + + var xml = sw.ToString(); + + // Verify Payment is present + Assert.Contains("", xml); + Assert.Contains("PMT-999", xml); + + // Verify Account and Customer are NOT present (no xsi:nil) + Assert.DoesNotContain("AcctRpt", xml); + Assert.DoesNotContain("CstmrData", xml); + Assert.DoesNotContain("xsi:nil", xml); + } + + [Fact] + public void MessageEnvelope_WithAccount_ShouldNotIncludeNilElements() + { + var account = new AccountReport + { + ReportId = "RPT-123", + AccountId = "ACC-456", + Balance = 10000.00m, + BalanceDate = new DateTime(2024, 12, 25) + }; + + var envelope = new MessageEnvelope + { + Header = new ApplicationHeader + { + BusinessMessageIdentifier = "MSG-ACCT", + CreationDateTime = new DateTime(2024, 12, 25, 10, 0, 0), + MessageDefinitionIdentifier = "camt.053" + }, + Account = account + }; + + var serializer = new XmlSerializer(typeof(MessageEnvelope)); + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, envelope); + + var xml = sw.ToString(); + + // Verify Account is present + Assert.Contains("", xml); + Assert.Contains("RPT-123", xml); + + // Verify Payment and Customer are NOT present (no xsi:nil) + Assert.DoesNotContain("PmtInstr", xml); + Assert.DoesNotContain("CstmrData", xml); + Assert.DoesNotContain("xsi:nil", xml); + } + + [Fact] + public void MessageEnvelope_WithCustomer_ShouldNotIncludeNilElements() + { + var customer = new CustomerData + { + CustomerId = "CUST-789", + Name = "John Doe", + Email = "john@example.com", + PhoneNumber = "+1-555-0100", + Country = "USA" + }; + + var envelope = new MessageEnvelope + { + Header = new ApplicationHeader + { + BusinessMessageIdentifier = "MSG-CUST", + CreationDateTime = new DateTime(2024, 12, 25, 15, 0, 0), + MessageDefinitionIdentifier = "acmt.001" + }, + Customer = customer + }; + + var serializer = new XmlSerializer(typeof(MessageEnvelope)); + using var sw = new StringWriter(); + using var writer = XmlWriter.Create(sw, s_xmlWriterSettings); + serializer.Serialize(writer, envelope); + + var xml = sw.ToString(); + + // Verify Customer is present + Assert.Contains("", xml); + Assert.Contains("CUST-789", xml); + + // Verify Payment and Account are NOT present (no xsi:nil) + Assert.DoesNotContain("PmtInstr", xml); + Assert.DoesNotContain("AcctRpt", xml); + Assert.DoesNotContain("xsi:nil", xml); + } +}