Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
<Authors>ALTA Software llc.</Authors>
<Product>Choice generator</Product>
<Company>ALTA Software llc.</Company>
<Copyright>Copyright © 2024 ALTA Software llc.</Copyright>
<Version>2.1.5</Version>
<Copyright>Copyright © 2024-2026 ALTA Software llc.</Copyright>
<Version>2.2.0</Version>
</PropertyGroup>

<PropertyGroup>
Expand Down
39 changes: 32 additions & 7 deletions src/AltaSoft.Choice.Generator/Executor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,14 @@ private static SourceCodeBuilder Process(INamedTypeSymbol typeSymbol, List<IProp
else
{
sb.AppendLine("[DisallowNull]");
sb.Append("[XmlElement(\"").Append(p.XmlNameValue).AppendLine("\")]");

// Generate XmlElement attribute with optional namespace
sb.Append("[XmlElement(\"").Append(p.XmlNameValue).Append("\"");
if (p.XmlNamespace is not null)
{
sb.Append(", Namespace = \"").Append(p.XmlNamespace).Append("\"");
}
sb.AppendLine(")]");
}

sb.AppendLine("[ChoiceProperty]");
Expand Down Expand Up @@ -146,7 +153,12 @@ private static SourceCodeBuilder Process(INamedTypeSymbol typeSymbol, List<IProp

if (isDateOnly)
{
sb.Append("[XmlElement(\"").Append(p.XmlNameValue).AppendLine("\")]");
sb.Append("[XmlElement(\"").Append(p.XmlNameValue).Append("\"");
if (p.XmlNamespace is not null)
{
sb.Append(", Namespace = \"").Append(p.XmlNamespace).Append("\"");
}
sb.AppendLine(")]");
sb.AppendLine("[JsonIgnore]");

sb.Append($"public string? {p.Name}Surrogate")
Expand Down Expand Up @@ -182,17 +194,21 @@ private static SourceCodeBuilder Process(INamedTypeSymbol typeSymbol, List<IProp
if (!ProcessImplicitOperators(sb, typeSymbol.Name, processedProperties))
sb.NewLine();

foreach (var p in processedProperties.Where(x => 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 <see cref=\"{p}\"/> property should be serialized.")
.AppendBlock("returns", $"<c>true</c> if <see cref=\"{p}\"/> has a value; otherwise, <c>false</c>.");
sb.AppendSummary($"Determines whether the <see cref=\"{p.Name}\"/> property should be serialized.")
.AppendBlock("returns", $"<c>true</c> if <see cref=\"{p.Name}\"/> is the active choice; otherwise, <c>false</c>.");

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(";");
Comment thread
GregoryNikolaishvili marked this conversation as resolved.

sb.NewLine();
}

Expand Down Expand Up @@ -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();
Expand All @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions src/AltaSoft.Choice.Generator/Models/PropertyDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ internal sealed class PropertyDetails
/// </summary>
internal string XmlNameValue { get; private set; }

/// <summary>
/// The XML namespace for the property element.
/// </summary>
internal string? XmlNamespace { get; private set; }

/// <summary>
/// The access modifiers for the property, such as "public", "private", etc.
/// </summary>
Expand Down Expand Up @@ -66,6 +71,7 @@ public PropertyDetails(
string typeName,
string @namespace,
string xmlNameValue,
string? xmlNamespace,
string modifiers,
string? summary,
Accessibility getterAccessibility,
Expand All @@ -76,6 +82,7 @@ public PropertyDetails(
TypeName = typeName;
Namespace = @namespace;
XmlNameValue = xmlNameValue;
XmlNamespace = xmlNamespace;
Modifiers = modifiers;
Summary = summary;
GetterAccessibility = getterAccessibility;
Expand Down
8 changes: 8 additions & 0 deletions src/AltaSoft.Choice/XmlTagAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ public sealed class XmlTagAttribute : Attribute
/// </value>
public string Tag { get; }

/// <summary>
/// Gets the XML namespace associated with the property.
/// </summary>
/// <value>
/// A <see cref="string"/> representing the XML namespace, or <c>null</c> if no namespace is specified.
/// </value>
public string? Namespace { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="XmlTagAttribute"/> class with the specified XML tag value.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{

/// <summary>
/// <para>Specifies the authorisation, in a coded form.</para>
/// </summary>
[XmlElement("Cd")]

public partial string? Code { get; set; }

/// <summary>
/// <para>Specifies the authorisation, in a free text form.</para>
/// </summary>
[XmlElement("Prtry")]

public partial Authorisation1Code? Proprietary { get; set; }
}
}

namespace TestNamespace.OtherNamespace
{
public enum Authorisation1Code
Expand All @@ -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
{
/// <summary>
/// <para>Specifies the code with namespace.</para>
/// </summary>
[XmlTag("Cd", Namespace = "urn:test:code")]
public partial string? Code { get; set; }

/// <summary>
/// <para>Specifies the proprietary value with namespace.</para>
/// </summary>
[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
{
/// <summary>
/// <para>Code with XmlTag and namespace.</para>
/// </summary>
[XmlTag("Cd", Namespace = "urn:test:code")]
public partial string? Code { get; set; }

/// <summary>
/// <para>Proprietary with standard XmlElement.</para>
/// </summary>
[XmlElement("Prtry")]
public partial string? Proprietary { get; set; }

/// <summary>
/// <para>Amount with XmlTag but no namespace.</para>
/// </summary>
[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<ImmutableArray<Diagnostic>, List<string>, GeneratorDriver>? additionalChecks = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,19 @@ public void Switch(
/// Determines whether the <see cref="Code"/> property should be serialized.
/// </summary>
/// <returns>
/// <c>true</c> if <see cref="Code"/> has a value; otherwise, <c>false</c>.
/// <c>true</c> if <see cref="Code"/> is the active choice; otherwise, <c>false</c>.
/// </returns>
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public bool ShouldSerializeCode() => Code.HasValue;
public bool ShouldSerializeCode() => ChoiceType == ChoiceOf.Code;

/// <summary>
/// Determines whether the <see cref="Proprietary"/> property should be serialized.
/// </summary>
/// <returns>
/// <c>true</c> if <see cref="Proprietary"/> is the active choice; otherwise, <c>false</c>.
/// </returns>
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public bool ShouldSerializeProprietary() => ChoiceType == ChoiceOf.Proprietary;

/// <summary>
/// <para>Choice enumeration</para>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,24 @@ public void Switch(
return value is null ? null : CreateAsAccounts(value);
}

/// <summary>
/// Determines whether the <see cref="StringChoice"/> property should be serialized.
/// </summary>
/// <returns>
/// <c>true</c> if <see cref="StringChoice"/> is the active choice; otherwise, <c>false</c>.
/// </returns>
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public bool ShouldSerializeStringChoice() => ChoiceType == ChoiceOf.StringChoice;

/// <summary>
/// Determines whether the <see cref="Accounts"/> property should be serialized.
/// </summary>
/// <returns>
/// <c>true</c> if <see cref="Accounts"/> is the active choice; otherwise, <c>false</c>.
/// </returns>
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public bool ShouldSerializeAccounts() => ChoiceType == ChoiceOf.Accounts;

/// <summary>
/// <para>Choice enumeration</para>
/// </summary>
Expand Down
Loading
Loading