From 5e62d7cbe82df7d91e664c2f792206d711dad91a Mon Sep 17 00:00:00 2001 From: Pawel Gerr Date: Mon, 9 Sep 2024 19:54:39 +0200 Subject: [PATCH] Access modifier of union ctors are configurable + implicit conversions can be omitted. --- .../TextOrNumberWithAdditionalProperty.cs | 22 + .../DiscriminatedUnions/AllEnumSettings.cs | 8 + .../DiscriminatedUnions/UnionCodeGenerator.cs | 6 +- .../DiscriminatedUnions/UnionSettings.cs | 6 + .../UnionConstructorAccessModifier.cs | 8 + .../Extensions/AttributeDataExtensions.cs | 11 + .../Extensions/StringBuilderExtensions.cs | 20 + .../UnionAttributeBase.cs | 14 + .../UnionConstructorAccessModifier.cs | 22 + .../UnionSourceGeneratorTests.cs | 602 ++++++++++++++++++ ...ion_class_string_int_with_private_ctors.cs | 21 + 11 files changed, 738 insertions(+), 2 deletions(-) create mode 100644 samples/Thinktecture.Runtime.Extensions.Samples/DiscriminatedUnions/TextOrNumberWithAdditionalProperty.cs create mode 100644 src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/UnionConstructorAccessModifier.cs create mode 100644 src/Thinktecture.Runtime.Extensions/UnionConstructorAccessModifier.cs create mode 100644 test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_with_private_ctors.cs diff --git a/samples/Thinktecture.Runtime.Extensions.Samples/DiscriminatedUnions/TextOrNumberWithAdditionalProperty.cs b/samples/Thinktecture.Runtime.Extensions.Samples/DiscriminatedUnions/TextOrNumberWithAdditionalProperty.cs new file mode 100644 index 00000000..a05ec3ba --- /dev/null +++ b/samples/Thinktecture.Runtime.Extensions.Samples/DiscriminatedUnions/TextOrNumberWithAdditionalProperty.cs @@ -0,0 +1,22 @@ +namespace Thinktecture.DiscriminatedUnions; + +[Union(T1Name = "Text", + T2Name = "Number", + SkipImplicitConversionFromValue = true, + ConstructorAccessModifier = UnionConstructorAccessModifier.Private)] +public sealed partial class TextOrNumberExtended +{ + public required string AdditionalProperty { get; init; } + + public TextOrNumberExtended(string text, string additionalProperty) + : this(text) + { + AdditionalProperty = additionalProperty; + } + + public TextOrNumberExtended(int number, string additionalProperty) + : this(number) + { + AdditionalProperty = additionalProperty; + } +} diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/AllEnumSettings.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/AllEnumSettings.cs index 67c45cda..c25633c2 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/AllEnumSettings.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/AllEnumSettings.cs @@ -7,6 +7,8 @@ public sealed class AllUnionSettings : IEquatable public SwitchMapMethodsGeneration MapMethods { get; } public IReadOnlyList MemberTypeSettings { get; } public StringComparison DefaultStringComparison { get; } + public UnionConstructorAccessModifier ConstructorAccessModifier { get; } + public bool SkipImplicitConversionFromValue { get; } public AllUnionSettings(AttributeData attribute, int numberOfMemberTypes) { @@ -14,6 +16,8 @@ public AllUnionSettings(AttributeData attribute, int numberOfMemberTypes) SwitchMethods = attribute.FindSwitchMethods(); MapMethods = attribute.FindMapMethods(); DefaultStringComparison = attribute.FindDefaultStringComparison(); + ConstructorAccessModifier = attribute.FindUnionConstructorAccessModifier(); + SkipImplicitConversionFromValue = attribute.FindSkipImplicitConversionFromValue(); var memberTypeSettings = new MemberTypeSetting[numberOfMemberTypes]; MemberTypeSettings = memberTypeSettings; @@ -41,6 +45,8 @@ public bool Equals(AllUnionSettings? other) && SwitchMethods == other.SwitchMethods && MapMethods == other.MapMethods && DefaultStringComparison == other.DefaultStringComparison + && ConstructorAccessModifier == other.ConstructorAccessModifier + && SkipImplicitConversionFromValue == other.SkipImplicitConversionFromValue && MemberTypeSettings.SequenceEqual(other.MemberTypeSettings); } @@ -52,6 +58,8 @@ public override int GetHashCode() hashCode = (hashCode * 397) ^ SwitchMethods.GetHashCode(); hashCode = (hashCode * 397) ^ MapMethods.GetHashCode(); hashCode = (hashCode * 397) ^ (int)DefaultStringComparison; + hashCode = (hashCode * 397) ^ (int)ConstructorAccessModifier; + hashCode = (hashCode * 397) ^ SkipImplicitConversionFromValue.GetHashCode(); hashCode = (hashCode * 397) ^ MemberTypeSettings.ComputeHashCode(); return hashCode; diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionCodeGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionCodeGenerator.cs index 0a7ecff5..a228d63c 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionCodeGenerator.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionCodeGenerator.cs @@ -97,7 +97,9 @@ private void GenerateUnion(CancellationToken cancellationToken) GenerateMap(true); } - GenerateImplicitConversions(); + if (!_state.Settings.SkipImplicitConversionFromValue) + GenerateImplicitConversions(); + GenerateExplicitConversions(); GenerateEqualityOperators(); GenerateEquals(); @@ -702,7 +704,7 @@ private void GenerateConstructors() /// Initializes new instance with . /// /// Value to create a new instance for. - public ").Append(_state.Name).Append("(").AppendTypeFullyQualified(memberType).Append(" ").Append(memberType.ArgumentName.Escaped).Append(@") + ").AppendAccessModifier(_state.Settings.ConstructorAccessModifier).Append(" ").Append(_state.Name).Append("(").AppendTypeFullyQualified(memberType).Append(" ").Append(memberType.ArgumentName.Escaped).Append(@") { this._").Append(memberType.ArgumentName.Raw).Append(" = ").Append(memberType.ArgumentName.Escaped).Append(@"; this._valueIndex = ").Append(i + 1).Append(@"; diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSettings.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSettings.cs index 0a18c0b9..34817e39 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSettings.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSettings.cs @@ -9,6 +9,8 @@ namespace Thinktecture.CodeAnalysis.DiscriminatedUnions; public SwitchMapMethodsGeneration SwitchMethods => _settings.SwitchMethods; public SwitchMapMethodsGeneration MapMethods => _settings.MapMethods; public StringComparison DefaultStringComparison => _settings.DefaultStringComparison; + public UnionConstructorAccessModifier ConstructorAccessModifier => _settings.ConstructorAccessModifier; + public bool SkipImplicitConversionFromValue => _settings.SkipImplicitConversionFromValue; public bool HasStructLayoutAttribute => _attributeInfo.HasStructLayoutAttribute; public UnionSettings(AllUnionSettings settings, AttributeInfo attributeInfo) @@ -28,6 +30,8 @@ public bool Equals(UnionSettings other) && SwitchMethods == other.SwitchMethods && MapMethods == other.MapMethods && DefaultStringComparison == other.DefaultStringComparison + && ConstructorAccessModifier == other.ConstructorAccessModifier + && SkipImplicitConversionFromValue == other.SkipImplicitConversionFromValue && HasStructLayoutAttribute == other.HasStructLayoutAttribute; } @@ -39,6 +43,8 @@ public override int GetHashCode() hashCode = (hashCode * 397) ^ SwitchMethods.GetHashCode(); hashCode = (hashCode * 397) ^ MapMethods.GetHashCode(); hashCode = (hashCode * 397) ^ (int)DefaultStringComparison; + hashCode = (hashCode * 397) ^ (int)ConstructorAccessModifier; + hashCode = (hashCode * 397) ^ SkipImplicitConversionFromValue.GetHashCode(); hashCode = (hashCode * 397) ^ HasStructLayoutAttribute.GetHashCode(); return hashCode; diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/UnionConstructorAccessModifier.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/UnionConstructorAccessModifier.cs new file mode 100644 index 00000000..e9c7a2cd --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/UnionConstructorAccessModifier.cs @@ -0,0 +1,8 @@ +namespace Thinktecture.CodeAnalysis; + +public enum UnionConstructorAccessModifier +{ + Private = 1 << 0, + Internal = 1 << 2, + Public = 1 << 3 +} diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs index 5403065b..082b6240 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs @@ -169,6 +169,17 @@ public static bool FindTxIsNullableReferenceType(this AttributeData attributeDat return GetStringParameterValue(attributeData, $"T{index}Name"); } + public static UnionConstructorAccessModifier FindUnionConstructorAccessModifier(this AttributeData attributeData) + { + return (UnionConstructorAccessModifier?)GetIntegerParameterValue(attributeData, "ConstructorAccessModifier") + ?? UnionConstructorAccessModifier.Public; + } + + public static bool FindSkipImplicitConversionFromValue(this AttributeData attributeData) + { + return GetBooleanParameterValue(attributeData, "SkipImplicitConversionFromValue") ?? false; + } + public static (ITypeSymbol ComparerType, ITypeSymbol ItemType)? GetComparerTypes(this AttributeData attributeData) { if (attributeData.AttributeClass is not { } attributeClass || attributeClass.TypeKind == TypeKind.Error) diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/StringBuilderExtensions.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/StringBuilderExtensions.cs index f9844703..9df68eae 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/StringBuilderExtensions.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/StringBuilderExtensions.cs @@ -43,6 +43,26 @@ public static StringBuilder RenderAccessModifier( return sb; } + public static StringBuilder AppendAccessModifier( + this StringBuilder sb, + UnionConstructorAccessModifier accessModifier) + { + switch (accessModifier) + { + case UnionConstructorAccessModifier.Private: + sb.Append("private"); + break; + case UnionConstructorAccessModifier.Internal: + sb.Append("internal"); + break; + case UnionConstructorAccessModifier.Public: + sb.Append("public"); + break; + } + + return sb; + } + public static StringBuilder RenderArguments( this StringBuilder sb, IReadOnlyList members, diff --git a/src/Thinktecture.Runtime.Extensions/UnionAttributeBase.cs b/src/Thinktecture.Runtime.Extensions/UnionAttributeBase.cs index c959dbee..e117ae98 100644 --- a/src/Thinktecture.Runtime.Extensions/UnionAttributeBase.cs +++ b/src/Thinktecture.Runtime.Extensions/UnionAttributeBase.cs @@ -16,6 +16,20 @@ public abstract class UnionAttributeBase : Attribute /// public bool SkipToString { get; set; } + /// + /// Defines the access modifier of the constructors. + /// Default is . + /// + /// + /// Access modifier of the constructors will have effect on the access modifier of implicit casts. + /// + public UnionConstructorAccessModifier ConstructorAccessModifier { get; set; } = UnionConstructorAccessModifier.Public; + + /// + /// Indication whether the generator should skip the implementation of implicit conversions from value to union type. + /// + public bool SkipImplicitConversionFromValue { get; set; } + /// /// Indication whether and how the generator should generate the methods Switch. /// diff --git a/src/Thinktecture.Runtime.Extensions/UnionConstructorAccessModifier.cs b/src/Thinktecture.Runtime.Extensions/UnionConstructorAccessModifier.cs new file mode 100644 index 00000000..e1a4c9d7 --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions/UnionConstructorAccessModifier.cs @@ -0,0 +1,22 @@ +namespace Thinktecture; + +/// +/// Access modifer. +/// +public enum UnionConstructorAccessModifier +{ + /// + /// Access modifer "private". + /// + Private = 1 << 0, + + /// + /// Access modifer "internal". + /// + Internal = 1 << 2, + + /// + /// Access modifer "public". + /// + Public = 1 << 3 +} diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/UnionSourceGeneratorTests.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/UnionSourceGeneratorTests.cs index d21b452b..9a5417ae 100644 --- a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/UnionSourceGeneratorTests.cs +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/UnionSourceGeneratorTests.cs @@ -329,6 +329,608 @@ public override int GetHashCode() """); } + [Fact] + public void Should_generate_class_with_string_and_int_without_implicit_conversion() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + [Union(SkipImplicitConversionFromValue = true)] + public partial class TestUnion; + } + """; + var outputs = GetGeneratedOutputs(source, typeof(UnionAttribute<,>).Assembly); + outputs.Should().HaveCount(1); + + var mainOutput = outputs.Single(kvp => kvp.Key.Contains("Thinktecture.Tests.TestUnion.g.cs")).Value; + + AssertOutput(mainOutput, _GENERATED_HEADER + """ + namespace Thinktecture.Tests + { + sealed partial class TestUnion : + global::System.IEquatable, + global::System.Numerics.IEqualityOperators + { + private static readonly int _typeHashCode = typeof(global::Thinktecture.Tests.TestUnion).GetHashCode(); + + private readonly int _valueIndex; + + private readonly string? _string; + private readonly int _int32; + + /// + /// Indication whether the current value is of type string. + /// + public bool IsString => this._valueIndex == 1; + + /// + /// Indication whether the current value is of type int. + /// + public bool IsInt32 => this._valueIndex == 2; + + /// + /// Gets the current value as string. + /// + /// If the current value is not of type string. + public string AsString => IsString ? this._string! : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'string'."); + + /// + /// Gets the current value as int. + /// + /// If the current value is not of type int. + public int AsInt32 => IsInt32 ? this._int32 : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'int'."); + + /// + /// Gets the current value as . + /// + public object Value => this._valueIndex switch + { + 1 => this._string!, + 2 => this._int32, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(string @string) + { + this._string = @string; + this._valueIndex = 1; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(int int32) + { + this._int32 = int32; + this._valueIndex = 2; + } + + /// + /// Executes an action depending on the current value. + /// + /// The action to execute if the current value is of type string. + /// The action to execute if the current value is of type int. + public void Switch( + global::System.Action @string, + global::System.Action int32) + { + switch (this._valueIndex) + { + case 1: + @string(this._string!); + return; + case 2: + int32(this._int32); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Executes an action depending on the current value. + /// + /// Context to be passed to the callbacks. + /// The action to execute if the current value is of type string. + /// The action to execute if the current value is of type int. + public void Switch( + TContext context, + global::System.Action @string, + global::System.Action int32) + { + switch (this._valueIndex) + { + case 1: + @string(context, this._string!); + return; + case 2: + int32(context, this._int32); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Executes a function depending on the current value. + /// + /// The function to execute if the current value is of type string. + /// The function to execute if the current value is of type int. + public TResult Switch( + global::System.Func @string, + global::System.Func int32) + { + switch (this._valueIndex) + { + case 1: + return @string(this._string!); + case 2: + return int32(this._int32); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Executes a function depending on the current value. + /// + /// Context to be passed to the callbacks. + /// The function to execute if the current value is of type string. + /// The function to execute if the current value is of type int. + public TResult Switch( + TContext context, + global::System.Func @string, + global::System.Func int32) + { + switch (this._valueIndex) + { + case 1: + return @string(context, this._string!); + case 2: + return int32(context, this._int32); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Maps current value to an instance of type . + /// + /// The instance to return if the current value is of type string. + /// The instance to return if the current value is of type int. + public TResult Map( + TResult @string, + TResult int32) + { + switch (this._valueIndex) + { + case 1: + return @string; + case 2: + return int32; + default: + throw new global::System.ArgumentOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Implicit conversion to type string. + /// + /// Object to covert. + /// Inner value of type string. + /// If the inner value is not a string. + public static explicit operator string(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsString; + } + + /// + /// Implicit conversion to type int. + /// + /// Object to covert. + /// Inner value of type int. + /// If the inner value is not a int. + public static explicit operator int(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsInt32; + } + + /// + /// Compares two instances of . + /// + /// Instance to compare. + /// Another instance to compare. + /// true if objects are equal; otherwise false. + public static bool operator ==(global::Thinktecture.Tests.TestUnion? obj, global::Thinktecture.Tests.TestUnion? other) + { + if (obj is null) + return other is null; + + return obj.Equals(other); + } + + /// + /// Compares two instances of . + /// + /// Instance to compare. + /// Another instance to compare. + /// false if objects are equal; otherwise true. + public static bool operator !=(global::Thinktecture.Tests.TestUnion? obj, global::Thinktecture.Tests.TestUnion? other) + { + return !(obj == other); + } + + /// + public override bool Equals(object? other) + { + return other is global::Thinktecture.Tests.TestUnion obj && Equals(obj); + } + + /// + public bool Equals(global::Thinktecture.Tests.TestUnion? other) + { + if (other is null) + return false; + + if (ReferenceEquals(this, other)) + return true; + + if (this._valueIndex != other._valueIndex) + return false; + + return this._valueIndex switch + { + 1 => this._string is null ? other._string is null : this._string.Equals(other._string, global::System.StringComparison.OrdinalIgnoreCase), + 2 => this._int32.Equals(other._int32), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override int GetHashCode() + { + return this._valueIndex switch + { + 1 => global::System.HashCode.Combine(global::Thinktecture.Tests.TestUnion._typeHashCode, this._string?.GetHashCode(global::System.StringComparison.OrdinalIgnoreCase) ?? 0), + 2 => global::System.HashCode.Combine(global::Thinktecture.Tests.TestUnion._typeHashCode, this._int32.GetHashCode()), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override string? ToString() + { + return this._valueIndex switch + { + 1 => this._string, + 2 => this._int32.ToString(), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } + } + + """); + } + + [Fact] + public void Should_generate_class_with_string_and_int_with_private_ctors() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + [Union(ConstructorAccessModifier = UnionConstructorAccessModifier.Private)] + public partial class TestUnion; + } + """; + var outputs = GetGeneratedOutputs(source, typeof(UnionAttribute<,>).Assembly); + outputs.Should().HaveCount(1); + + var mainOutput = outputs.Single(kvp => kvp.Key.Contains("Thinktecture.Tests.TestUnion.g.cs")).Value; + + AssertOutput(mainOutput, _GENERATED_HEADER + """ + namespace Thinktecture.Tests + { + sealed partial class TestUnion : + global::System.IEquatable, + global::System.Numerics.IEqualityOperators + { + private static readonly int _typeHashCode = typeof(global::Thinktecture.Tests.TestUnion).GetHashCode(); + + private readonly int _valueIndex; + + private readonly string? _string; + private readonly int _int32; + + /// + /// Indication whether the current value is of type string. + /// + public bool IsString => this._valueIndex == 1; + + /// + /// Indication whether the current value is of type int. + /// + public bool IsInt32 => this._valueIndex == 2; + + /// + /// Gets the current value as string. + /// + /// If the current value is not of type string. + public string AsString => IsString ? this._string! : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'string'."); + + /// + /// Gets the current value as int. + /// + /// If the current value is not of type int. + public int AsInt32 => IsInt32 ? this._int32 : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'int'."); + + /// + /// Gets the current value as . + /// + public object Value => this._valueIndex switch + { + 1 => this._string!, + 2 => this._int32, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + private TestUnion(string @string) + { + this._string = @string; + this._valueIndex = 1; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + private TestUnion(int int32) + { + this._int32 = int32; + this._valueIndex = 2; + } + + /// + /// Executes an action depending on the current value. + /// + /// The action to execute if the current value is of type string. + /// The action to execute if the current value is of type int. + public void Switch( + global::System.Action @string, + global::System.Action int32) + { + switch (this._valueIndex) + { + case 1: + @string(this._string!); + return; + case 2: + int32(this._int32); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Executes an action depending on the current value. + /// + /// Context to be passed to the callbacks. + /// The action to execute if the current value is of type string. + /// The action to execute if the current value is of type int. + public void Switch( + TContext context, + global::System.Action @string, + global::System.Action int32) + { + switch (this._valueIndex) + { + case 1: + @string(context, this._string!); + return; + case 2: + int32(context, this._int32); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Executes a function depending on the current value. + /// + /// The function to execute if the current value is of type string. + /// The function to execute if the current value is of type int. + public TResult Switch( + global::System.Func @string, + global::System.Func int32) + { + switch (this._valueIndex) + { + case 1: + return @string(this._string!); + case 2: + return int32(this._int32); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Executes a function depending on the current value. + /// + /// Context to be passed to the callbacks. + /// The function to execute if the current value is of type string. + /// The function to execute if the current value is of type int. + public TResult Switch( + TContext context, + global::System.Func @string, + global::System.Func int32) + { + switch (this._valueIndex) + { + case 1: + return @string(context, this._string!); + case 2: + return int32(context, this._int32); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Maps current value to an instance of type . + /// + /// The instance to return if the current value is of type string. + /// The instance to return if the current value is of type int. + public TResult Map( + TResult @string, + TResult int32) + { + switch (this._valueIndex) + { + case 1: + return @string; + case 2: + return int32; + default: + throw new global::System.ArgumentOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + } + + /// + /// Implicit conversion from type string. + /// + /// Value to covert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(string value) + { + return new global::Thinktecture.Tests.TestUnion(value); + } + + /// + /// Implicit conversion from type int. + /// + /// Value to covert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(int value) + { + return new global::Thinktecture.Tests.TestUnion(value); + } + + /// + /// Implicit conversion to type string. + /// + /// Object to covert. + /// Inner value of type string. + /// If the inner value is not a string. + public static explicit operator string(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsString; + } + + /// + /// Implicit conversion to type int. + /// + /// Object to covert. + /// Inner value of type int. + /// If the inner value is not a int. + public static explicit operator int(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsInt32; + } + + /// + /// Compares two instances of . + /// + /// Instance to compare. + /// Another instance to compare. + /// true if objects are equal; otherwise false. + public static bool operator ==(global::Thinktecture.Tests.TestUnion? obj, global::Thinktecture.Tests.TestUnion? other) + { + if (obj is null) + return other is null; + + return obj.Equals(other); + } + + /// + /// Compares two instances of . + /// + /// Instance to compare. + /// Another instance to compare. + /// false if objects are equal; otherwise true. + public static bool operator !=(global::Thinktecture.Tests.TestUnion? obj, global::Thinktecture.Tests.TestUnion? other) + { + return !(obj == other); + } + + /// + public override bool Equals(object? other) + { + return other is global::Thinktecture.Tests.TestUnion obj && Equals(obj); + } + + /// + public bool Equals(global::Thinktecture.Tests.TestUnion? other) + { + if (other is null) + return false; + + if (ReferenceEquals(this, other)) + return true; + + if (this._valueIndex != other._valueIndex) + return false; + + return this._valueIndex switch + { + 1 => this._string is null ? other._string is null : this._string.Equals(other._string, global::System.StringComparison.OrdinalIgnoreCase), + 2 => this._int32.Equals(other._int32), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override int GetHashCode() + { + return this._valueIndex switch + { + 1 => global::System.HashCode.Combine(global::Thinktecture.Tests.TestUnion._typeHashCode, this._string?.GetHashCode(global::System.StringComparison.OrdinalIgnoreCase) ?? 0), + 2 => global::System.HashCode.Combine(global::Thinktecture.Tests.TestUnion._typeHashCode, this._int32.GetHashCode()), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override string? ToString() + { + return this._valueIndex switch + { + 1 => this._string, + 2 => this._int32.ToString(), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } + } + + """); + } + [Fact] public void Should_generate_class_with_string_and_int_with_SwitchPartially() { diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_with_private_ctors.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_with_private_ctors.cs new file mode 100644 index 00000000..cb1b76f1 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_with_private_ctors.cs @@ -0,0 +1,21 @@ +namespace Thinktecture.Runtime.Tests.TestUnions; + +// ReSharper disable once InconsistentNaming +[Union(SkipImplicitConversionFromValue = true, + ConstructorAccessModifier = UnionConstructorAccessModifier.Private)] +public partial class TestUnion_class_string_int_with_private_ctors +{ + public required object OtherValue { get; init; } + + public TestUnion_class_string_int_with_private_ctors(string value, object otherValue) + : this(value) + { + OtherValue = otherValue; + } + + public TestUnion_class_string_int_with_private_ctors(int value, object otherValue) + : this(value) + { + OtherValue = otherValue; + } +}