diff --git a/Directory.Packages.props b/Directory.Packages.props index d61e99d6..73fe33c6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,28 +1,28 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/Thinktecture.Runtime.Extensions.Samples/DiscriminatedUnions/DiscriminatedUnionsDemos.cs b/samples/Thinktecture.Runtime.Extensions.Samples/DiscriminatedUnions/DiscriminatedUnionsDemos.cs new file mode 100644 index 00000000..dcbf97ff --- /dev/null +++ b/samples/Thinktecture.Runtime.Extensions.Samples/DiscriminatedUnions/DiscriminatedUnionsDemos.cs @@ -0,0 +1,53 @@ +using Serilog; + +namespace Thinktecture.DiscriminatedUnions; + +public class DiscriminatedUnionsDemos +{ + public static void Demo(ILogger logger) + { + logger.Information(""" + + + ==== Demo for Union ==== + + """); + + TextOrNumber textOrNumberFromString = "text"; + logger.Information("Implicitly casted from string: {TextOrNumberFromString}", textOrNumberFromString); + + TextOrNumber textOrNumberFromInt = 42; + logger.Information("Implicitly casted from int: {TextOrNumberFromInt}", textOrNumberFromInt); + + logger.Information("TextOrNumber from string: IsText = {IsText}", textOrNumberFromString.IsText); + logger.Information("TextOrNumber from string: IsNumber = {IsNumber}", textOrNumberFromString.IsNumber); + logger.Information("TextOrNumber from int: IsText = {IsText}", textOrNumberFromInt.IsText); + logger.Information("TextOrNumber from int: IsNumber = {IsNumber}", textOrNumberFromInt.IsNumber); + + logger.Information("TextOrNumber from string: AsText = {AsText}", textOrNumberFromString.AsText); + logger.Information("TextOrNumber from int: AsNumber = {AsNumber}", textOrNumberFromInt.AsNumber); + + logger.Information("TextOrNumber from string: Value = {Value}", textOrNumberFromString.Value); + logger.Information("TextOrNumber from int: Value = {Value}", textOrNumberFromInt.Value); + + textOrNumberFromString.Switch(text: s => logger.Information("String Action: {Text}", s), + number: i => logger.Information("Int Action: {Number}", i)); + + textOrNumberFromString.Switch(logger, + text: static (l, s) => l.Information("String Action with context: {Text}", s), + number: static (l, i) => l.Information("Int Action with context: {Number}", i)); + + var switchResponse = textOrNumberFromInt.Switch(text: static s => $"String Func: {s}", + number: static i => $"Int Func: {i}"); + logger.Information("{Response}", switchResponse); + + var switchResponseWithContext = textOrNumberFromInt.Switch(123.45, + text: static (ctx, s) => $"String Func with context: {ctx} | {s}", + number: static (ctx, i) => $"Int Func with context: {ctx} | {i}"); + logger.Information("{Response}", switchResponseWithContext); + + var mapResponse = textOrNumberFromString.Map(text: "Mapped string", + number: "Mapped int"); + logger.Information("{Response}", mapResponse); + } +} diff --git a/samples/Thinktecture.Runtime.Extensions.Samples/DiscriminatedUnions/TextOrNumber.cs b/samples/Thinktecture.Runtime.Extensions.Samples/DiscriminatedUnions/TextOrNumber.cs new file mode 100644 index 00000000..a7627f1d --- /dev/null +++ b/samples/Thinktecture.Runtime.Extensions.Samples/DiscriminatedUnions/TextOrNumber.cs @@ -0,0 +1,8 @@ +namespace Thinktecture.DiscriminatedUnions; + +[Union(T1IsNullableReferenceType = true, + T1Name = "Text", + T2Name = "Number", + SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)] +public sealed partial class TextOrNumber; diff --git a/samples/Thinktecture.Runtime.Extensions.Samples/Program.cs b/samples/Thinktecture.Runtime.Extensions.Samples/Program.cs index 5b050e6d..9bd83e16 100644 --- a/samples/Thinktecture.Runtime.Extensions.Samples/Program.cs +++ b/samples/Thinktecture.Runtime.Extensions.Samples/Program.cs @@ -1,4 +1,5 @@ using Serilog; +using Thinktecture.DiscriminatedUnions; using Thinktecture.EmptyClass; using Thinktecture.SmartEnums; using Thinktecture.ValueObjects; @@ -13,6 +14,7 @@ public static void Main() SmartEnumDemos.Demo(logger); ValueObjectDemos.Demo(logger); + DiscriminatedUnionsDemos.Demo(logger); EmptyActionDemos.Demo(); EmptyCollectionsDemos.Demo(); } diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ComparableCodeGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ComparableCodeGenerator.cs index 5155f93a..9dff877a 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ComparableCodeGenerator.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ComparableCodeGenerator.cs @@ -33,7 +33,7 @@ public int CompareTo(object? obj) return 1; if(obj is not ").AppendTypeFullyQualified(state.Type).Append(@" item) - throw new global::System.ArgumentException(""Argument must be of type \""").Append(state.Type.TypeMinimallyQualified).Append(@"\""."", nameof(obj)); + throw new global::System.ArgumentException(""Argument must be of type \""").AppendTypeMinimallyQualified(state.Type).Append(@"\""."", nameof(obj)); return this.CompareTo(item); } diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Constants.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Constants.cs index bd77d9c1..3b13e30d 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Constants.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Constants.cs @@ -70,5 +70,15 @@ public static class SmartEnum public const string KEYED_FULL_NAME = "Thinktecture.SmartEnumAttribute`1"; public const string KEYLESS_FULL_NAME = "Thinktecture.SmartEnumAttribute"; } + + public static class Union + { + public const string NAMESPACE = "Thinktecture"; + public const string NAME = "UnionAttribute"; + public const string FULL_NAME_2_TYPES = "Thinktecture.UnionAttribute`2"; + public const string FULL_NAME_3_TYPES = "Thinktecture.UnionAttribute`3"; + public const string FULL_NAME_4_TYPES = "Thinktecture.UnionAttribute`4"; + public const string FULL_NAME_5_TYPES = "Thinktecture.UnionAttribute`5"; + } } } diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Diagnostics/ThinktectureRuntimeExtensionsAnalyzer.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Diagnostics/ThinktectureRuntimeExtensionsAnalyzer.cs index 0f7ab4e9..06ec648f 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Diagnostics/ThinktectureRuntimeExtensionsAnalyzer.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/Diagnostics/ThinktectureRuntimeExtensionsAnalyzer.cs @@ -41,7 +41,8 @@ public sealed class ThinktectureRuntimeExtensionsAnalyzer : DiagnosticAnalyzer DiagnosticsDescriptors.PrimaryConstructorNotAllowed, DiagnosticsDescriptors.CustomKeyMemberImplementationNotFound, DiagnosticsDescriptors.CustomKeyMemberImplementationTypeMismatch, - DiagnosticsDescriptors.IndexBasedSwitchAndMapMustUseNamedParameters); + DiagnosticsDescriptors.IndexBasedSwitchAndMapMustUseNamedParameters, + DiagnosticsDescriptors.TypeMustBeClass); /// public override void Initialize(AnalysisContext context) @@ -51,6 +52,7 @@ public override void Initialize(AnalysisContext context) context.RegisterOperationAction(AnalyzeSmartEnum, OperationKind.Attribute); context.RegisterOperationAction(AnalyzeValueObject, OperationKind.Attribute); + context.RegisterOperationAction(AnalyzeUnion, OperationKind.Attribute); context.RegisterOperationAction(AnalyzeMethodCall, OperationKind.Invocation); } @@ -177,6 +179,56 @@ private static void AnalyzeValueObject(OperationAnalysisContext context) } } + private static void AnalyzeUnion(OperationAnalysisContext context) + { + if (context.ContainingSymbol.Kind != SymbolKind.NamedType + || context.Operation is not IAttributeOperation { Operation: IObjectCreationOperation attrCreation } + || context.ContainingSymbol is not INamedTypeSymbol type + || type.TypeKind == TypeKind.Error + || type.DeclaringSyntaxReferences.IsDefaultOrEmpty) + { + return; + } + + try + { + if (!attrCreation.Type.IsUnionAttribute()) + return; + + var locationOfFirstDeclaration = type.Locations.IsDefaultOrEmpty ? Location.None : type.Locations[0]; // a representative for all + + ValidateUnion(context, type, attrCreation, locationOfFirstDeclaration); + } + catch (Exception ex) + { + context.ReportDiagnostic(Diagnostic.Create(DiagnosticsDescriptors.ErrorDuringCodeAnalysis, + Location.None, + type.ToFullyQualifiedDisplayString(), ex.Message)); + } + } + + private static void ValidateUnion(OperationAnalysisContext context, + INamedTypeSymbol type, + IObjectCreationOperation attribute, + Location locationOfFirstDeclaration) + { + if (type.IsRecord || type.TypeKind is not TypeKind.Class) + { + ReportDiagnostic(context, DiagnosticsDescriptors.TypeMustBeClass, locationOfFirstDeclaration, type); + return; + } + + if (type.ContainingType is not null) // is nested class + { + ReportDiagnostic(context, DiagnosticsDescriptors.TypeCannotBeNestedClass, locationOfFirstDeclaration, type); + return; + } + + EnsureNoPrimaryConstructor(context, type); + TypeMustBePartial(context, type); + TypeMustNotBeGeneric(context, type, locationOfFirstDeclaration, "Union"); + } + private static void ValidateKeyedValueObject(OperationAnalysisContext context, IReadOnlyList? assignableMembers, INamedTypeSymbol type, diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiagnosticsDescriptors.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiagnosticsDescriptors.cs index f63a89ad..b51fa34e 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiagnosticsDescriptors.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiagnosticsDescriptors.cs @@ -29,6 +29,7 @@ internal static class DiagnosticsDescriptors public static readonly DiagnosticDescriptor CustomKeyMemberImplementationNotFound = new("TTRESG044", "Custom implementation of the key member not found", $"Provide a custom implementation of the key member. Implement a field or property '{{0}}'. Use '{Constants.Attributes.Properties.KEY_MEMBER_NAME}' to change the name.", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true); public static readonly DiagnosticDescriptor CustomKeyMemberImplementationTypeMismatch = new("TTRESG045", "Key member type mismatch", "The type of the key member '{0}' must be '{2}' instead of '{1}'", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true); public static readonly DiagnosticDescriptor IndexBasedSwitchAndMapMustUseNamedParameters = new("TTRESG046", "The arguments of \"Switch\" and \"Map\" must named", "Not all arguments of \"Switch/Map\" on the enumeration '{0}' are named", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true); + public static readonly DiagnosticDescriptor TypeMustBeClass = new("TTRESG047", "The type must be a class", "The type '{0}' must be a class", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true); public static readonly DiagnosticDescriptor ErrorDuringCodeAnalysis = new("TTRESG098", "Error during code analysis", "Error during code analysis of '{0}': '{1}'", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Warning, true); public static readonly DiagnosticDescriptor ErrorDuringGeneration = new("TTRESG099", "Error during code generation", "Error during code generation for '{0}': '{1}'", nameof(ThinktectureRuntimeExtensionsAnalyzer), DiagnosticSeverity.Error, true); diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/AllEnumSettings.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/AllEnumSettings.cs new file mode 100644 index 00000000..67c45cda --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/AllEnumSettings.cs @@ -0,0 +1,60 @@ +namespace Thinktecture.CodeAnalysis.DiscriminatedUnions; + +public sealed class AllUnionSettings : IEquatable +{ + public bool SkipToString { get; } + public SwitchMapMethodsGeneration SwitchMethods { get; } + public SwitchMapMethodsGeneration MapMethods { get; } + public IReadOnlyList MemberTypeSettings { get; } + public StringComparison DefaultStringComparison { get; } + + public AllUnionSettings(AttributeData attribute, int numberOfMemberTypes) + { + SkipToString = attribute.FindSkipToString() ?? false; + SwitchMethods = attribute.FindSwitchMethods(); + MapMethods = attribute.FindMapMethods(); + DefaultStringComparison = attribute.FindDefaultStringComparison(); + + var memberTypeSettings = new MemberTypeSetting[numberOfMemberTypes]; + MemberTypeSettings = memberTypeSettings; + + for (var i = 0; i < numberOfMemberTypes; i++) + { + memberTypeSettings[i] = new MemberTypeSetting(attribute.FindTxIsNullableReferenceType(i + 1), + attribute.FindTxName(i + 1)); + } + } + + public override bool Equals(object? obj) + { + return obj is AllUnionSettings enumSettings && Equals(enumSettings); + } + + public bool Equals(AllUnionSettings? other) + { + if (other is null) + return false; + if (ReferenceEquals(this, other)) + return true; + + return SkipToString == other.SkipToString + && SwitchMethods == other.SwitchMethods + && MapMethods == other.MapMethods + && DefaultStringComparison == other.DefaultStringComparison + && MemberTypeSettings.SequenceEqual(other.MemberTypeSettings); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = SkipToString.GetHashCode(); + hashCode = (hashCode * 397) ^ SwitchMethods.GetHashCode(); + hashCode = (hashCode * 397) ^ MapMethods.GetHashCode(); + hashCode = (hashCode * 397) ^ (int)DefaultStringComparison; + hashCode = (hashCode * 397) ^ MemberTypeSettings.ComputeHashCode(); + + return hashCode; + } + } +} diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/MemberTypeSetting.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/MemberTypeSetting.cs new file mode 100644 index 00000000..b62795ea --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/MemberTypeSetting.cs @@ -0,0 +1,34 @@ +namespace Thinktecture.CodeAnalysis.DiscriminatedUnions; + +public readonly struct MemberTypeSetting : IEquatable, IHashCodeComputable +{ + public bool IsNullableReferenceType { get; } + public string? Name { get; } + + public MemberTypeSetting( + bool isNullableReferenceType, + string? name) + { + IsNullableReferenceType = isNullableReferenceType; + Name = name; + } + + public override bool Equals(object? obj) + { + return obj is MemberTypeSetting other && Equals(other); + } + + public bool Equals(MemberTypeSetting other) + { + return IsNullableReferenceType == other.IsNullableReferenceType + && Name == other.Name; + } + + public override int GetHashCode() + { + unchecked + { + return (IsNullableReferenceType.GetHashCode() * 397) ^ (Name?.GetHashCode() ?? 0); + } + } +} diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/MemberTypeState.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/MemberTypeState.cs new file mode 100644 index 00000000..f550a2ad --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/MemberTypeState.cs @@ -0,0 +1,63 @@ +namespace Thinktecture.CodeAnalysis.DiscriminatedUnions; + +public sealed class MemberTypeState : IEquatable, IMemberInformation, ITypeMinimallyQualified, IHashCodeComputable +{ + public string TypeFullyQualified { get; } + public string TypeMinimallyQualified { get; } + public string Name { get; } + public bool IsReferenceType { get; } + public NullableAnnotation NullableAnnotation { get; } + public bool IsNullableStruct { get; } + public SpecialType SpecialType { get; } + + public ArgumentName ArgumentName { get; } + public MemberTypeSetting Setting { get; } + + public MemberTypeState( + INamedTypeSymbol type, + ITypedMemberState typeState, + MemberTypeSetting setting) + { + Name = setting.Name ?? (typeState.IsNullableStruct ? $"Nullable{type.TypeArguments[0].Name}" : type.Name); + TypeFullyQualified = typeState.TypeFullyQualified; + TypeMinimallyQualified = typeState.TypeMinimallyQualified; + IsReferenceType = typeState.IsReferenceType; + NullableAnnotation = typeState.NullableAnnotation; + IsNullableStruct = typeState.IsNullableStruct; + SpecialType = typeState.SpecialType; + + ArgumentName = Name.MakeArgumentName(); + Setting = setting; + } + + public override bool Equals(object? obj) + { + return obj is MemberTypeState other && Equals(other); + } + + public bool Equals(MemberTypeState? other) + { + if (other is null) + return false; + if (ReferenceEquals(this, other)) + return true; + + return TypeFullyQualified == other.TypeFullyQualified + && IsReferenceType == other.IsReferenceType + && SpecialType == other.SpecialType + && Setting.Equals(other.Setting); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = TypeFullyQualified.GetHashCode(); + hashCode = (hashCode * 397) ^ IsReferenceType.GetHashCode(); + hashCode = (hashCode * 397) ^ (int)SpecialType; + hashCode = (hashCode * 397) ^ Setting.GetHashCode(); + + return hashCode; + } + } +} diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionCodeGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionCodeGenerator.cs new file mode 100644 index 00000000..0a7ecff5 --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionCodeGenerator.cs @@ -0,0 +1,772 @@ +using System.Text; + +namespace Thinktecture.CodeAnalysis.DiscriminatedUnions; + +public class UnionCodeGenerator : CodeGeneratorBase +{ + public override string CodeGeneratorName => "Union-CodeGenerator"; + public override string? FileNameSuffix => null; + + private readonly UnionSourceGenState _state; + private readonly StringBuilder _sb; + + public UnionCodeGenerator( + UnionSourceGenState state, + StringBuilder sb) + { + _state = state; + _sb = sb; + } + + public override void Generate(CancellationToken cancellationToken) + { + _sb.AppendLine(GENERATED_CODE_PREFIX); + + var hasNamespace = _state.Namespace is not null; + + if (hasNamespace) + { + _sb.Append(@" +namespace ").Append(_state.Namespace).Append(@" +{"); + } + + GenerateUnion(cancellationToken); + + if (hasNamespace) + { + _sb.Append(@" +}"); + } + + _sb.Append(@" +"); + } + + private void GenerateUnion(CancellationToken cancellationToken) + { + _sb.GenerateStructLayoutAttributeIfRequired(_state.IsReferenceType, _state.Settings.HasStructLayoutAttribute); + + _sb.Append(@" + "); + + _sb.Append(_state.IsReferenceType ? "sealed " : "readonly ").Append("partial ").Append(_state.IsReferenceType ? "class" : "struct").Append(" ").Append(_state.Name).Append(" :") + .Append(@" + global::System.IEquatable<").AppendTypeFullyQualified(_state).Append(@">, + global::System.Numerics.IEqualityOperators<").AppendTypeFullyQualified(_state).Append(@", ").AppendTypeFullyQualified(_state).Append(@", bool> + { + private static readonly int _typeHashCode = typeof(").AppendTypeFullyQualified(_state).Append(@").GetHashCode(); + + private readonly int _valueIndex; +"); // index is 1-based + + GenerateMemberTypeFieldsAndProps(); + GenerateRawValueGetter(); + GenerateConstructors(); + + cancellationToken.ThrowIfCancellationRequested(); + + if (_state.Settings.SwitchMethods != SwitchMapMethodsGeneration.None) + { + GenerateSwitchForAction(false, false); + + if (_state.Settings.SwitchMethods == SwitchMapMethodsGeneration.DefaultWithPartialOverloads) + GenerateSwitchForAction(false, true); + + GenerateSwitchForAction(true, false); + + if (_state.Settings.SwitchMethods == SwitchMapMethodsGeneration.DefaultWithPartialOverloads) + GenerateSwitchForAction(true, true); + + GenerateSwitchForFunc(false, false); + + if (_state.Settings.SwitchMethods == SwitchMapMethodsGeneration.DefaultWithPartialOverloads) + GenerateSwitchForFunc(false, true); + + GenerateSwitchForFunc(true, false); + + if (_state.Settings.SwitchMethods == SwitchMapMethodsGeneration.DefaultWithPartialOverloads) + GenerateSwitchForFunc(true, true); + } + + if (_state.Settings.MapMethods != SwitchMapMethodsGeneration.None) + { + GenerateMap(false); + + if (_state.Settings.MapMethods == SwitchMapMethodsGeneration.DefaultWithPartialOverloads) + GenerateMap(true); + } + + GenerateImplicitConversions(); + GenerateExplicitConversions(); + GenerateEqualityOperators(); + GenerateEquals(); + GenerateGetHashCode(); + + if (!_state.Settings.SkipToString) + GenerateToString(); + + _sb.Append(@" + }"); + } + + private void GenerateImplicitConversions() + { + foreach (var memberType in _state.MemberTypes) + { + _sb.Append(@" + + /// + /// Implicit conversion from type ").AppendTypeMinimallyQualified(memberType).Append(@". + /// + /// Value to covert from. + /// A new instance of converted from . + public static implicit operator ").AppendTypeFullyQualified(_state).Append("(").AppendTypeFullyQualified(memberType).Append(@" value) + { + return new ").AppendTypeFullyQualified(_state).Append(@"(value); + }"); + } + } + + private void GenerateExplicitConversions() + { + foreach (var memberType in _state.MemberTypes) + { + _sb.Append(@" + + /// + /// Implicit conversion to type ").AppendTypeMinimallyQualified(memberType).Append(@". + /// + /// Object to covert. + /// Inner value of type ").AppendTypeMinimallyQualified(memberType).Append(@". + /// If the inner value is not a ").AppendTypeMinimallyQualified(memberType).Append(@". + public static explicit operator ").AppendTypeFullyQualified(memberType).Append("(").AppendTypeFullyQualified(_state).Append(@" obj) + { + return obj.As").Append(memberType.Name).Append(@"; + }"); + } + } + + private void GenerateEqualityOperators() + { + _sb.Append(@" + + /// + /// Compares two instances of . + /// + /// Instance to compare. + /// Another instance to compare. + /// true if objects are equal; otherwise false. + public static bool operator ==(").AppendTypeFullyQualifiedNullAnnotated(_state).Append(" obj, ").AppendTypeFullyQualifiedNullAnnotated(_state).Append(@" other) + {"); + + if (_state.IsReferenceType) + { + _sb.Append(@" + if (obj is null) + return other is null; +"); + } + + _sb.Append(@" + 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 !=(").AppendTypeFullyQualifiedNullAnnotated(_state).Append(" obj, ").AppendTypeFullyQualifiedNullAnnotated(_state).Append(@" other) + { + return !(obj == other); + }"); + } + + private void GenerateToString() + { + _sb.Append(@" + + /// + public override string? ToString() + { + return this._valueIndex switch + {"); + + for (var i = 0; i < _state.MemberTypes.Length; i++) + { + var memberType = _state.MemberTypes[i]; + + _sb.Append(@" + ").Append(i + 1).Append(" => this._").Append(memberType.ArgumentName.Raw); + + if (memberType.SpecialType != SpecialType.System_String) + { + if (memberType.IsReferenceType) + _sb.Append("?"); + + _sb.Append(".ToString()"); + } + + _sb.Append(","); + } + + _sb.Append(@" + _ => throw new global::System.IndexOutOfRangeException($""Unexpected value index '{this._valueIndex}'."") + }; + }"); + } + + private void GenerateGetHashCode() + { + _sb.Append(@" + + /// + public override int GetHashCode() + { + return this._valueIndex switch + {"); + + for (var i = 0; i < _state.MemberTypes.Length; i++) + { + var memberType = _state.MemberTypes[i]; + + _sb.Append(@" + ").Append(i + 1).Append(" => "); + + _sb.Append("global::System.HashCode.Combine(").AppendTypeFullyQualified(_state).Append("._typeHashCode, this._").Append(memberType.ArgumentName.Raw); + + if (memberType.IsReferenceType) + _sb.Append("?"); + + _sb.Append(".GetHashCode("); + + if (memberType.SpecialType == SpecialType.System_String) + _sb.Append("global::System.StringComparison.").Append(Enum.GetName(typeof(StringComparison), _state.Settings.DefaultStringComparison)); + + _sb.Append(")"); + + if (memberType.IsReferenceType) + _sb.Append(" ?? 0"); + + _sb.Append("),"); + } + + _sb.Append(@" + _ => throw new global::System.IndexOutOfRangeException($""Unexpected value index '{this._valueIndex}'."") + }; + }"); + } + + private void GenerateEquals() + { + _sb.Append(@" + + /// + public override bool Equals(object? other) + { + return other is ").AppendTypeFullyQualified(_state).Append(@" obj && Equals(obj); + } + + /// + public bool Equals(").AppendTypeFullyQualifiedNullAnnotated(_state).Append(@" other) + {"); + + if (_state.IsReferenceType) + { + _sb.Append(@" + if (other is null) + return false; + + if (ReferenceEquals(this, other)) + return true; +"); + } + + _sb.Append(@" + if (this._valueIndex != other._valueIndex) + return false; + + return this._valueIndex switch + {"); + + for (var i = 0; i < _state.MemberTypes.Length; i++) + { + var memberType = _state.MemberTypes[i]; + + _sb.Append(@" + ").Append(i + 1).Append(" => "); + + if (memberType.IsReferenceType) + _sb.Append("this._").Append(memberType.ArgumentName.Raw).Append(" is null ? other._").Append(memberType.ArgumentName.Raw).Append(" is null : "); + + _sb.Append("this._").Append(memberType.ArgumentName.Raw).Append(".Equals(other._").Append(memberType.ArgumentName.Raw); + + if (memberType.SpecialType == SpecialType.System_String) + _sb.Append(", global::System.StringComparison.").Append(Enum.GetName(typeof(StringComparison), _state.Settings.DefaultStringComparison)); + + _sb.Append("),"); + } + + _sb.Append(@" + _ => throw new global::System.IndexOutOfRangeException($""Unexpected value index '{this._valueIndex}'."") + }; + }"); + } + + private void GenerateSwitchForAction(bool withContext, bool isPartially) + { + _sb.Append(@" + + /// + /// Executes an action depending on the current value. + /// "); + + if (withContext) + { + _sb.Append(@" + /// Context to be passed to the callbacks."); + } + + if (isPartially) + { + _sb.Append(@" + /// The action to execute if no value-specific action is provided."); + } + + for (var i = 0; i < _state.MemberTypes.Length; i++) + { + var memberType = _state.MemberTypes[i]; + + _sb.Append(@" + /// The action to execute if the current value is of type ").AppendTypeMinimallyQualified(memberType).Append("."); + } + + var methodName = isPartially ? "SwitchPartially" : "Switch"; + + if (withContext) + { + _sb.Append(@" + public void ").Append(methodName).Append(@"( + TContext context,"); + } + else + { + _sb.Append(@" + public void ").Append(methodName).Append("("); + } + + if (isPartially) + { + _sb.Append(@" + global::System.Action<"); + + if (withContext) + _sb.Append("TContext, "); + + _sb.Append("object?>? @default = null,"); + } + + for (var i = 0; i < _state.MemberTypes.Length; i++) + { + var memberType = _state.MemberTypes[i]; + + if (i != 0) + _sb.Append(","); + + _sb.Append(@" + global::System.Action<"); + + if (withContext) + _sb.Append("TContext, "); + + _sb.AppendTypeFullyQualified(memberType).Append(">"); + + if (isPartially) + _sb.Append('?'); + + _sb.Append(' ').Append(memberType.ArgumentName.Escaped); + + if (isPartially) + _sb.Append(" = null"); + } + + _sb.Append(@") + {"); + + GenerateIndexBasedActionSwitchBody(withContext, isPartially); + + _sb.Append(@" + }"); + } + + private void GenerateIndexBasedActionSwitchBody(bool withContext, bool isPartially) + { + _sb.Append(@" + switch (this._valueIndex) + {"); + + for (var i = 0; i < _state.MemberTypes.Length; i++) + { + var memberType = _state.MemberTypes[i]; + + _sb.Append(@" + case ").Append(i + 1).Append(":"); + + if (isPartially) + { + _sb.Append(@" + if (").Append(memberType.ArgumentName.Escaped).Append(@" is null) + break; +"); + } + + _sb.Append(@" + ").Append(memberType.ArgumentName.Escaped).Append("("); + + if (withContext) + _sb.Append("context, "); + + _sb.Append("this._").Append(memberType.ArgumentName.Raw).Append(memberType.IsReferenceType && memberType.NullableAnnotation != NullableAnnotation.Annotated ? "!" : null).Append(@"); + return;"); + } + + _sb.Append(@" + default: + throw new global::System.IndexOutOfRangeException($""Unexpected value index '{this._valueIndex}'.""); + }"); + + if (isPartially) + { + _sb.Append(@" + + @default?.Invoke("); + + if (withContext) + _sb.Append("context, "); + + _sb.Append("this.Value);"); + } + } + + private void GenerateSwitchForFunc(bool withContext, bool isPartially) + { + _sb.Append(@" + + /// + /// Executes a function depending on the current value. + /// "); + + if (withContext) + { + _sb.Append(@" + /// Context to be passed to the callbacks."); + } + + if (isPartially) + { + _sb.Append(@" + /// The function to execute if no value-specific action is provided."); + } + + for (var i = 0; i < _state.MemberTypes.Length; i++) + { + var memberType = _state.MemberTypes[i]; + + _sb.Append(@" + /// The function to execute if the current value is of type ").AppendTypeMinimallyQualified(memberType).Append("."); + } + + var methodName = isPartially ? "SwitchPartially" : "Switch"; + + if (withContext) + { + _sb.Append(@" + public TResult ").Append(methodName).Append(@"( + TContext context,"); + } + else + { + _sb.Append(@" + public TResult ").Append(methodName).Append("("); + } + + if (isPartially) + { + _sb.Append(@" + global::System.Func<"); + + if (withContext) + _sb.Append("TContext, "); + + _sb.Append("object?, TResult> @default,"); + } + + for (var i = 0; i < _state.MemberTypes.Length; i++) + { + var memberType = _state.MemberTypes[i]; + + if (i != 0) + _sb.Append(","); + + _sb.Append(@" + "); + + _sb.Append("global::System.Func<"); + + if (withContext) + _sb.Append("TContext, "); + + _sb.AppendTypeFullyQualified(memberType).Append(", TResult>"); + + if (isPartially) + _sb.Append('?'); + + _sb.Append(' ').Append(memberType.ArgumentName.Escaped); + + if (isPartially) + _sb.Append(" = null"); + } + + _sb.Append(@") + {"); + + GenerateIndexBasedFuncSwitchBody(withContext, isPartially); + + _sb.Append(@" + }"); + } + + private void GenerateIndexBasedFuncSwitchBody(bool withContext, bool isPartially) + { + _sb.Append(@" + switch (this._valueIndex) + {"); + + for (var i = 0; i < _state.MemberTypes.Length; i++) + { + var memberType = _state.MemberTypes[i]; + + _sb.Append(@" + case ").Append(i + 1).Append(":"); + + if (isPartially) + { + _sb.Append(@" + if (").Append(memberType.ArgumentName.Escaped).Append(@" is null) + break; +"); + } + + _sb.Append(@" + return ").Append(memberType.ArgumentName.Escaped).Append("("); + + if (withContext) + _sb.Append("context, "); + + _sb.Append("this._").Append(memberType.ArgumentName.Raw).Append(memberType is { IsReferenceType: true, Setting.IsNullableReferenceType: false } ? "!" : null).Append(");"); + } + + _sb.Append(@" + default: + throw new global::System.IndexOutOfRangeException($""Unexpected value index '{this._valueIndex}'.""); + }"); + + if (isPartially) + { + _sb.Append(@" + + return @default("); + + if (withContext) + _sb.Append("context, "); + + _sb.Append("this.Value);"); + } + } + + private void GenerateMap(bool isPartially) + { + _sb.Append(@" + + /// + /// Maps current value to an instance of type . + /// "); + + if (isPartially) + { + _sb.Append(@" + /// The instance to return if no value is provided for the current value."); + } + + for (var i = 0; i < _state.MemberTypes.Length; i++) + { + var memberType = _state.MemberTypes[i]; + + _sb.Append(@" + /// The instance to return if the current value is of type ").AppendTypeMinimallyQualified(memberType).Append("."); + } + + var methodName = isPartially ? "MapPartially" : "Map"; + + _sb.Append(@" + public TResult ").Append(methodName).Append("("); + + if (isPartially) + { + _sb.Append(@" + TResult @default,"); + } + + for (var i = 0; i < _state.MemberTypes.Length; i++) + { + if (i != 0) + _sb.Append(","); + + _sb.Append(@" + "); + + if (isPartially) + _sb.Append("global::Thinktecture.Argument<"); + + _sb.Append("TResult"); + + if (isPartially) + _sb.Append(">"); + + _sb.Append(" ").Append(_state.MemberTypes[i].ArgumentName.Escaped); + + if (isPartially) + _sb.Append(" = default"); + } + + _sb.Append(@") + {"); + + GenerateIndexBasedMapSwitchBody(isPartially); + + _sb.Append(@" + }"); + } + + private void GenerateIndexBasedMapSwitchBody(bool isPartially) + { + _sb.Append(@" + switch (this._valueIndex) + {"); + + for (var i = 0; i < _state.MemberTypes.Length; i++) + { + _sb.Append(@" + case ").Append(i + 1).Append(":"); + + if (isPartially) + { + _sb.Append(@" + if (!").Append(_state.MemberTypes[i].ArgumentName.Escaped).Append(@".IsSet) + break; +"); + } + + _sb.Append(@" + return ").Append(_state.MemberTypes[i].ArgumentName.Escaped); + + if (isPartially) + _sb.Append(".Value"); + + _sb.Append(";"); + } + + _sb.Append(@" + default: + throw new global::System.ArgumentOutOfRangeException($""Unexpected value index '{this._valueIndex}'.""); + }"); + + if (isPartially) + { + _sb.Append(@" + + return @default;"); + } + } + + private void GenerateConstructors() + { + for (var i = 0; i < _state.MemberTypes.Length; i++) + { + var memberType = _state.MemberTypes[i]; + _sb.Append(@" + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public ").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(@"; + }"); + } + } + + private void GenerateMemberTypeFieldsAndProps() + { + for (var i = 0; i < _state.MemberTypes.Length; i++) + { + var memberType = _state.MemberTypes[i]; + _sb.Append(@" + private readonly ").AppendTypeFullyQualifiedNullAnnotated(memberType).Append(" _").Append(memberType.ArgumentName.Raw).Append(";"); + } + + for (var i = 0; i < _state.MemberTypes.Length; i++) + { + var memberType = _state.MemberTypes[i]; + _sb.Append(@" + + /// + /// Indication whether the current value is of type ").AppendTypeMinimallyQualified(memberType).Append(@". + /// + public bool Is").Append(memberType.Name).Append(" => this._valueIndex == ").Append(i + 1).Append(";"); + } + + for (var i = 0; i < _state.MemberTypes.Length; i++) + { + var memberType = _state.MemberTypes[i]; + _sb.Append(@" + + /// + /// Gets the current value as ").AppendTypeMinimallyQualified(memberType).Append(@". + /// + /// If the current value is not of type ").AppendTypeMinimallyQualified(memberType).Append(@". + public ").AppendTypeFullyQualified(memberType).Append(" As").Append(memberType.Name).Append(" => Is").Append(memberType.Name) + .Append(" ? this._").Append(memberType.ArgumentName.Raw).Append(memberType.IsReferenceType && memberType.NullableAnnotation != NullableAnnotation.Annotated ? "!" : null) + .Append(" : throw new global::System.InvalidOperationException($\"'{nameof(").AppendTypeFullyQualified(_state).Append(")}' is not of type '").AppendTypeMinimallyQualified(memberType).Append("'.\");"); + } + } + + private void GenerateRawValueGetter() + { + var hasNullableTypes = _state.MemberTypes.Any(t => t.IsNullableStruct || t.NullableAnnotation == NullableAnnotation.Annotated); + + _sb.Append(@" + + /// + /// Gets the current value as . + /// + public object").Append(hasNullableTypes ? "?" : null).Append(@" Value => this._valueIndex switch + {"); + + for (var i = 0; i < _state.MemberTypes.Length; i++) + { + var memberType = _state.MemberTypes[i]; + + _sb.Append(@" + ").Append(i + 1).Append(" => this._").Append(memberType.ArgumentName.Raw).Append(memberType.IsReferenceType && !hasNullableTypes ? "!" : null).Append(","); + } + + _sb.Append(@" + _ => throw new global::System.IndexOutOfRangeException($""Unexpected value index '{this._valueIndex}'."") + };"); + } +} diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionCodeGeneratorFactory.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionCodeGeneratorFactory.cs new file mode 100644 index 00000000..4af6dda3 --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionCodeGeneratorFactory.cs @@ -0,0 +1,24 @@ +using System.Text; + +namespace Thinktecture.CodeAnalysis.DiscriminatedUnions; + +public class UnionCodeGeneratorFactory : ICodeGeneratorFactory +{ + public static readonly ICodeGeneratorFactory Instance = new UnionCodeGeneratorFactory(); + + public string CodeGeneratorName => "Union-CodeGenerator"; + + private UnionCodeGeneratorFactory() + { + } + + public CodeGeneratorBase Create(UnionSourceGenState state, StringBuilder stringBuilder) + { + return new UnionCodeGenerator(state, stringBuilder); + } + + public bool Equals(ICodeGeneratorFactory other) + { + return ReferenceEquals(this, other); + } +} diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSettings.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSettings.cs new file mode 100644 index 00000000..0a18c0b9 --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSettings.cs @@ -0,0 +1,47 @@ +namespace Thinktecture.CodeAnalysis.DiscriminatedUnions; + +public readonly struct UnionSettings : IEquatable +{ + private readonly AllUnionSettings _settings; + private readonly AttributeInfo _attributeInfo; + + public bool SkipToString => _settings.SkipToString; + public SwitchMapMethodsGeneration SwitchMethods => _settings.SwitchMethods; + public SwitchMapMethodsGeneration MapMethods => _settings.MapMethods; + public StringComparison DefaultStringComparison => _settings.DefaultStringComparison; + public bool HasStructLayoutAttribute => _attributeInfo.HasStructLayoutAttribute; + + public UnionSettings(AllUnionSettings settings, AttributeInfo attributeInfo) + { + _settings = settings; + _attributeInfo = attributeInfo; + } + + public override bool Equals(object? obj) + { + return obj is UnionSettings enumSettings && Equals(enumSettings); + } + + public bool Equals(UnionSettings other) + { + return SkipToString == other.SkipToString + && SwitchMethods == other.SwitchMethods + && MapMethods == other.MapMethods + && DefaultStringComparison == other.DefaultStringComparison + && HasStructLayoutAttribute == other.HasStructLayoutAttribute; + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = SkipToString.GetHashCode(); + hashCode = (hashCode * 397) ^ SwitchMethods.GetHashCode(); + hashCode = (hashCode * 397) ^ MapMethods.GetHashCode(); + hashCode = (hashCode * 397) ^ (int)DefaultStringComparison; + hashCode = (hashCode * 397) ^ HasStructLayoutAttribute.GetHashCode(); + + return hashCode; + } + } +} diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSourceGenState.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSourceGenState.cs new file mode 100644 index 00000000..f15285d5 --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSourceGenState.cs @@ -0,0 +1,63 @@ +namespace Thinktecture.CodeAnalysis.DiscriminatedUnions; + +public sealed class UnionSourceGenState : ITypeInformation, IEquatable +{ + public string? Namespace { get; } + public string Name { get; } + public string TypeFullyQualified { get; } + public string TypeMinimallyQualified { get; } + public bool IsReferenceType { get; } + public NullableAnnotation NullableAnnotation { get; set; } + public bool IsNullableStruct { get; set; } + public bool IsEqualWithReferenceEquality => false; + + public ImmutableArray MemberTypes { get; } + public UnionSettings Settings { get; } + + public UnionSourceGenState( + INamedTypeSymbol type, + ImmutableArray memberTypes, + UnionSettings settings) + { + MemberTypes = memberTypes; + Settings = settings; + Name = type.Name; + Namespace = type.ContainingNamespace?.IsGlobalNamespace == true ? null : type.ContainingNamespace?.ToString(); + TypeFullyQualified = type.ToFullyQualifiedDisplayString(); + TypeMinimallyQualified = type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); + IsReferenceType = type.IsReferenceType; + NullableAnnotation = type.NullableAnnotation; + IsNullableStruct = type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T; + } + + public override bool Equals(object? obj) + { + return obj is UnionSourceGenState other && Equals(other); + } + + public bool Equals(UnionSourceGenState? other) + { + if (other is null) + return false; + if (ReferenceEquals(this, other)) + return true; + + return TypeFullyQualified == other.TypeFullyQualified + && IsReferenceType == other.IsReferenceType + && Settings.Equals(other.Settings) + && MemberTypes.SequenceEqual(other.MemberTypes); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = TypeFullyQualified.GetHashCode(); + hashCode = (hashCode * 397) ^ IsReferenceType.GetHashCode(); + hashCode = (hashCode * 397) ^ Settings.GetHashCode(); + hashCode = (hashCode * 397) ^ MemberTypes.ComputeHashCode(); + + return hashCode; + } + } +} diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSourceGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSourceGenerator.cs new file mode 100644 index 00000000..f5b89bae --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/DiscriminatedUnions/UnionSourceGenerator.cs @@ -0,0 +1,248 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Thinktecture.CodeAnalysis.DiscriminatedUnions; + +[Generator] +public class UnionSourceGenerator : ThinktectureSourceGeneratorBase, IIncrementalGenerator +{ + public UnionSourceGenerator() + : base(10_000) + { + } + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var options = GetGeneratorOptions(context); + + SetupLogger(context, options); + + InitializeUnionSourceGen(context, options); + } + + private void InitializeUnionSourceGen( + IncrementalGeneratorInitializationContext context, + IncrementalValueProvider options) + { + InitializeUnionSourceGen(context, options, Constants.Attributes.Union.FULL_NAME_2_TYPES); + InitializeUnionSourceGen(context, options, Constants.Attributes.Union.FULL_NAME_3_TYPES); + InitializeUnionSourceGen(context, options, Constants.Attributes.Union.FULL_NAME_4_TYPES); + InitializeUnionSourceGen(context, options, Constants.Attributes.Union.FULL_NAME_5_TYPES); + } + + private void InitializeUnionSourceGen( + IncrementalGeneratorInitializationContext context, + IncrementalValueProvider options, + string fullyQualifiedMetadataName) + { + var unionTypeOrError = context.SyntaxProvider + .ForAttributeWithMetadataName(fullyQualifiedMetadataName, + IsCandidate, + GetSourceGenContextOrNull) + .SelectMany(static (state, _) => state.HasValue + ? [state.Value] + : ImmutableArray.Empty); + + var validStates = unionTypeOrError.SelectMany(static (state, _) => state.ValidState is not null + ? [state.ValidState] + : ImmutableArray.Empty); + + InitializeUnionTypeGeneration(context, validStates, options); + + InitializeErrorReporting(context, unionTypeOrError); + InitializeExceptionReporting(context, unionTypeOrError); + } + + private bool IsCandidate(SyntaxNode syntaxNode, CancellationToken cancellationToken) + { + try + { + return syntaxNode switch + { + ClassDeclarationSyntax classDeclaration when IsUnionCandidate(classDeclaration) => true, + _ => false + }; + } + catch (Exception ex) + { + Logger.LogError("Error during checking whether a syntax node is a discriminated union candidate", exception: ex); + return false; + } + } + + private bool IsUnionCandidate(TypeDeclarationSyntax typeDeclaration) + { + var isCandidate = typeDeclaration.IsPartial() + && !typeDeclaration.IsGeneric(); + + if (isCandidate) + { + Logger.LogDebug("The type declaration is a discriminated union candidate", typeDeclaration); + } + else + { + Logger.LogTrace("The type declaration is not a discriminated union candidate", typeDeclaration); + } + + return isCandidate; + } + + private SourceGenContext? GetSourceGenContextOrNull(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) + { + var tds = (TypeDeclarationSyntax)context.TargetNode; + + try + { + var type = (INamedTypeSymbol)context.TargetSymbol; + + if (type.TypeKind == TypeKind.Error) + { + Logger.LogDebug("Type from semantic model is erroneous", tds); + return null; + } + + if (type.ContainingType is not null) + { + Logger.LogDebug("Nested types are not supported", tds); + return null; + } + + if (context.Attributes.Length > 1) + { + Logger.LogDebug($"Type has more than 1 '{Constants.Attributes.Union.NAME}'", tds); + return null; + } + + var attributetype = context.Attributes[0].AttributeClass; + + if (attributetype is null) + { + Logger.LogDebug("The attribute type is null", tds); + return null; + } + + if (attributetype.TypeKind == TypeKind.Error) + { + Logger.LogDebug("The attribute type is erroneous", tds); + return null; + } + + if (attributetype.TypeArguments.Length < 2) + { + Logger.LogDebug($"Expected the attribute type to have at least 2 type arguments but found {attributetype.TypeArguments.Length.ToString()}", tds); + return null; + } + + var errorMessage = AttributeInfo.TryCreate(type, out var attributeInfo); + + if (errorMessage is not null) + { + Logger.LogDebug(errorMessage, tds); + return null; + } + + var factory = TypedMemberStateFactoryProvider.GetFactoryOrNull(context.SemanticModel.Compilation, Logger); + + if (factory is null) + return new SourceGenContext(new SourceGenError("Could not fetch type information for code generation of a discriminated union", tds)); + + var settings = new AllUnionSettings(context.Attributes[0], attributetype.TypeArguments.Length); + var memberTypeStates = ImmutableArray.Empty; + + for (var i = 0; i < attributetype.TypeArguments.Length; i++) + { + var memberType = attributetype.TypeArguments[i]; + + if (memberType.TypeKind == TypeKind.Error) + { + Logger.LogDebug("Type of the member is erroneous", tds); + return null; + } + + if (memberType is not INamedTypeSymbol namedMemberType) + { + Logger.LogDebug("Type of the member must be a named type", tds); + return null; + } + + var memberTypeSettings = settings.MemberTypeSettings[i]; + memberType = memberType.IsReferenceType && memberTypeSettings.IsNullableReferenceType ? memberType.WithNullableAnnotation(NullableAnnotation.Annotated) : memberType; + var typeState = factory.Create(memberType); + + var memberTypeState = new MemberTypeState(namedMemberType, typeState, memberTypeSettings); + memberTypeStates = memberTypeStates.Add(memberTypeState); + } + + var unionState = new UnionSourceGenState(type, + memberTypeStates, + new UnionSettings(settings, attributeInfo)); + + Logger.LogDebug("The type declaration is a valid union", null, unionState); + + return new SourceGenContext(unionState); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception ex) + { + Logger.LogError("Error during extraction of relevant information out of semantic model for generation of a discriminated union", tds, ex); + + return new SourceGenContext(new SourceGenException(ex, tds)); + } + } + + private void InitializeUnionTypeGeneration( + IncrementalGeneratorInitializationContext context, + IncrementalValuesProvider validStates, + IncrementalValueProvider options) + { + var unionTypes = validStates + .Collect() + .Select(static (states, _) => states.IsDefaultOrEmpty + ? ImmutableArray.Empty + : states.Distinct(TypeOnlyComparer.Instance)) + .WithComparer(new SetComparer()) + .SelectMany((states, _) => states); + + context.RegisterSourceOutput(unionTypes.Combine(options), (ctx, tuple) => GenerateCode(ctx, tuple.Left, tuple.Right, UnionCodeGeneratorFactory.Instance)); + } + + private void InitializeErrorReporting( + IncrementalGeneratorInitializationContext context, + IncrementalValuesProvider unionTypeOrException) + { + var exceptions = unionTypeOrException.SelectMany(static (state, _) => state.Error is not null + ? [state.Error.Value] + : ImmutableArray.Empty); + context.RegisterSourceOutput(exceptions, ReportError); + } + + private void InitializeExceptionReporting( + IncrementalGeneratorInitializationContext context, + IncrementalValuesProvider unionTypeOrException) + { + var exceptions = unionTypeOrException.SelectMany(static (state, _) => state.Exception is not null + ? [state.Exception.Value] + : ImmutableArray.Empty); + context.RegisterSourceOutput(exceptions, ReportException); + } + + private readonly record struct SourceGenContext(UnionSourceGenState? ValidState, SourceGenException? Exception, SourceGenError? Error) + { + public SourceGenContext(UnionSourceGenState validState) + : this(validState, null, null) + { + } + + public SourceGenContext(SourceGenException exception) + : this(null, exception, null) + { + } + + public SourceGenContext(SourceGenError errorMessage) + : this(null, null, errorMessage) + { + } + } +} diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/EqualityComparisonOperatorsCodeGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/EqualityComparisonOperatorsCodeGenerator.cs index ca920757..4c37e9a5 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/EqualityComparisonOperatorsCodeGenerator.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/EqualityComparisonOperatorsCodeGenerator.cs @@ -71,7 +71,7 @@ private static void GenerateUsingEquals(StringBuilder sb, EqualityComparisonOper { sb.Append(@" /// - /// Compares two instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -105,7 +105,7 @@ private static void GenerateUsingEquals(StringBuilder sb, EqualityComparisonOper } /// - /// Compares two instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -175,7 +175,7 @@ private static bool Equals(").AppendTypeFullyQualifiedNullAnnotated(state.Type). } /// - /// Compares an instance of with . + /// Compares an instance of with . /// /// Instance to compare. /// Value to compare with. @@ -186,7 +186,7 @@ private static bool Equals(").AppendTypeFullyQualifiedNullAnnotated(state.Type). } /// - /// Compares an instance of with . + /// Compares an instance of with . /// /// Value to compare. /// Instance to compare with. @@ -197,7 +197,7 @@ private static bool Equals(").AppendTypeFullyQualifiedNullAnnotated(state.Type). } /// - /// Compares an instance of with . + /// Compares an instance of with . /// /// Instance to compare. /// Value to compare with. @@ -208,7 +208,7 @@ private static bool Equals(").AppendTypeFullyQualifiedNullAnnotated(state.Type). } /// - /// Compares an instance of with . + /// Compares an instance of with . /// /// Value to compare. /// Instance to compare with. diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ITypeInformation.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ITypeInformation.cs index 781a5e00..09ba21b8 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ITypeInformation.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ITypeInformation.cs @@ -1,7 +1,6 @@ namespace Thinktecture.CodeAnalysis; -public interface ITypeInformation : INamespaceAndName, ITypeInformationWithNullability +public interface ITypeInformation : INamespaceAndName, ITypeInformationWithNullability, ITypeMinimallyQualified { - string TypeMinimallyQualified { get; } bool IsEqualWithReferenceEquality { get; } } diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ITypeMinimallyQualified.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ITypeMinimallyQualified.cs new file mode 100644 index 00000000..bb683ca9 --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ITypeMinimallyQualified.cs @@ -0,0 +1,6 @@ +namespace Thinktecture.CodeAnalysis; + +public interface ITypeMinimallyQualified +{ + string TypeMinimallyQualified { get; } +} diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/SmartEnums/SmartEnumCodeGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/SmartEnums/SmartEnumCodeGenerator.cs index 38de7da3..0a9073a9 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/SmartEnums/SmartEnumCodeGenerator.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/SmartEnums/SmartEnumCodeGenerator.cs @@ -809,7 +809,7 @@ private void GenerateTryGet(IMemberState keyProperty) /// Gets a valid enumeration item for provided if a valid item exists. /// /// The identifier to return an enumeration item for. - /// An instance of . + /// An instance of . /// true if a valid item with provided exists; false otherwise. public static bool TryGet([global::System.Diagnostics.CodeAnalysis.AllowNull] ").AppendTypeFullyQualified(keyProperty).Append(" ").Append(keyProperty.ArgumentName.Escaped).Append(", [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out ").AppendTypeFullyQualified(_state).Append(@" item) {"); @@ -855,7 +855,7 @@ private void GenerateValidate(IMemberState keyProperty) /// /// The identifier to return an enumeration item for. /// An object that provides culture-specific formatting information. - /// An instance of . + /// An instance of . /// null if a valid item with provided exists; with an error message otherwise. public static ").AppendTypeFullyQualified(_state.ValidationError).Append("? Validate([global::System.Diagnostics.CodeAnalysis.AllowNull] ").AppendTypeFullyQualified(keyProperty).Append(" ").Append(keyProperty.ArgumentName.Escaped).Append(", global::System.IFormatProvider? ").Append(providerArgumentName).Append(", [global::System.Diagnostics.CodeAnalysis.MaybeNull] out ").AppendTypeFullyQualified(_state).Append(@" item) { @@ -882,7 +882,7 @@ private void GenerateValidate(IMemberState keyProperty) } _sb.Append(@" - return global::Thinktecture.Internal.ValidationErrorCreator.CreateValidationError<").AppendTypeFullyQualified(_state.ValidationError).Append(@">($""There is no item of type '").Append(_state.TypeMinimallyQualified).Append("' with the identifier '{").Append(keyProperty.ArgumentName.Escaped).Append(@"}'.""); + return global::Thinktecture.Internal.ValidationErrorCreator.CreateValidationError<").AppendTypeFullyQualified(_state.ValidationError).Append(@">($""There is no item of type '").AppendTypeMinimallyQualified(_state).Append("' with the identifier '{").Append(keyProperty.ArgumentName.Escaped).Append(@"}'.""); } }"); } @@ -895,7 +895,7 @@ private void GenerateImplicitConversion(IMemberState keyProperty) /// Implicit conversion to the type . /// /// Item to covert. - /// The of provided or default if is null. + /// The of provided or default if is null. [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(""item"")] public static implicit operator ").AppendTypeFullyQualifiedNullAnnotated(keyProperty).Append("(").AppendTypeFullyQualifiedNullAnnotated(_state).Append(@" item) {"); @@ -923,7 +923,7 @@ private void GenerateExplicitConversion(IMemberState keyProperty) /// Explicit conversion from the type . /// /// Value to covert. - /// An instance of if the is a known item or implements . + /// An instance of if the is a known item or implements . [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(""").Append(keyProperty.ArgumentName.Escaped).Append(@""")] public static explicit operator ").AppendTypeFullyQualifiedNullAnnotated(_state).Append("(").AppendTypeFullyQualifiedNullAnnotated(keyProperty).Append(" ").Append(keyProperty.ArgumentName.Escaped).Append(@") { @@ -1022,7 +1022,7 @@ void AddItem(").AppendTypeFullyQualified(_state).Append(@" item, string itemName { _sb.Append(@" if (item is null) - throw new global::System.ArgumentNullException($""The item \""{itemName}\"" of type \""").Append(_state.TypeMinimallyQualified).Append(@"\"" must not be null.""); + throw new global::System.ArgumentNullException($""The item \""{itemName}\"" of type \""").AppendTypeMinimallyQualified(_state).Append(@"\"" must not be null.""); "); } @@ -1030,7 +1030,7 @@ void AddItem(").AppendTypeFullyQualified(_state).Append(@" item, string itemName { _sb.Append(@" if (item.").Append(keyMember.Name).Append(@" is null) - throw new global::System.ArgumentException($""The \""").Append(keyMember.Name).Append(@"\"" of the item \""{itemName}\"" of type \""").Append(_state.TypeMinimallyQualified).Append(@"\"" must not be null.""); + throw new global::System.ArgumentException($""The \""").Append(keyMember.Name).Append(@"\"" of the item \""{itemName}\"" of type \""").AppendTypeMinimallyQualified(_state).Append(@"\"" must not be null.""); "); } @@ -1038,13 +1038,13 @@ void AddItem(").AppendTypeFullyQualified(_state).Append(@" item, string itemName { _sb.Append(@" if (!item.IsValid) - throw new global::System.ArgumentException($""All \""public static readonly\"" fields of type \""").Append(_state.TypeMinimallyQualified).Append(@"\"" must be valid but the item \""{itemName}\"" with the identifier \""{item.").Append(keyMember.Name).Append(@"}\"" is not.""); + throw new global::System.ArgumentException($""All \""public static readonly\"" fields of type \""").AppendTypeMinimallyQualified(_state).Append(@"\"" must be valid but the item \""{itemName}\"" with the identifier \""{item.").Append(keyMember.Name).Append(@"}\"" is not.""); "); } _sb.Append(@" if (lookup.ContainsKey(item.").Append(keyMember.Name).Append(@")) - throw new global::System.ArgumentException($""The type \""").Append(_state.TypeMinimallyQualified).Append(@"\"" has multiple items with the identifier \""{item.").Append(keyMember.Name).Append(@"}\"".""); + throw new global::System.ArgumentException($""The type \""").AppendTypeMinimallyQualified(_state).Append(@"\"" has multiple items with the identifier \""{item.").Append(keyMember.Name).Append(@"}\"".""); lookup.Add(item.").Append(keyMember.Name).Append(@", item); } @@ -1097,7 +1097,7 @@ private void GenerateGetItems() void AddItem(").AppendTypeFullyQualified(_state).Append(@" item, string itemName) { if (item is null) - throw new global::System.ArgumentNullException($""The item \""{itemName}\"" of type \""").Append(_state.TypeMinimallyQualified).Append(@"\"" must not be null.""); + throw new global::System.ArgumentNullException($""The item \""{itemName}\"" of type \""").AppendTypeMinimallyQualified(_state).Append(@"\"" must not be null.""); list.Add(item); } @@ -1155,7 +1155,7 @@ private void GenerateGet(IMemberState keyProperty) /// Gets an enumeration item for provided . /// /// The identifier to return an enumeration item for. - /// An instance of if is not null; otherwise null."); + /// An instance of if is not null; otherwise null."); if (!_state.Settings.IsValidatable) { diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/TypeInformationComparer.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/TypeInformationComparer.cs index 49fb23cd..6da965cc 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/TypeInformationComparer.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/TypeInformationComparer.cs @@ -12,7 +12,7 @@ public bool Equals(ITypeInformation? x, ITypeInformation? y) if (y is null) return false; - return x.TypeMinimallyQualified == y.TypeMinimallyQualified + return x.TypeFullyQualified == y.TypeFullyQualified && x.IsReferenceType == y.IsReferenceType; } @@ -20,7 +20,7 @@ public int GetHashCode(ITypeInformation obj) { unchecked { - var hashCode = obj.TypeMinimallyQualified.GetHashCode(); + var hashCode = obj.TypeFullyQualified.GetHashCode(); hashCode = (hashCode * 397) ^ obj.IsReferenceType.GetHashCode(); return hashCode; diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/TypeOnlyComparer.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/TypeOnlyComparer.cs index 6bba5218..7c7e836b 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/TypeOnlyComparer.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/TypeOnlyComparer.cs @@ -1,3 +1,4 @@ +using Thinktecture.CodeAnalysis.DiscriminatedUnions; using Thinktecture.CodeAnalysis.SmartEnums; using Thinktecture.CodeAnalysis.ValueObjects; @@ -13,7 +14,8 @@ public class TypeOnlyComparer IEqualityComparer, IEqualityComparer, IEqualityComparer, - IEqualityComparer + IEqualityComparer, + IEqualityComparer { public static readonly TypeOnlyComparer Instance = new(); @@ -27,6 +29,7 @@ public class TypeOnlyComparer public bool Equals(SmartEnumDerivedTypes x, SmartEnumDerivedTypes y) => x.TypeFullyQualified == y.TypeFullyQualified; public bool Equals(KeyedValueObjectSourceGeneratorState x, KeyedValueObjectSourceGeneratorState y) => x.TypeFullyQualified == y.TypeFullyQualified; public bool Equals(ComplexValueObjectSourceGeneratorState x, ComplexValueObjectSourceGeneratorState y) => x.TypeFullyQualified == y.TypeFullyQualified; + public bool Equals(UnionSourceGenState x, UnionSourceGenState y) => x.TypeFullyQualified == y.TypeFullyQualified; public int GetHashCode(FormattableGeneratorState obj) => obj.Type.TypeFullyQualified.GetHashCode(); public int GetHashCode(ComparableGeneratorState obj) => obj.Type.TypeFullyQualified.GetHashCode(); @@ -38,6 +41,7 @@ public class TypeOnlyComparer public int GetHashCode(SmartEnumDerivedTypes obj) => obj.TypeFullyQualified.GetHashCode(); public int GetHashCode(KeyedValueObjectSourceGeneratorState obj) => obj.TypeFullyQualified.GetHashCode(); public int GetHashCode(ComplexValueObjectSourceGeneratorState obj) => obj.TypeFullyQualified.GetHashCode(); + public int GetHashCode(UnionSourceGenState obj) => obj.TypeFullyQualified.GetHashCode(); private TypeOnlyComparer() { diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectCodeGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectCodeGenerator.cs index 47985b86..dc4a3d69 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectCodeGenerator.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectCodeGenerator.cs @@ -304,7 +304,7 @@ private void GenerateEqualityOperators() _sb.Append(@" /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -325,7 +325,7 @@ private void GenerateEqualityOperators() } /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -567,7 +567,7 @@ public override string ToString() else { _sb.Append(@" - return """).Append(_state.TypeMinimallyQualified).Append(@""";"); + return """).AppendTypeMinimallyQualified(_state).Append(@""";"); } _sb.Append(@" diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectJsonCodeGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectJsonCodeGenerator.cs index 21d710ab..496a4922 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectJsonCodeGenerator.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectJsonCodeGenerator.cs @@ -80,7 +80,7 @@ public ValueObjectJsonConverter(global::System.Text.Json.JsonSerializerOptions o return default; if (reader.TokenType != global::System.Text.Json.JsonTokenType.StartObject) - throw new global::System.Text.Json.JsonException($""Unexpected token \""{reader.TokenType}\"" when trying to deserialize \""").Append(_type.TypeMinimallyQualified).Append(@"\"". Expected token: \""{(global::System.Text.Json.JsonTokenType.StartObject)}\"".""); + throw new global::System.Text.Json.JsonException($""Unexpected token \""{reader.TokenType}\"" when trying to deserialize \""").AppendTypeMinimallyQualified(_type).Append(@"\"". Expected token: \""{(global::System.Text.Json.JsonTokenType.StartObject)}\"".""); "); cancellationToken.ThrowIfCancellationRequested(); @@ -103,12 +103,12 @@ public ValueObjectJsonConverter(global::System.Text.Json.JsonSerializerOptions o break; if (reader.TokenType != global::System.Text.Json.JsonTokenType.PropertyName) - throw new global::System.Text.Json.JsonException($""Unexpected token \""{reader.TokenType}\"" when trying to deserialize \""").Append(_type.TypeMinimallyQualified).Append(@"\"". Expected token: \""{(global::System.Text.Json.JsonTokenType.PropertyName)}\"".""); + throw new global::System.Text.Json.JsonException($""Unexpected token \""{reader.TokenType}\"" when trying to deserialize \""").AppendTypeMinimallyQualified(_type).Append(@"\"". Expected token: \""{(global::System.Text.Json.JsonTokenType.PropertyName)}\"".""); var propName = reader.GetString(); if(!reader.Read()) - throw new global::System.Text.Json.JsonException($""Unexpected end of the JSON message when trying the read the value of \""{propName}\"" during deserialization of \""").Append(_type.TypeMinimallyQualified).Append(@"\"".""); + throw new global::System.Text.Json.JsonException($""Unexpected end of the JSON message when trying the read the value of \""{propName}\"" during deserialization of \""").AppendTypeMinimallyQualified(_type).Append(@"\"".""); "); cancellationToken.ThrowIfCancellationRequested(); @@ -139,7 +139,7 @@ public ValueObjectJsonConverter(global::System.Text.Json.JsonSerializerOptions o _sb.Append(@" else { - throw new global::System.Text.Json.JsonException($""Unknown member \""{propName}\"" encountered when trying to deserialize \""").Append(_type.TypeMinimallyQualified).Append(@"\"".""); + throw new global::System.Text.Json.JsonException($""Unknown member \""{propName}\"" encountered when trying to deserialize \""").AppendTypeMinimallyQualified(_type).Append(@"\"".""); }"); } diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectMessagePackCodeGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectMessagePackCodeGenerator.cs index bdad21a0..f33d6468 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectMessagePackCodeGenerator.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectMessagePackCodeGenerator.cs @@ -87,7 +87,7 @@ public sealed class ValueObjectMessagePackFormatter : global::MessagePack.Format out var obj); if (validationError is not null) - throw new global::MessagePack.MessagePackSerializationException(validationError.ToString() ?? ""Unable to deserialize \""").Append(_type.TypeMinimallyQualified).Append(@"\"".""); + throw new global::MessagePack.MessagePackSerializationException(validationError.ToString() ?? ""Unable to deserialize \""").AppendTypeMinimallyQualified(_type).Append(@"\"".""); return obj; } diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectNewtonsoftJsonCodeGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectNewtonsoftJsonCodeGenerator.cs index 24cfed9a..a3f8423e 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectNewtonsoftJsonCodeGenerator.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/ComplexValueObjectNewtonsoftJsonCodeGenerator.cs @@ -68,7 +68,7 @@ public override bool CanConvert(global::System.Type objectType) var (lineNumber, linePosition) = GetLineInfo(reader); throw new global::Newtonsoft.Json.JsonReaderException( - $""Unexpected token \""{reader.TokenType}\"" when trying to deserialize \""").Append(_type.TypeMinimallyQualified).Append(@"\"". Expected token: \""{(global::Newtonsoft.Json.JsonToken.StartObject)}\""."", + $""Unexpected token \""{reader.TokenType}\"" when trying to deserialize \""").AppendTypeMinimallyQualified(_type).Append(@"\"". Expected token: \""{(global::Newtonsoft.Json.JsonToken.StartObject)}\""."", reader.Path, lineNumber, linePosition, @@ -98,7 +98,7 @@ public override bool CanConvert(global::System.Type objectType) var (lineNumber, linePosition) = GetLineInfo(reader); throw new global::Newtonsoft.Json.JsonReaderException( - $""Unexpected token \""{reader.TokenType}\"" when trying to deserialize \""").Append(_type.TypeMinimallyQualified).Append(@"\"". Expected token: \""{(global::Newtonsoft.Json.JsonToken.PropertyName)}\""."", + $""Unexpected token \""{reader.TokenType}\"" when trying to deserialize \""").AppendTypeMinimallyQualified(_type).Append(@"\"". Expected token: \""{(global::Newtonsoft.Json.JsonToken.PropertyName)}\""."", reader.Path, lineNumber, linePosition, @@ -112,7 +112,7 @@ public override bool CanConvert(global::System.Type objectType) var (lineNumber, linePosition) = GetLineInfo(reader); throw new global::Newtonsoft.Json.JsonReaderException( - $""Unexpected end of the JSON message when trying the read the value of \""{propName}\"" during deserialization of \""").Append(_type.TypeMinimallyQualified).Append(@"\""."", + $""Unexpected end of the JSON message when trying the read the value of \""{propName}\"" during deserialization of \""").AppendTypeMinimallyQualified(_type).Append(@"\""."", reader.Path, lineNumber, linePosition, @@ -151,7 +151,7 @@ public override bool CanConvert(global::System.Type objectType) var (lineNumber, linePosition) = GetLineInfo(reader); throw new global::Newtonsoft.Json.JsonReaderException( - $""Unknown member \""{propName}\"" encountered when trying to deserialize \""").Append(_type.TypeMinimallyQualified).Append(@"\""."", + $""Unknown member \""{propName}\"" encountered when trying to deserialize \""").AppendTypeMinimallyQualified(_type).Append(@"\""."", reader.Path, lineNumber, linePosition, @@ -182,7 +182,7 @@ public override bool CanConvert(global::System.Type objectType) var (lineNumber, linePosition) = GetLineInfo(reader); throw new global::Newtonsoft.Json.JsonSerializationException( - validationError.ToString() ?? ""Unable to deserialize \""").Append(_type.TypeMinimallyQualified).Append(@"\""."", + validationError.ToString() ?? ""Unable to deserialize \""").AppendTypeMinimallyQualified(_type).Append(@"\""."", reader.Path, lineNumber, linePosition, diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/KeyedValueObjectCodeGenerator.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/KeyedValueObjectCodeGenerator.cs index 2f66c8a9..380436bf 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/KeyedValueObjectCodeGenerator.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/CodeAnalysis/ValueObjects/KeyedValueObjectCodeGenerator.cs @@ -264,7 +264,7 @@ private void GenerateExplicitConversion(bool emptyStringYieldsNull) /// Explicit conversion from the type . /// /// Value to covert. - /// An instance of ."); + /// An instance of ."); if (bothAreReferenceTypes && !emptyStringYieldsNull) { diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs index b3f7b836..5403065b 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/AttributeDataExtensions.cs @@ -149,6 +149,26 @@ public static SerializationFrameworks FindUseForSerialization(this AttributeData return frameworks.Value; } + public static StringComparison FindDefaultStringComparison(this AttributeData attributeData) + { + var frameworks = (StringComparison?)GetIntegerParameterValue(attributeData, "DefaultStringComparison"); + + if (frameworks is null || !frameworks.Value.IsValid()) + return StringComparison.OrdinalIgnoreCase; + + return frameworks.Value; + } + + public static bool FindTxIsNullableReferenceType(this AttributeData attributeData, int index) + { + return GetBooleanParameterValue(attributeData, $"T{index}IsNullableReferenceType") ?? false; + } + + public static string? FindTxName(this AttributeData attributeData, int index) + { + return GetStringParameterValue(attributeData, $"T{index}Name"); + } + 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 a6281807..f9844703 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/StringBuilderExtensions.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/StringBuilderExtensions.cs @@ -121,6 +121,13 @@ public static StringBuilder AppendCast( return sb; } + public static StringBuilder AppendTypeMinimallyQualified( + this StringBuilder sb, + ITypeMinimallyQualified type) + { + return sb.Append(type.TypeMinimallyQualified); + } + public static StringBuilder AppendTypeFullyQualified( this StringBuilder sb, ITypeFullyQualified type) diff --git a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/TypeSymbolExtensions.cs b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/TypeSymbolExtensions.cs index 256f3f1a..dfd4c51f 100644 --- a/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/TypeSymbolExtensions.cs +++ b/src/Thinktecture.Runtime.Extensions.SourceGenerator/Extensions/TypeSymbolExtensions.cs @@ -45,6 +45,14 @@ public static bool IsSmartEnumAttribute(this ITypeSymbol? attributeType) return attributeType is { Name: Constants.Attributes.SmartEnum.NAME, ContainingNamespace: { Name: Constants.Attributes.SmartEnum.NAMESPACE, ContainingNamespace.IsGlobalNamespace: true } }; } + public static bool IsUnionAttribute(this ITypeSymbol? attributeType) + { + if (attributeType is null || attributeType.TypeKind == TypeKind.Error) + return false; + + return attributeType is { Name: Constants.Attributes.Union.NAME, ContainingNamespace: { Name: Constants.Attributes.Union.NAMESPACE, ContainingNamespace.IsGlobalNamespace: true } }; + } + public static bool IsValueObjectMemberEqualityComparerAttribute(this ITypeSymbol? attributeType) { if (attributeType is null || attributeType.TypeKind == TypeKind.Error) diff --git a/src/Thinktecture.Runtime.Extensions/UnionAttribute.cs b/src/Thinktecture.Runtime.Extensions/UnionAttribute.cs new file mode 100644 index 00000000..0c6c6585 --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions/UnionAttribute.cs @@ -0,0 +1,211 @@ +namespace Thinktecture; + +/// +/// Marks a type as a discriminated union. +/// +/// One of the types of the discriminated union. +/// One of the types of the discriminated union. +[AttributeUsage(AttributeTargets.Class)] +public class UnionAttribute : UnionAttributeBase +{ + /// + /// Changes the name of all members regarding . + /// By default, the type name is used. + /// + public string? T1Name { get; set; } + + /// + /// Makes the type argument a nullable reference type. + /// This setting has no effect if is a struct. + /// + public bool T1IsNullableReferenceType { get; set; } + + /// + /// Changes the name of all members regarding . + /// By default, the type name is used. + /// + public string? T2Name { get; set; } + + /// + /// Makes the type argument a nullable reference type. + /// This setting has no effect if is a struct. + /// + public bool T2IsNullableReferenceType { get; set; } +} + +/// +/// Marks a type as a discriminated union. +/// +/// One of the types of the discriminated union. +/// One of the types of the discriminated union. +/// One of the types of the discriminated union. +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] +public class UnionAttribute : UnionAttributeBase +{ + /// + /// Changes the name of all members regarding . + /// By default, the type name is used. + /// + public string? T1Name { get; set; } + + /// + /// Makes the type argument a nullable reference type. + /// This setting has no effect if is a struct. + /// + public bool T1IsNullableReferenceType { get; set; } + + /// + /// Changes the name of all members regarding . + /// By default, the type name is used. + /// + public string? T2Name { get; set; } + + /// + /// Makes the type argument a nullable reference type. + /// This setting has no effect if is a struct. + /// + public bool T2IsNullableReferenceType { get; set; } + + /// + /// Changes the name of all members regarding . + /// By default, the type name is used. + /// + public string? T3Name { get; set; } + + /// + /// Makes the type argument a nullable reference type. + /// This setting has no effect if is a struct. + /// + public bool T3IsNullableReferenceType { get; set; } +} + +/// +/// Marks a type as a discriminated union. +/// +/// One of the types of the discriminated union. +/// One of the types of the discriminated union. +/// One of the types of the discriminated union. +/// One of the types of the discriminated union. +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] +public class UnionAttribute : UnionAttributeBase +{ + /// + /// Changes the name of all members regarding . + /// By default, the type name is used. + /// + public string? T1Name { get; set; } + + /// + /// Makes the type argument a nullable reference type. + /// This setting has no effect if is a struct. + /// + public bool T1IsNullableReferenceType { get; set; } + + /// + /// Changes the name of all members regarding . + /// By default, the type name is used. + /// + public string? T2Name { get; set; } + + /// + /// Makes the type argument a nullable reference type. + /// This setting has no effect if is a struct. + /// + public bool T2IsNullableReferenceType { get; set; } + + /// + /// Changes the name of all members regarding . + /// By default, the type name is used. + /// + public string? T3Name { get; set; } + + /// + /// Makes the type argument a nullable reference type. + /// This setting has no effect if is a struct. + /// + public bool T3IsNullableReferenceType { get; set; } + + /// + /// Changes the name of all members regarding . + /// By default, the type name is used. + /// + public string? T4Name { get; set; } + + /// + /// Makes the type argument a nullable reference type. + /// This setting has no effect if is a struct. + /// + public bool T4IsNullableReferenceType { get; set; } +} + +/// +/// Marks a type as a discriminated union. +/// +/// One of the types of the discriminated union. +/// One of the types of the discriminated union. +/// One of the types of the discriminated union. +/// One of the types of the discriminated union. +/// One of the types of the discriminated union. +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] +public class UnionAttribute : UnionAttributeBase +{ + /// + /// Changes the name of all members regarding . + /// By default, the type name is used. + /// + public string? T1Name { get; set; } + + /// + /// Makes the type argument a nullable reference type. + /// This setting has no effect if is a struct. + /// + public bool T1IsNullableReferenceType { get; set; } + + /// + /// Changes the name of all members regarding . + /// By default, the type name is used. + /// + public string? T2Name { get; set; } + + /// + /// Makes the type argument a nullable reference type. + /// This setting has no effect if is a struct. + /// + public bool T2IsNullableReferenceType { get; set; } + + /// + /// Changes the name of all members regarding . + /// By default, the type name is used. + /// + public string? T3Name { get; set; } + + /// + /// Makes the type argument a nullable reference type. + /// This setting has no effect if is a struct. + /// + public bool T3IsNullableReferenceType { get; set; } + + /// + /// Changes the name of all members regarding . + /// By default, the type name is used. + /// + public string? T4Name { get; set; } + + /// + /// Makes the type argument a nullable reference type. + /// This setting has no effect if is a struct. + /// + public bool T4IsNullableReferenceType { get; set; } + + /// + /// Changes the name of all members regarding . + /// By default, the type name is used. + /// + public string? T5Name { get; set; } + + /// + /// Makes the type argument a nullable reference type. + /// This setting has no effect if is a struct. + /// + public bool T5IsNullableReferenceType { get; set; } +} diff --git a/src/Thinktecture.Runtime.Extensions/UnionAttributeBase.cs b/src/Thinktecture.Runtime.Extensions/UnionAttributeBase.cs new file mode 100644 index 00000000..c959dbee --- /dev/null +++ b/src/Thinktecture.Runtime.Extensions/UnionAttributeBase.cs @@ -0,0 +1,28 @@ +namespace Thinktecture; + +/// +/// Base class for marking a type as a discriminated union. +/// +public abstract class UnionAttributeBase : Attribute +{ + /// + /// Defines the . + /// Default is . + /// + public StringComparison DefaultStringComparison { get; set; } = StringComparison.OrdinalIgnoreCase; + + /// + /// Indication whether the generator should skip the implementation of the method or not. + /// + public bool SkipToString { get; set; } + + /// + /// Indication whether and how the generator should generate the methods Switch. + /// + public SwitchMapMethodsGeneration SwitchMethods { get; set; } + + /// + /// Indication whether and how the generator should generate the methods Map. + /// + public SwitchMapMethodsGeneration MapMethods { get; set; } +} diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG006_TypeMustBePartial.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG006_TypeMustBePartial.cs index 41b8666a..a6334543 100644 --- a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG006_TypeMustBePartial.cs +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG006_TypeMustBePartial.cs @@ -352,4 +352,54 @@ public partial struct {|#0:TestValueObject|} await Verifier.VerifyAnalyzerAsync(code, new[] { typeof(ComplexValueObjectAttribute).Assembly }); } } + + public class Union_must_be_partial + { + [Fact] + public async Task Should_trigger_on_non_partial_class() + { + var code = """ + using System; + using Thinktecture; + + namespace TestNamespace + { + [Union] + public class {|#0:TestUnion|}; + } + """; + + var expectedCode = """ + using System; + using Thinktecture; + + namespace TestNamespace + { + [Union] + public partial class {|#0:TestUnion|}; + } + """; + + var expected = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments("TestUnion"); + await Verifier.VerifyCodeFixAsync(code, expectedCode, new[] { typeof(ComplexValueObjectAttribute).Assembly }, expected); + } + + [Fact] + public async Task Should_not_trigger_on_partial_class() + { + var code = """ + + using System; + using Thinktecture; + + namespace TestNamespace + { + [Union] + public partial class {|#0:TestUnion|}; + } + """; + + await Verifier.VerifyAnalyzerAsync(code, new[] { typeof(ComplexValueObjectAttribute).Assembly }); + } + } } diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG016_TypeCannotBeNestedClass.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG016_TypeCannotBeNestedClass.cs index 8eabfe9f..ff6312dc 100644 --- a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG016_TypeCannotBeNestedClass.cs +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG016_TypeCannotBeNestedClass.cs @@ -89,4 +89,29 @@ public partial class {|#0:TestValueObject|} await Verifier.VerifyAnalyzerAsync(code, new[] { typeof(ComplexValueObjectAttribute).Assembly }, expected); } } + + public class Union_cannot_be_nested_class + { + [Fact] + public async Task Should_trigger_if_enum_is_nested_class() + { + var code = """ + + using System; + using Thinktecture; + + namespace TestNamespace + { + public class SomeClass + { + [Union] + public partial class {|#0:TestUnion|}; + } + } + """; + + var expected = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments("TestUnion"); + await Verifier.VerifyAnalyzerAsync(code, new[] { typeof(IEnum<>).Assembly }, expected); + } + } } diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG017_KeyMemberShouldNotBeNullable.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG017_KeyMemberShouldNotBeNullable.cs index 3252e687..698dcd8b 100644 --- a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG017_KeyMemberShouldNotBeNullable.cs +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG017_KeyMemberShouldNotBeNullable.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; -using Microsoft.CodeAnalysis; using Verifier = Thinktecture.Runtime.Tests.Verifiers.CodeFixVerifier; namespace Thinktecture.Runtime.Tests.AnalyzerAndCodeFixTests; diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG033_EnumsAndValueObjectsMustNotBeGeneric.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG033_EnumsAndValueObjectsMustNotBeGeneric.cs index 4037496d..8dc99421 100644 --- a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG033_EnumsAndValueObjectsMustNotBeGeneric.cs +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG033_EnumsAndValueObjectsMustNotBeGeneric.cs @@ -146,4 +146,26 @@ public partial struct {|#0:TestValueObject|} await CodeFixVerifier.VerifyAnalyzerAsync(code, new[] { typeof(IEnum<>).Assembly }, expected); } } + + public class Union_must_not_be_generic + { + [Fact] + public async Task Should_trigger_on_generic_class() + { + var code = """ + + using System; + using Thinktecture; + + namespace TestNamespace + { + [Union] + public partial class {|#0:TestUnion|}; + } + """; + + var expected = CodeFixVerifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments("Union", "TestUnion"); + await CodeFixVerifier.VerifyAnalyzerAsync(code, new[] { typeof(IEnum<>).Assembly }, expected); + } + } } diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG043_PrimaryConstructorNotAllowed.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG043_PrimaryConstructorNotAllowed.cs index aa2705e8..8f5c1440 100644 --- a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG043_PrimaryConstructorNotAllowed.cs +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG043_PrimaryConstructorNotAllowed.cs @@ -144,4 +144,26 @@ public partial struct {|#0:ValueObject|}() await Verifier.VerifyAnalyzerAsync(code, new[] { typeof(IEnum<>).Assembly }, expected); } } + + public class Union_cannot_have_a_primary_constructor + { + [Fact] + public async Task Should_trigger_if_union_is_class_and_has_primary_constructor() + { + var code = """ + + using System; + using Thinktecture; + + namespace TestNamespace + { + [Union] + public partial class {|#0:TestUnion|}(); + } + """; + + var expected = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments("TestUnion"); + await Verifier.VerifyAnalyzerAsync(code, new[] { typeof(IEnum<>).Assembly }, expected); + } + } } diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG047_TypeMustBeClass.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG047_TypeMustBeClass.cs new file mode 100644 index 00000000..eab28114 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/AnalyzerAndCodeFixTests/TTRESG047_TypeMustBeClass.cs @@ -0,0 +1,50 @@ +using System.Threading.Tasks; +using Verifier = Thinktecture.Runtime.Tests.Verifiers.CodeFixVerifier; + +namespace Thinktecture.Runtime.Tests.AnalyzerAndCodeFixTests; + +// ReSharper disable InconsistentNaming +public class TTRESG047_TypeMustBeClassOrStruct +{ + private const string _DIAGNOSTIC_ID = "TTRESG047"; + + public class Union_must_be_class + { + [Fact] + public async Task Should_trigger_on_record() + { + var code = """ + + using System; + using Thinktecture; + + namespace TestNamespace + { + [Union] + public partial record {|#0:TestUnion|}; + } + """; + + var expected = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments("TestUnion"); + await Verifier.VerifyAnalyzerAsync(code, new[] { typeof(UnionAttribute<,>).Assembly }, expected); + } + + [Fact] + public async Task Should_not_trigger_on_class() + { + var code = """ + + using System; + using Thinktecture; + + namespace TestNamespace + { + [Union] + public partial class {|#0:TestUnion|}; + } + """; + + await Verifier.VerifyAnalyzerAsync(code, new[] { typeof(UnionAttribute<,>).Assembly }); + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/UnionSourceGeneratorTests.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/UnionSourceGeneratorTests.cs new file mode 100644 index 00000000..d21b452b --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/UnionSourceGeneratorTests.cs @@ -0,0 +1,3376 @@ +using System.Linq; +using Thinktecture.CodeAnalysis.DiscriminatedUnions; +using Xunit.Abstractions; + +namespace Thinktecture.Runtime.Tests.SourceGeneratorTests; + +public class UnionSourceGeneratorTests : SourceGeneratorTestsBase +{ + private const string _GENERATED_HEADER = """ + // + #nullable enable + + + """; + + public UnionSourceGeneratorTests(ITestOutputHelper output) + : base(output) + { + } + + [Fact] + public void Should_generate_class_with_string_and_int() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + [Union] + 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 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() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + [Union(SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)] + 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. + /// + /// The action to execute if no value-specific action is provided. + /// 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 SwitchPartially( + global::System.Action? @default = null, + global::System.Action? @string = null, + global::System.Action? int32 = null) + { + switch (this._valueIndex) + { + case 1: + if (@string is null) + break; + + @string(this._string!); + return; + case 2: + if (int32 is null) + break; + + int32(this._int32); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + + @default?.Invoke(this.Value); + } + + /// + /// 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 an action depending on the current value. + /// + /// Context to be passed to the callbacks. + /// The action to execute if no value-specific action is provided. + /// 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 SwitchPartially( + TContext context, + global::System.Action? @default = null, + global::System.Action? @string = null, + global::System.Action? int32 = null) + { + switch (this._valueIndex) + { + case 1: + if (@string is null) + break; + + @string(context, this._string!); + return; + case 2: + if (int32 is null) + break; + + int32(context, this._int32); + return; + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + + @default?.Invoke(context, this.Value); + } + + /// + /// 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. + /// + /// The function to execute if no value-specific action is provided. + /// 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 SwitchPartially( + global::System.Func @default, + global::System.Func? @string = null, + global::System.Func? int32 = null) + { + switch (this._valueIndex) + { + case 1: + if (@string is null) + break; + + return @string(this._string!); + case 2: + if (int32 is null) + break; + + return int32(this._int32); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + + return @default(this.Value); + } + + /// + /// 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}'."); + } + } + + /// + /// Executes a function depending on the current value. + /// + /// Context to be passed to the callbacks. + /// The function to execute if no value-specific action is provided. + /// 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 SwitchPartially( + TContext context, + global::System.Func @default, + global::System.Func? @string = null, + global::System.Func? int32 = null) + { + switch (this._valueIndex) + { + case 1: + if (@string is null) + break; + + return @string(context, this._string!); + case 2: + if (int32 is null) + break; + + return int32(context, this._int32); + default: + throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + + return @default(context, this.Value); + } + + /// + /// 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_MapPartially() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + [Union(MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)] + 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}'."); + } + } + + /// + /// Maps current value to an instance of type . + /// + /// The instance to return if no value is provided for the current value. + /// 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 MapPartially( + TResult @default, + global::Thinktecture.Argument @string = default, + global::Thinktecture.Argument int32 = default) + { + switch (this._valueIndex) + { + case 1: + if (!@string.IsSet) + break; + + return @string.Value; + case 2: + if (!int32.IsSet) + break; + + return int32.Value; + default: + throw new global::System.ArgumentOutOfRangeException($"Unexpected value index '{this._valueIndex}'."); + } + + return @default; + } + + /// + /// 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_without_Map() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + [Union(MapMethods = SwitchMapMethodsGeneration.None)] + 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}'."); + } + } + + /// + /// 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_without_Switch() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + [Union(SwitchMethods = SwitchMapMethodsGeneration.None)] + 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; + } + + /// + /// 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_without_ToString() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + [Union(SkipToString = 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 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}'.") + }; + } + } + } + + """); + } + + [Fact] + public void Should_generate_class_with_string_and_int_and_custom_string_comparison() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + [Union(DefaultStringComparison = StringComparison.Ordinal)] + 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 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.Ordinal), + 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.Ordinal) ?? 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_nullable_string_and_nullable_int() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + [Union(T1IsNullableReferenceType = 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? _nullableInt32; + + /// + /// 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 IsNullableInt32 => 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? AsNullableInt32 => IsNullableInt32 ? this._nullableInt32 : 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._nullableInt32, + _ => 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? nullableInt32) + { + this._nullableInt32 = nullableInt32; + 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 nullableInt32) + { + switch (this._valueIndex) + { + case 1: + @string(this._string); + return; + case 2: + nullableInt32(this._nullableInt32); + 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 nullableInt32) + { + switch (this._valueIndex) + { + case 1: + @string(context, this._string); + return; + case 2: + nullableInt32(context, this._nullableInt32); + 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 nullableInt32) + { + switch (this._valueIndex) + { + case 1: + return @string(this._string); + case 2: + return nullableInt32(this._nullableInt32); + 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 nullableInt32) + { + switch (this._valueIndex) + { + case 1: + return @string(context, this._string); + case 2: + return nullableInt32(context, this._nullableInt32); + 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 nullableInt32) + { + switch (this._valueIndex) + { + case 1: + return @string; + case 2: + return nullableInt32; + 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.AsNullableInt32; + } + + /// + /// 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._nullableInt32.Equals(other._nullableInt32), + _ => 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._nullableInt32.GetHashCode()), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override string? ToString() + { + return this._valueIndex switch + { + 1 => this._string, + 2 => this._nullableInt32.ToString(), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } + } + + """); + } + + [Fact] + public void Should_generate_class_with_string_and_int_with_custom_names() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + [Union(T1Name = "Text", T2Name = "Number")] + 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? _text; + private readonly int _number; + + /// + /// Indication whether the current value is of type string. + /// + public bool IsText => this._valueIndex == 1; + + /// + /// Indication whether the current value is of type int. + /// + public bool IsNumber => this._valueIndex == 2; + + /// + /// Gets the current value as string. + /// + /// If the current value is not of type string. + public string AsText => IsText ? this._text! : 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 AsNumber => IsNumber ? this._number : 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._text!, + 2 => this._number, + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(string text) + { + this._text = text; + this._valueIndex = 1; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(int number) + { + this._number = number; + 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 text, + global::System.Action number) + { + switch (this._valueIndex) + { + case 1: + text(this._text!); + return; + case 2: + number(this._number); + 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 text, + global::System.Action number) + { + switch (this._valueIndex) + { + case 1: + text(context, this._text!); + return; + case 2: + number(context, this._number); + 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 text, + global::System.Func number) + { + switch (this._valueIndex) + { + case 1: + return text(this._text!); + case 2: + return number(this._number); + 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 text, + global::System.Func number) + { + switch (this._valueIndex) + { + case 1: + return text(context, this._text!); + case 2: + return number(context, this._number); + 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 text, + TResult number) + { + switch (this._valueIndex) + { + case 1: + return text; + case 2: + return number; + 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.AsText; + } + + /// + /// 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.AsNumber; + } + + /// + /// 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._text is null ? other._text is null : this._text.Equals(other._text, global::System.StringComparison.OrdinalIgnoreCase), + 2 => this._number.Equals(other._number), + _ => 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._text?.GetHashCode(global::System.StringComparison.OrdinalIgnoreCase) ?? 0), + 2 => global::System.HashCode.Combine(global::Thinktecture.Tests.TestUnion._typeHashCode, this._number.GetHashCode()), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + + /// + public override string? ToString() + { + return this._valueIndex switch + { + 1 => this._text, + 2 => this._number.ToString(), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } + } + + """); + } + + [Fact] + public void Should_generate_class_with_string_and_int_bool_guid_char() + { + var source = """ + using System; + + namespace Thinktecture.Tests + { + [Union] + 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; + private readonly bool _boolean; + private readonly global::System.Guid _guid; + private readonly char _char; + + /// + /// 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; + + /// + /// Indication whether the current value is of type bool. + /// + public bool IsBoolean => this._valueIndex == 3; + + /// + /// Indication whether the current value is of type Guid. + /// + public bool IsGuid => this._valueIndex == 4; + + /// + /// Indication whether the current value is of type char. + /// + public bool IsChar => this._valueIndex == 5; + + /// + /// 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 bool. + /// + /// If the current value is not of type bool. + public bool AsBoolean => IsBoolean ? this._boolean : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'bool'."); + + /// + /// Gets the current value as Guid. + /// + /// If the current value is not of type Guid. + public global::System.Guid AsGuid => IsGuid ? this._guid : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'Guid'."); + + /// + /// Gets the current value as char. + /// + /// If the current value is not of type char. + public char AsChar => IsChar ? this._char : throw new global::System.InvalidOperationException($"'{nameof(global::Thinktecture.Tests.TestUnion)}' is not of type 'char'."); + + /// + /// Gets the current value as . + /// + public object Value => this._valueIndex switch + { + 1 => this._string!, + 2 => this._int32, + 3 => this._boolean, + 4 => this._guid, + 5 => this._char, + _ => 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; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(bool boolean) + { + this._boolean = boolean; + this._valueIndex = 3; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(global::System.Guid guid) + { + this._guid = guid; + this._valueIndex = 4; + } + + /// + /// Initializes new instance with . + /// + /// Value to create a new instance for. + public TestUnion(char @char) + { + this._char = @char; + this._valueIndex = 5; + } + + /// + /// 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. + /// The action to execute if the current value is of type bool. + /// The action to execute if the current value is of type Guid. + /// The action to execute if the current value is of type char. + public void Switch( + global::System.Action @string, + global::System.Action int32, + global::System.Action boolean, + global::System.Action guid, + global::System.Action @char) + { + switch (this._valueIndex) + { + case 1: + @string(this._string!); + return; + case 2: + int32(this._int32); + return; + case 3: + boolean(this._boolean); + return; + case 4: + guid(this._guid); + return; + case 5: + @char(this._char); + 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. + /// The action to execute if the current value is of type bool. + /// The action to execute if the current value is of type Guid. + /// The action to execute if the current value is of type char. + public void Switch( + TContext context, + global::System.Action @string, + global::System.Action int32, + global::System.Action boolean, + global::System.Action guid, + global::System.Action @char) + { + switch (this._valueIndex) + { + case 1: + @string(context, this._string!); + return; + case 2: + int32(context, this._int32); + return; + case 3: + boolean(context, this._boolean); + return; + case 4: + guid(context, this._guid); + return; + case 5: + @char(context, this._char); + 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. + /// The function to execute if the current value is of type bool. + /// The function to execute if the current value is of type Guid. + /// The function to execute if the current value is of type char. + public TResult Switch( + global::System.Func @string, + global::System.Func int32, + global::System.Func boolean, + global::System.Func guid, + global::System.Func @char) + { + switch (this._valueIndex) + { + case 1: + return @string(this._string!); + case 2: + return int32(this._int32); + case 3: + return boolean(this._boolean); + case 4: + return guid(this._guid); + case 5: + return @char(this._char); + 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. + /// The function to execute if the current value is of type bool. + /// The function to execute if the current value is of type Guid. + /// The function to execute if the current value is of type char. + public TResult Switch( + TContext context, + global::System.Func @string, + global::System.Func int32, + global::System.Func boolean, + global::System.Func guid, + global::System.Func @char) + { + switch (this._valueIndex) + { + case 1: + return @string(context, this._string!); + case 2: + return int32(context, this._int32); + case 3: + return boolean(context, this._boolean); + case 4: + return guid(context, this._guid); + case 5: + return @char(context, this._char); + 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. + /// The instance to return if the current value is of type bool. + /// The instance to return if the current value is of type Guid. + /// The instance to return if the current value is of type char. + public TResult Map( + TResult @string, + TResult int32, + TResult boolean, + TResult guid, + TResult @char) + { + switch (this._valueIndex) + { + case 1: + return @string; + case 2: + return int32; + case 3: + return boolean; + case 4: + return guid; + case 5: + return @char; + 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 from type bool. + /// + /// Value to covert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(bool value) + { + return new global::Thinktecture.Tests.TestUnion(value); + } + + /// + /// Implicit conversion from type Guid. + /// + /// Value to covert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(global::System.Guid value) + { + return new global::Thinktecture.Tests.TestUnion(value); + } + + /// + /// Implicit conversion from type char. + /// + /// Value to covert from. + /// A new instance of converted from . + public static implicit operator global::Thinktecture.Tests.TestUnion(char 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; + } + + /// + /// Implicit conversion to type bool. + /// + /// Object to covert. + /// Inner value of type bool. + /// If the inner value is not a bool. + public static explicit operator bool(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsBoolean; + } + + /// + /// Implicit conversion to type Guid. + /// + /// Object to covert. + /// Inner value of type Guid. + /// If the inner value is not a Guid. + public static explicit operator global::System.Guid(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsGuid; + } + + /// + /// Implicit conversion to type char. + /// + /// Object to covert. + /// Inner value of type char. + /// If the inner value is not a char. + public static explicit operator char(global::Thinktecture.Tests.TestUnion obj) + { + return obj.AsChar; + } + + /// + /// 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), + 3 => this._boolean.Equals(other._boolean), + 4 => this._guid.Equals(other._guid), + 5 => this._char.Equals(other._char), + _ => 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()), + 3 => global::System.HashCode.Combine(global::Thinktecture.Tests.TestUnion._typeHashCode, this._boolean.GetHashCode()), + 4 => global::System.HashCode.Combine(global::Thinktecture.Tests.TestUnion._typeHashCode, this._guid.GetHashCode()), + 5 => global::System.HashCode.Combine(global::Thinktecture.Tests.TestUnion._typeHashCode, this._char.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(), + 3 => this._boolean.ToString(), + 4 => this._guid.ToString(), + 5 => this._char.ToString(), + _ => throw new global::System.IndexOutOfRangeException($"Unexpected value index '{this._valueIndex}'.") + }; + } + } + } + + """); + } +} diff --git a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/ValueObjectSourceGeneratorTests.cs b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/ValueObjectSourceGeneratorTests.cs index 3a6351c6..4ebe3147 100644 --- a/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/ValueObjectSourceGeneratorTests.cs +++ b/test/Thinktecture.Runtime.Extensions.SourceGenerator.Tests/SourceGeneratorTests/ValueObjectSourceGeneratorTests.cs @@ -1343,7 +1343,7 @@ private TestValueObject() } /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -1357,7 +1357,7 @@ private TestValueObject() } /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -1482,7 +1482,7 @@ public static bool TryCreate( partial void FactoryPostInit(); /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -1493,7 +1493,7 @@ public static bool TryCreate( } /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -2014,7 +2014,7 @@ private TestValueObject(string? prop1, global::System.Func?>? prop2); /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -2028,7 +2028,7 @@ private TestValueObject(string? prop1, global::System.Func - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -2183,7 +2183,7 @@ private TestValueObject() } /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -2197,7 +2197,7 @@ private TestValueObject() } /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -2350,7 +2350,7 @@ private TestValueObject() } /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -2364,7 +2364,7 @@ private TestValueObject() } /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -2548,7 +2548,7 @@ private TestValueObject() } /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -2562,7 +2562,7 @@ private TestValueObject() } /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -2889,7 +2889,7 @@ public static bool TryCreate( partial void FactoryPostInit(); /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -2900,7 +2900,7 @@ public static bool TryCreate( } /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -5488,7 +5488,7 @@ private TestValueObject(string stringValue, int intValue, string referenceProper static partial void ValidateConstructorArguments(ref string stringValue, ref int intValue, ref string referenceProperty, ref string? nullableReferenceProperty, ref int structProperty, ref int? nullableStructProperty); /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -5502,7 +5502,7 @@ private TestValueObject(string stringValue, int intValue, string referenceProper } /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -5721,7 +5721,7 @@ private TestValueObject(string stringValue, int intValue, string referenceProper static partial void ValidateConstructorArguments(ref string stringValue, ref int intValue, ref string referenceProperty, ref string? nullableReferenceProperty, ref int structProperty, ref int? nullableStructProperty); /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -5735,7 +5735,7 @@ private TestValueObject(string stringValue, int intValue, string referenceProper } /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -5998,7 +5998,7 @@ private TestValueObject(string stringValue, int intValue, string referenceProper static partial void ValidateConstructorArguments(ref string stringValue, ref int intValue, ref string referenceProperty, ref string? nullableReferenceProperty, ref int structProperty, ref int? nullableStructProperty); /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -6012,7 +6012,7 @@ private TestValueObject(string stringValue, int intValue, string referenceProper } /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -6265,7 +6265,7 @@ private TestValueObject(string value1, string value2, string value3, string valu static partial void ValidateConstructorArguments(ref string value1, ref string value2, ref string value3, ref string value4, ref string value5, ref string value6, ref string value7, ref string value8, ref string value9); /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. @@ -6279,7 +6279,7 @@ private TestValueObject(string value1, string value2, string value3, string valu } /// - /// Compares to instances of . + /// Compares two instances of . /// /// Instance to compare. /// Another instance to compare. diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_nullable_string_int.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_nullable_string_int.cs new file mode 100644 index 00000000..06edece2 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_nullable_string_int.cs @@ -0,0 +1,5 @@ +namespace Thinktecture.Runtime.Tests.TestUnions; + +// ReSharper disable once InconsistentNaming +[Union(T1IsNullableReferenceType = true)] +public partial class TestUnion_class_nullable_string_int; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_nullable_string_nullable_int.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_nullable_string_nullable_int.cs new file mode 100644 index 00000000..28c124f2 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_nullable_string_nullable_int.cs @@ -0,0 +1,5 @@ +namespace Thinktecture.Runtime.Tests.TestUnions; + +// ReSharper disable once InconsistentNaming +[Union(T1IsNullableReferenceType = true)] +public partial class TestUnion_class_nullable_string_nullable_int; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int.cs new file mode 100644 index 00000000..b92a4870 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int.cs @@ -0,0 +1,6 @@ +namespace Thinktecture.Runtime.Tests.TestUnions; + +// ReSharper disable once InconsistentNaming +[Union(SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)] +public partial class TestUnion_class_string_int; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_bool.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_bool.cs new file mode 100644 index 00000000..6da2d0eb --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_bool.cs @@ -0,0 +1,6 @@ +namespace Thinktecture.Runtime.Tests.TestUnions; + +// ReSharper disable once InconsistentNaming +[Union(SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)] +public partial class TestUnion_class_string_int_bool; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_bool_case_sensitive.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_bool_case_sensitive.cs new file mode 100644 index 00000000..ff29785d --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_bool_case_sensitive.cs @@ -0,0 +1,9 @@ +using System; + +namespace Thinktecture.Runtime.Tests.TestUnions; + +// ReSharper disable once InconsistentNaming +[Union(SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + DefaultStringComparison = StringComparison.Ordinal)] +public partial class TestUnion_class_string_int_bool_case_sensitive; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_bool_guid.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_bool_guid.cs new file mode 100644 index 00000000..88032af4 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_bool_guid.cs @@ -0,0 +1,8 @@ +using System; + +namespace Thinktecture.Runtime.Tests.TestUnions; + +// ReSharper disable once InconsistentNaming +[Union(SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)] +public partial class TestUnion_class_string_int_bool_guid; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_bool_guid_case_sensitive.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_bool_guid_case_sensitive.cs new file mode 100644 index 00000000..64f0b32c --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_bool_guid_case_sensitive.cs @@ -0,0 +1,9 @@ +using System; + +namespace Thinktecture.Runtime.Tests.TestUnions; + +// ReSharper disable once InconsistentNaming +[Union(SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + DefaultStringComparison = StringComparison.Ordinal)] +public partial class TestUnion_class_string_int_bool_guid_case_sensitive; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_bool_guid_char.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_bool_guid_char.cs new file mode 100644 index 00000000..8c20f7c9 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_bool_guid_char.cs @@ -0,0 +1,8 @@ +using System; + +namespace Thinktecture.Runtime.Tests.TestUnions; + +// ReSharper disable once InconsistentNaming +[Union(SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)] +public partial class TestUnion_class_string_int_bool_guid_char; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_bool_guid_char_case_sensitive.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_bool_guid_char_case_sensitive.cs new file mode 100644 index 00000000..147d2208 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_bool_guid_char_case_sensitive.cs @@ -0,0 +1,9 @@ +using System; + +namespace Thinktecture.Runtime.Tests.TestUnions; + +// ReSharper disable once InconsistentNaming +[Union(SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads, + DefaultStringComparison = StringComparison.Ordinal)] +public partial class TestUnion_class_string_int_bool_guid_char_case_sensitive; diff --git a/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_case_sensitive.cs b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_case_sensitive.cs new file mode 100644 index 00000000..68391120 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests.Shared/TestUnions/TestUnion_class_string_int_case_sensitive.cs @@ -0,0 +1,7 @@ +using System; + +namespace Thinktecture.Runtime.Tests.TestUnions; + +// ReSharper disable once InconsistentNaming +[Union(DefaultStringComparison = StringComparison.Ordinal)] +public partial class TestUnion_class_string_int_case_sensitive; diff --git a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/EqualityOperator.cs b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/EqualityOperator.cs index 105b890c..39ee88fc 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/EqualityOperator.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/EqualityOperator.cs @@ -10,7 +10,7 @@ public class EqualityOperator [Fact] public void Should_return_false_if_item_is_null() { - (TestEnum.Item1 is null).Should().BeFalse(); + (TestEnum.Item1 == null).Should().BeFalse(); } [Fact] diff --git a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/Validate.cs b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/Validate.cs index 884c78ff..6678a0ce 100644 --- a/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/Validate.cs +++ b/test/Thinktecture.Runtime.Extensions.Tests/EnumTests/Validate.cs @@ -1,5 +1,4 @@ using System; -using System.ComponentModel.DataAnnotations; using Thinktecture.Runtime.Tests.TestEnums; namespace Thinktecture.Runtime.Tests.EnumTests; diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/AsValue.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/AsValue.cs new file mode 100644 index 00000000..360ec68a --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/AsValue.cs @@ -0,0 +1,107 @@ +#nullable enable +using System; +using Thinktecture.Runtime.Tests.TestUnions; + +namespace Thinktecture.Runtime.Tests.UnionTests; + +public class AsValue +{ + [Fact] + public void Should_return_correct_value_or_throw_exception_having_2_types() + { + new TestUnion_class_string_int("text").AsString.Should().Be("text"); + new TestUnion_class_string_int("text").Invoking(u => u.AsInt32.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int' is not of type 'int'."); + new TestUnion_class_string_int(1).Invoking(u => u.AsString.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int' is not of type 'string'."); + new TestUnion_class_string_int(1).AsInt32.Should().Be(1); + + new TestUnion_class_nullable_string_int(@string: null).AsString.Should().BeNull(); + new TestUnion_class_nullable_string_int(@string: null).Invoking(u => u.AsInt32.Should()).Should().Throw().WithMessage("'TestUnion_class_nullable_string_int' is not of type 'int'."); + new TestUnion_class_nullable_string_int("text").AsString.Should().Be("text"); + new TestUnion_class_nullable_string_int("text").Invoking(u => u.AsInt32.Should()).Should().Throw().WithMessage("'TestUnion_class_nullable_string_int' is not of type 'int'."); + new TestUnion_class_nullable_string_int(1).Invoking(u => u.AsString.Should()).Should().Throw().WithMessage("'TestUnion_class_nullable_string_int' is not of type 'string?'."); + new TestUnion_class_nullable_string_int(1).AsInt32.Should().Be(1); + + new TestUnion_class_nullable_string_nullable_int(@string: null).AsString.Should().BeNull(); + new TestUnion_class_nullable_string_nullable_int(@string: null).Invoking(u => u.AsNullableInt32.Should()).Should().Throw().WithMessage("'TestUnion_class_nullable_string_nullable_int' is not of type 'int?'."); + new TestUnion_class_nullable_string_nullable_int("text").AsString.Should().Be("text"); + new TestUnion_class_nullable_string_nullable_int("text").Invoking(u => u.AsNullableInt32.Should()).Should().Throw().WithMessage("'TestUnion_class_nullable_string_nullable_int' is not of type 'int?'."); + new TestUnion_class_nullable_string_nullable_int(1).Invoking(u => u.AsString.Should()).Should().Throw().WithMessage("'TestUnion_class_nullable_string_nullable_int' is not of type 'string?'."); + new TestUnion_class_nullable_string_nullable_int(1).AsNullableInt32.Should().Be(1); + new TestUnion_class_nullable_string_nullable_int(nullableInt32: null).Invoking(u => u.AsString.Should()).Should().Throw().WithMessage("'TestUnion_class_nullable_string_nullable_int' is not of type 'string?'."); + new TestUnion_class_nullable_string_nullable_int(nullableInt32: null).AsNullableInt32.Should().BeNull(); + } + + [Fact] + public void Should_return_correct_value_or_throw_exception_having_3_types() + { + new TestUnion_class_string_int_bool("text").AsString.Should().Be("text"); + new TestUnion_class_string_int_bool("text").Invoking(u => u.AsInt32.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool' is not of type 'int'."); + new TestUnion_class_string_int_bool("text").Invoking(u => u.AsBoolean.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool' is not of type 'bool'."); + + new TestUnion_class_string_int_bool(1).Invoking(u => u.AsString.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool' is not of type 'string'."); + new TestUnion_class_string_int_bool(1).AsInt32.Should().Be(1); + new TestUnion_class_string_int_bool(1).Invoking(u => u.AsBoolean.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool' is not of type 'bool'."); + + new TestUnion_class_string_int_bool(true).Invoking(u => u.AsString.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool' is not of type 'string'."); + new TestUnion_class_string_int_bool(true).Invoking(u => u.AsInt32.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool' is not of type 'int'."); + new TestUnion_class_string_int_bool(true).AsBoolean.Should().Be(true); + } + + [Fact] + public void Should_return_correct_value_or_throw_exception_having_4_types() + { + new TestUnion_class_string_int_bool_guid("text").AsString.Should().Be("text"); + new TestUnion_class_string_int_bool_guid("text").Invoking(u => u.AsInt32.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'int'."); + new TestUnion_class_string_int_bool_guid("text").Invoking(u => u.AsBoolean.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'bool'."); + new TestUnion_class_string_int_bool_guid("text").Invoking(u => u.AsGuid.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'Guid'."); + + new TestUnion_class_string_int_bool_guid(1).Invoking(u => u.AsString.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'string'."); + new TestUnion_class_string_int_bool_guid(1).AsInt32.Should().Be(1); + new TestUnion_class_string_int_bool_guid(1).Invoking(u => u.AsBoolean.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'bool'."); + new TestUnion_class_string_int_bool_guid(1).Invoking(u => u.AsGuid.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'Guid'."); + + new TestUnion_class_string_int_bool_guid(true).Invoking(u => u.AsString.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'string'."); + new TestUnion_class_string_int_bool_guid(true).Invoking(u => u.AsInt32.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'int'."); + new TestUnion_class_string_int_bool_guid(true).AsBoolean.Should().Be(true); + new TestUnion_class_string_int_bool_guid(true).Invoking(u => u.AsGuid.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'Guid'."); + + new TestUnion_class_string_int_bool_guid(new Guid("60AF342A-2A29-4F4B-AA26-B69E857CCF5C")).Invoking(u => u.AsString.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'string'."); + new TestUnion_class_string_int_bool_guid(new Guid("60AF342A-2A29-4F4B-AA26-B69E857CCF5C")).Invoking(u => u.AsInt32.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'int'."); + new TestUnion_class_string_int_bool_guid(new Guid("60AF342A-2A29-4F4B-AA26-B69E857CCF5C")).Invoking(u => u.AsBoolean.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'bool'."); + new TestUnion_class_string_int_bool_guid(new Guid("60AF342A-2A29-4F4B-AA26-B69E857CCF5C")).AsGuid.Should().Be(new Guid("60AF342A-2A29-4F4B-AA26-B69E857CCF5C")); + } + + [Fact] + public void Should_return_correct_value_or_throw_exception_having_5_types() + { + new TestUnion_class_string_int_bool_guid_char("text").AsString.Should().Be("text"); + new TestUnion_class_string_int_bool_guid_char("text").Invoking(u => u.AsInt32.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'int'."); + new TestUnion_class_string_int_bool_guid_char("text").Invoking(u => u.AsBoolean.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'bool'."); + new TestUnion_class_string_int_bool_guid_char("text").Invoking(u => u.AsGuid.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'Guid'."); + new TestUnion_class_string_int_bool_guid_char("text").Invoking(u => u.AsChar.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'char'."); + + new TestUnion_class_string_int_bool_guid_char(1).Invoking(u => u.AsString.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'string'."); + new TestUnion_class_string_int_bool_guid_char(1).AsInt32.Should().Be(1); + new TestUnion_class_string_int_bool_guid_char(1).Invoking(u => u.AsBoolean.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'bool'."); + new TestUnion_class_string_int_bool_guid_char(1).Invoking(u => u.AsGuid.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'Guid'."); + new TestUnion_class_string_int_bool_guid_char(1).Invoking(u => u.AsChar.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'char'."); + + new TestUnion_class_string_int_bool_guid_char(true).Invoking(u => u.AsString.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'string'."); + new TestUnion_class_string_int_bool_guid_char(true).Invoking(u => u.AsInt32.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'int'."); + new TestUnion_class_string_int_bool_guid_char(true).AsBoolean.Should().Be(true); + new TestUnion_class_string_int_bool_guid_char(true).Invoking(u => u.AsGuid.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'Guid'."); + new TestUnion_class_string_int_bool_guid_char(true).Invoking(u => u.AsChar.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'char'."); + + new TestUnion_class_string_int_bool_guid_char(new Guid("60AF342A-2A29-4F4B-AA26-B69E857CCF5C")).Invoking(u => u.AsString.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'string'."); + new TestUnion_class_string_int_bool_guid_char(new Guid("60AF342A-2A29-4F4B-AA26-B69E857CCF5C")).Invoking(u => u.AsInt32.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'int'."); + new TestUnion_class_string_int_bool_guid_char(new Guid("60AF342A-2A29-4F4B-AA26-B69E857CCF5C")).Invoking(u => u.AsBoolean.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'bool'."); + new TestUnion_class_string_int_bool_guid_char(new Guid("60AF342A-2A29-4F4B-AA26-B69E857CCF5C")).AsGuid.Should().Be(new Guid("60AF342A-2A29-4F4B-AA26-B69E857CCF5C")); + new TestUnion_class_string_int_bool_guid_char(new Guid("60AF342A-2A29-4F4B-AA26-B69E857CCF5C")).Invoking(u => u.AsChar.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'char'."); + + new TestUnion_class_string_int_bool_guid_char('A').Invoking(u => u.AsString.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'string'."); + new TestUnion_class_string_int_bool_guid_char('A').Invoking(u => u.AsInt32.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'int'."); + new TestUnion_class_string_int_bool_guid_char('A').Invoking(u => u.AsBoolean.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'bool'."); + new TestUnion_class_string_int_bool_guid_char('A').Invoking(u => u.AsGuid.Should()).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'Guid'."); + new TestUnion_class_string_int_bool_guid_char('A').AsChar.Should().Be('A'); + } +} diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/EqualityOperator.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/EqualityOperator.cs new file mode 100644 index 00000000..b44d18f5 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/EqualityOperator.cs @@ -0,0 +1,60 @@ +using System; +using Thinktecture.Runtime.Tests.TestUnions; + +namespace Thinktecture.Runtime.Tests.UnionTests; + +// ReSharper disable SuspiciousTypeConversion.Global +// ReSharper disable EqualExpressionComparison +// ReSharper disable ConditionIsAlwaysTrueOrFalse +public class EqualityOperator +{ + [Fact] + public void Should_compare_unions_with_2_types() + { + Compare(s => new TestUnion_class_string_int(s), + n => new TestUnion_class_string_int(n), + s => new TestUnion_class_string_int_case_sensitive(s)); + } + + [Fact] + public void Should_compare_unions_with_3_types() + { + Compare(s => new TestUnion_class_string_int_bool(s), + n => new TestUnion_class_string_int_bool(n), + s => new TestUnion_class_string_int_bool_case_sensitive(s)); + } + + [Fact] + public void Should_compare_unions_with_4_types() + { + Compare(s => new TestUnion_class_string_int_bool_guid(s), + n => new TestUnion_class_string_int_bool_guid(n), + s => new TestUnion_class_string_int_bool_guid_case_sensitive(s)); + } + + [Fact] + public void Should_compare_unions_with_5_types() + { + Compare(s => new TestUnion_class_string_int_bool_guid_char(s), + n => new TestUnion_class_string_int_bool_guid_char(n), + s => new TestUnion_class_string_int_bool_guid_char_case_sensitive(s)); + } + + private static void Compare( + Func stringFactory, + Func intFactory, + Func caseSensitiveFactory) + where T : System.Numerics.IEqualityOperators + where T2 : System.Numerics.IEqualityOperators + { + var obj = stringFactory("text"); + + (obj == null).Should().BeFalse(); + (obj == obj).Should().BeTrue(); + (obj == stringFactory("text")).Should().BeTrue(); + (obj == stringFactory("TEXT")).Should().BeTrue(); + (caseSensitiveFactory("text") == caseSensitiveFactory("TEXT")).Should().BeFalse(); + (obj == stringFactory("other text")).Should().BeFalse(); + (stringFactory("42") == intFactory(42)).Should().BeFalse(); + } +} diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/Equals.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/Equals.cs new file mode 100644 index 00000000..047c88d0 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/Equals.cs @@ -0,0 +1,78 @@ +using System; +using Thinktecture.Runtime.Tests.TestUnions; + +namespace Thinktecture.Runtime.Tests.UnionTests; + +// ReSharper disable SuspiciousTypeConversion.Global +// ReSharper disable EqualExpressionComparison +// ReSharper disable ConditionIsAlwaysTrueOrFalse +public class Equals +{ + [Fact] + public void Should_compare_unions_with_2_types() + { + Compare(s => new TestUnion_class_string_int(s), + n => new TestUnion_class_string_int(n), + s => new TestUnion_class_string_int_case_sensitive(s)); + } + + [Fact] + public void Should_compare_unions_with_3_types() + { + Compare(s => new TestUnion_class_string_int_bool(s), + n => new TestUnion_class_string_int_bool(n), + s => new TestUnion_class_string_int_bool_case_sensitive(s)); + } + + [Fact] + public void Should_compare_unions_with_4_types() + { + Compare(s => new TestUnion_class_string_int_bool_guid(s), + n => new TestUnion_class_string_int_bool_guid(n), + s => new TestUnion_class_string_int_bool_guid_case_sensitive(s)); + } + + [Fact] + public void Should_compare_unions_with_5_types() + { + Compare(s => new TestUnion_class_string_int_bool_guid_char(s), + n => new TestUnion_class_string_int_bool_guid_char(n), + s => new TestUnion_class_string_int_bool_guid_char_case_sensitive(s)); + } + + private static void Compare( + Func stringFactory, + Func intFactory, + Func caseSensitiveFactory) + where T : IEquatable + where T2 : IEquatable + { + var obj = stringFactory("text"); + + obj.Equals((TestUnion_class_string_int)null).Should().BeFalse(); + obj.Equals((object)null).Should().BeFalse(); + + obj.Equals(obj).Should().BeTrue(); + obj.Equals((object)obj).Should().BeTrue(); + + obj.Equals(stringFactory("text")).Should().BeTrue(); + obj.Equals((object)stringFactory("text")).Should().BeTrue(); + + obj.Equals(stringFactory("TEXT")).Should().BeTrue(); + obj.Equals((object)stringFactory("TEXT")).Should().BeTrue(); + + caseSensitiveFactory("text").Equals(caseSensitiveFactory("TEXT")).Should().BeFalse(); + caseSensitiveFactory("text").Equals((object)caseSensitiveFactory("TEXT")).Should().BeFalse(); + + obj.Equals(stringFactory("other text")).Should().BeFalse(); + obj.Equals((object)stringFactory("other text")).Should().BeFalse(); + + stringFactory("42").Equals(intFactory(42)).Should().BeFalse(); + stringFactory("42").Equals((object)intFactory(42)).Should().BeFalse(); + + stringFactory("42").Equals((object)"42").Should().BeFalse(); + stringFactory("42").Equals((object)42).Should().BeFalse(); + stringFactory("42").Equals(caseSensitiveFactory("42")).Should().BeFalse(); + stringFactory("42").Equals((object)caseSensitiveFactory("42")).Should().BeFalse(); + } +} diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ExplicitCasts.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ExplicitCasts.cs new file mode 100644 index 00000000..d7f228ae --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ExplicitCasts.cs @@ -0,0 +1,91 @@ +using System; +using Thinktecture.Runtime.Tests.TestUnions; + +namespace Thinktecture.Runtime.Tests.UnionTests; + +public class ExplicitCasts +{ + [Fact] + public void Should_have_explicit_casts_to_value_having_2_types() + { + ((string)new TestUnion_class_string_int("text")).Should().Be("text"); + FluentActions.Invoking(() => (int)new TestUnion_class_string_int("text")).Should().Throw().WithMessage("'TestUnion_class_string_int' is not of type 'int'."); + + ((int)new TestUnion_class_string_int(1)).Should().Be(1); + FluentActions.Invoking(() => (string)new TestUnion_class_string_int(1)).Should().Throw().WithMessage("'TestUnion_class_string_int' is not of type 'string'."); + } + + [Fact] + public void Should_have_explicit_casts_to_value_having_3_types() + { + ((string)new TestUnion_class_string_int_bool("text")).Should().Be("text"); + FluentActions.Invoking(() => (int)new TestUnion_class_string_int_bool("text")).Should().Throw().WithMessage("'TestUnion_class_string_int_bool' is not of type 'int'."); + FluentActions.Invoking(() => (bool)new TestUnion_class_string_int_bool("text")).Should().Throw().WithMessage("'TestUnion_class_string_int_bool' is not of type 'bool'."); + + ((int)new TestUnion_class_string_int_bool(1)).Should().Be(1); + FluentActions.Invoking(() => (string)new TestUnion_class_string_int_bool(1)).Should().Throw().WithMessage("'TestUnion_class_string_int_bool' is not of type 'string'."); + FluentActions.Invoking(() => (bool)new TestUnion_class_string_int_bool(1)).Should().Throw().WithMessage("'TestUnion_class_string_int_bool' is not of type 'bool'."); + + ((bool)new TestUnion_class_string_int_bool(true)).Should().Be(true); + FluentActions.Invoking(() => (string)new TestUnion_class_string_int_bool(true)).Should().Throw().WithMessage("'TestUnion_class_string_int_bool' is not of type 'string'."); + FluentActions.Invoking(() => (int)new TestUnion_class_string_int_bool(true)).Should().Throw().WithMessage("'TestUnion_class_string_int_bool' is not of type 'int'."); + } + + [Fact] + public void Should_have_explicit_casts_to_value_having_4_types() + { + ((string)new TestUnion_class_string_int_bool_guid("text")).Should().Be("text"); + FluentActions.Invoking(() => (int)new TestUnion_class_string_int_bool_guid("text")).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'int'."); + FluentActions.Invoking(() => (bool)new TestUnion_class_string_int_bool_guid("text")).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'bool'."); + FluentActions.Invoking(() => (Guid)new TestUnion_class_string_int_bool_guid("text")).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'Guid'."); + + ((int)new TestUnion_class_string_int_bool_guid(1)).Should().Be(1); + FluentActions.Invoking(() => (string)new TestUnion_class_string_int_bool_guid(1)).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'string'."); + FluentActions.Invoking(() => (bool)new TestUnion_class_string_int_bool_guid(1)).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'bool'."); + FluentActions.Invoking(() => (Guid)new TestUnion_class_string_int_bool_guid(1)).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'Guid'."); + + ((bool)new TestUnion_class_string_int_bool_guid(true)).Should().Be(true); + FluentActions.Invoking(() => (string)new TestUnion_class_string_int_bool_guid(true)).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'string'."); + FluentActions.Invoking(() => (int)new TestUnion_class_string_int_bool_guid(true)).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'int'."); + FluentActions.Invoking(() => (Guid)new TestUnion_class_string_int_bool_guid(true)).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'Guid'."); + + ((Guid)new TestUnion_class_string_int_bool_guid(new Guid("9265932D-D3EB-4204-A027-7876F72BD66E"))).Should().Be(new Guid("9265932D-D3EB-4204-A027-7876F72BD66E")); + FluentActions.Invoking(() => (string)new TestUnion_class_string_int_bool_guid(new Guid("9265932D-D3EB-4204-A027-7876F72BD66E"))).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'string'."); + FluentActions.Invoking(() => (int)new TestUnion_class_string_int_bool_guid(new Guid("9265932D-D3EB-4204-A027-7876F72BD66E"))).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'int'."); + FluentActions.Invoking(() => (bool)new TestUnion_class_string_int_bool_guid(new Guid("9265932D-D3EB-4204-A027-7876F72BD66E"))).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid' is not of type 'bool'."); + } + + [Fact] + public void Should_have_explicit_casts_to_value_having_5_types() + { + ((string)new TestUnion_class_string_int_bool_guid_char("text")).Should().Be("text"); + FluentActions.Invoking(() => (int)new TestUnion_class_string_int_bool_guid_char("text")).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'int'."); + FluentActions.Invoking(() => (bool)new TestUnion_class_string_int_bool_guid_char("text")).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'bool'."); + FluentActions.Invoking(() => (Guid)new TestUnion_class_string_int_bool_guid_char("text")).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'Guid'."); + FluentActions.Invoking(() => (char)new TestUnion_class_string_int_bool_guid_char("text")).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'char'."); + + ((int)new TestUnion_class_string_int_bool_guid_char(1)).Should().Be(1); + FluentActions.Invoking(() => (string)new TestUnion_class_string_int_bool_guid_char(1)).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'string'."); + FluentActions.Invoking(() => (bool)new TestUnion_class_string_int_bool_guid_char(1)).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'bool'."); + FluentActions.Invoking(() => (Guid)new TestUnion_class_string_int_bool_guid_char(1)).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'Guid'."); + FluentActions.Invoking(() => (char)new TestUnion_class_string_int_bool_guid_char(1)).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'char'."); + + ((bool)new TestUnion_class_string_int_bool_guid_char(true)).Should().Be(true); + FluentActions.Invoking(() => (string)new TestUnion_class_string_int_bool_guid_char(true)).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'string'."); + FluentActions.Invoking(() => (int)new TestUnion_class_string_int_bool_guid_char(true)).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'int'."); + FluentActions.Invoking(() => (Guid)new TestUnion_class_string_int_bool_guid_char(true)).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'Guid'."); + FluentActions.Invoking(() => (char)new TestUnion_class_string_int_bool_guid_char(true)).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'char'."); + + ((Guid)new TestUnion_class_string_int_bool_guid_char(new Guid("9265932D-D3EB-4204-A027-7876F72BD66E"))).Should().Be(new Guid("9265932D-D3EB-4204-A027-7876F72BD66E")); + FluentActions.Invoking(() => (string)new TestUnion_class_string_int_bool_guid_char(new Guid("9265932D-D3EB-4204-A027-7876F72BD66E"))).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'string'."); + FluentActions.Invoking(() => (int)new TestUnion_class_string_int_bool_guid_char(new Guid("9265932D-D3EB-4204-A027-7876F72BD66E"))).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'int'."); + FluentActions.Invoking(() => (bool)new TestUnion_class_string_int_bool_guid_char(new Guid("9265932D-D3EB-4204-A027-7876F72BD66E"))).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'bool'."); + FluentActions.Invoking(() => (char)new TestUnion_class_string_int_bool_guid_char(new Guid("9265932D-D3EB-4204-A027-7876F72BD66E"))).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'char'."); + + ((char)new TestUnion_class_string_int_bool_guid_char('A')).Should().Be('A'); + FluentActions.Invoking(() => (string)new TestUnion_class_string_int_bool_guid_char('A')).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'string'."); + FluentActions.Invoking(() => (int)new TestUnion_class_string_int_bool_guid_char('A')).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'int'."); + FluentActions.Invoking(() => (bool)new TestUnion_class_string_int_bool_guid_char('A')).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'bool'."); + FluentActions.Invoking(() => (Guid)new TestUnion_class_string_int_bool_guid_char('A')).Should().Throw().WithMessage("'TestUnion_class_string_int_bool_guid_char' is not of type 'Guid'."); + } +} diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/GetHashCode.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/GetHashCode.cs new file mode 100644 index 00000000..9333ea5b --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/GetHashCode.cs @@ -0,0 +1,69 @@ +using System; +using Thinktecture.Runtime.Tests.TestUnions; + +namespace Thinktecture.Runtime.Tests.UnionTests; + +public class GetHashCode +{ + [Fact] + public void Should_return_hashcode_of_the_type_plus_inner_value() + { + ComputeHashCode(new TestUnion_class_string_int("text"), "text"); + ComputeHashCode(new TestUnion_class_string_int("text"), "TEXT"); + ComputeHashCode(new TestUnion_class_string_int(42), 42); + + ComputeHashCode(new TestUnion_class_string_int_bool("text"), "text"); + ComputeHashCode(new TestUnion_class_string_int_bool("text"), "TEXT"); + ComputeHashCode(new TestUnion_class_string_int_bool(42), 42); + ComputeHashCode(new TestUnion_class_string_int_bool(true), true); + + ComputeHashCode(new TestUnion_class_string_int_bool_guid("text"), "text"); + ComputeHashCode(new TestUnion_class_string_int_bool_guid("text"), "TEXT"); + ComputeHashCode(new TestUnion_class_string_int_bool_guid(42), 42); + ComputeHashCode(new TestUnion_class_string_int_bool_guid(true), true); + ComputeHashCode(new TestUnion_class_string_int_bool_guid(new Guid("15A033FD-5887-465C-97E9-72DBE78AD02C")), new Guid("15A033FD-5887-465C-97E9-72DBE78AD02C")); + + ComputeHashCode(new TestUnion_class_string_int_bool_guid_char("text"), "text"); + ComputeHashCode(new TestUnion_class_string_int_bool_guid_char("text"), "TEXT"); + ComputeHashCode(new TestUnion_class_string_int_bool_guid_char(42), 42); + ComputeHashCode(new TestUnion_class_string_int_bool_guid_char(true), true); + ComputeHashCode(new TestUnion_class_string_int_bool_guid_char(new Guid("15A033FD-5887-465C-97E9-72DBE78AD02C")), new Guid("15A033FD-5887-465C-97E9-72DBE78AD02C")); + ComputeHashCode(new TestUnion_class_string_int_bool_guid_char('A'), 'A'); + } + + private static void ComputeHashCode(T union, T2 value) + { + var expected = HashCode.Combine(typeof(T), value is string s ? StringComparer.OrdinalIgnoreCase.GetHashCode(s) : value?.GetHashCode()); + union.GetHashCode().Should().Be(expected); + } + + [Fact] + public void Should_return_hashcode_of_case_sensitive_union() + { + ComputeHashCodeOrdinal(new TestUnion_class_string_int_case_sensitive("text"), "text", true); + ComputeHashCodeOrdinal(new TestUnion_class_string_int_case_sensitive("text"), "TEXT", false); + + ComputeHashCodeOrdinal(new TestUnion_class_string_int_bool_case_sensitive("text"), "text", true); + ComputeHashCodeOrdinal(new TestUnion_class_string_int_bool_case_sensitive("text"), "TEXT", false); + + ComputeHashCodeOrdinal(new TestUnion_class_string_int_bool_guid_case_sensitive("text"), "text", true); + ComputeHashCodeOrdinal(new TestUnion_class_string_int_bool_guid_case_sensitive("text"), "TEXT", false); + + ComputeHashCodeOrdinal(new TestUnion_class_string_int_bool_guid_char_case_sensitive("text"), "text", true); + ComputeHashCodeOrdinal(new TestUnion_class_string_int_bool_guid_char_case_sensitive("text"), "TEXT", false); + } + + private static void ComputeHashCodeOrdinal(T union, string value, bool equal) + { + var expected = HashCode.Combine(typeof(T), StringComparer.Ordinal.GetHashCode(value)); + + if (equal) + { + union.GetHashCode().Should().Be(expected); + } + else + { + union.GetHashCode().Should().NotBe(expected); + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ImplicitCasts.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ImplicitCasts.cs new file mode 100644 index 00000000..a11cbe81 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ImplicitCasts.cs @@ -0,0 +1,65 @@ +using System; +using Thinktecture.Runtime.Tests.TestUnions; + +namespace Thinktecture.Runtime.Tests.UnionTests; + +public class ImplicitCasts +{ + [Fact] + public void Should_have_implicit_casts_from_value_having_2_values() + { + TestUnion_class_string_int stringUnion = "text"; + stringUnion.Value.Should().Be("text"); + + TestUnion_class_string_int intUnion = 42; + intUnion.Value.Should().Be(42); + } + + [Fact] + public void Should_have_implicit_casts_from_value_having_3_values() + { + TestUnion_class_string_int_bool stringUnion = "text"; + stringUnion.Value.Should().Be("text"); + + TestUnion_class_string_int_bool intUnion = 42; + intUnion.Value.Should().Be(42); + + TestUnion_class_string_int_bool boolUnion = true; + boolUnion.Value.Should().Be(true); + } + + [Fact] + public void Should_have_implicit_casts_from_value_having_4_values() + { + TestUnion_class_string_int_bool_guid stringUnion = "text"; + stringUnion.Value.Should().Be("text"); + + TestUnion_class_string_int_bool_guid intUnion = 42; + intUnion.Value.Should().Be(42); + + TestUnion_class_string_int_bool_guid boolUnion = true; + boolUnion.Value.Should().Be(true); + + TestUnion_class_string_int_bool_guid guidUnion = new Guid("2FCC1FE5-5A0D-4FE7-ADBB-356CCABEDAC2"); + guidUnion.Value.Should().Be(new Guid("2FCC1FE5-5A0D-4FE7-ADBB-356CCABEDAC2")); + } + + [Fact] + public void Should_have_implicit_casts_from_value_having_5_values() + { + TestUnion_class_string_int_bool_guid_char stringUnion = "text"; + stringUnion.Value.Should().Be("text"); + + TestUnion_class_string_int_bool_guid_char intUnion = 42; + intUnion.Value.Should().Be(42); + + TestUnion_class_string_int_bool_guid_char boolUnion = true; + boolUnion.Value.Should().Be(true); + + TestUnion_class_string_int_bool_guid_char guidUnion = new Guid("2FCC1FE5-5A0D-4FE7-ADBB-356CCABEDAC2"); + guidUnion.Value.Should().Be(new Guid("2FCC1FE5-5A0D-4FE7-ADBB-356CCABEDAC2")); + + TestUnion_class_string_int_bool_guid_char charUnion = 'A'; + charUnion.Value.Should().Be('A'); + } +} diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/InequalityOperator.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/InequalityOperator.cs new file mode 100644 index 00000000..6fff432d --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/InequalityOperator.cs @@ -0,0 +1,60 @@ +using System; +using Thinktecture.Runtime.Tests.TestUnions; + +namespace Thinktecture.Runtime.Tests.UnionTests; + +// ReSharper disable SuspiciousTypeConversion.Global +// ReSharper disable EqualExpressionComparison +// ReSharper disable ConditionIsAlwaysTrueOrFalse +public class InequalityOperator +{ + [Fact] + public void Should_compare_unions_with_2_types() + { + Compare(s => new TestUnion_class_string_int(s), + n => new TestUnion_class_string_int(n), + s => new TestUnion_class_string_int_case_sensitive(s)); + } + + [Fact] + public void Should_compare_unions_with_3_types() + { + Compare(s => new TestUnion_class_string_int_bool(s), + n => new TestUnion_class_string_int_bool(n), + s => new TestUnion_class_string_int_bool_case_sensitive(s)); + } + + [Fact] + public void Should_compare_unions_with_4_types() + { + Compare(s => new TestUnion_class_string_int_bool_guid(s), + n => new TestUnion_class_string_int_bool_guid(n), + s => new TestUnion_class_string_int_bool_guid_case_sensitive(s)); + } + + [Fact] + public void Should_compare_unions_with_5_types() + { + Compare(s => new TestUnion_class_string_int_bool_guid_char(s), + n => new TestUnion_class_string_int_bool_guid_char(n), + s => new TestUnion_class_string_int_bool_guid_char_case_sensitive(s)); + } + + private static void Compare( + Func stringFactory, + Func intFactory, + Func caseSensitiveFactory) + where T : System.Numerics.IEqualityOperators + where T2 : System.Numerics.IEqualityOperators + { + var obj = stringFactory("text"); + + (obj != null).Should().BeTrue(); + (obj != obj).Should().BeFalse(); + (obj != stringFactory("text")).Should().BeFalse(); + (obj != stringFactory("TEXT")).Should().BeFalse(); + (caseSensitiveFactory("text") != caseSensitiveFactory("TEXT")).Should().BeTrue(); + (obj != stringFactory("other text")).Should().BeTrue(); + (stringFactory("42") != intFactory(42)).Should().BeTrue(); + } +} diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/IsValue.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/IsValue.cs new file mode 100644 index 00000000..73a6e5e4 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/IsValue.cs @@ -0,0 +1,106 @@ +#nullable enable +using System; +using Thinktecture.Runtime.Tests.TestUnions; + +namespace Thinktecture.Runtime.Tests.UnionTests; + +public class IsValue +{ + [Fact] + public void Should_use_correct_index_having_2_types() + { + new TestUnion_class_string_int("text").IsString.Should().BeTrue(); + new TestUnion_class_string_int("text").IsInt32.Should().BeFalse(); + new TestUnion_class_string_int(1).IsString.Should().BeFalse(); + new TestUnion_class_string_int(1).IsInt32.Should().BeTrue(); + + new TestUnion_class_nullable_string_int(@string: null).IsString.Should().BeTrue(); + new TestUnion_class_nullable_string_int(@string: null).IsInt32.Should().BeFalse(); + new TestUnion_class_nullable_string_int("text").IsString.Should().BeTrue(); + new TestUnion_class_nullable_string_int("text").IsInt32.Should().BeFalse(); + new TestUnion_class_nullable_string_int(1).IsString.Should().BeFalse(); + new TestUnion_class_nullable_string_int(1).IsInt32.Should().BeTrue(); + + new TestUnion_class_nullable_string_nullable_int(@string: null).IsString.Should().BeTrue(); + new TestUnion_class_nullable_string_nullable_int(@string: null).IsNullableInt32.Should().BeFalse(); + new TestUnion_class_nullable_string_nullable_int("text").IsString.Should().BeTrue(); + new TestUnion_class_nullable_string_nullable_int("text").IsNullableInt32.Should().BeFalse(); + new TestUnion_class_nullable_string_nullable_int(1).IsString.Should().BeFalse(); + new TestUnion_class_nullable_string_nullable_int(1).IsNullableInt32.Should().BeTrue(); + new TestUnion_class_nullable_string_nullable_int(nullableInt32: null).IsString.Should().BeFalse(); + new TestUnion_class_nullable_string_nullable_int(nullableInt32: null).IsNullableInt32.Should().BeTrue(); + } + + [Fact] + public void Should_use_correct_index_having_3_types() + { + new TestUnion_class_string_int_bool("text").IsString.Should().BeTrue(); + new TestUnion_class_string_int_bool("text").IsInt32.Should().BeFalse(); + new TestUnion_class_string_int_bool("text").IsBoolean.Should().BeFalse(); + + new TestUnion_class_string_int_bool(1).IsString.Should().BeFalse(); + new TestUnion_class_string_int_bool(1).IsInt32.Should().BeTrue(); + new TestUnion_class_string_int_bool(1).IsBoolean.Should().BeFalse(); + + new TestUnion_class_string_int_bool(true).IsString.Should().BeFalse(); + new TestUnion_class_string_int_bool(true).IsInt32.Should().BeFalse(); + new TestUnion_class_string_int_bool(true).IsBoolean.Should().BeTrue(); + } + + [Fact] + public void Should_use_correct_index_having_4_types() + { + new TestUnion_class_string_int_bool_guid("text").IsString.Should().BeTrue(); + new TestUnion_class_string_int_bool_guid("text").IsInt32.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid("text").IsBoolean.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid("text").IsGuid.Should().BeFalse(); + + new TestUnion_class_string_int_bool_guid(1).IsString.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid(1).IsInt32.Should().BeTrue(); + new TestUnion_class_string_int_bool_guid(1).IsBoolean.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid(1).IsGuid.Should().BeFalse(); + + new TestUnion_class_string_int_bool_guid(true).IsString.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid(true).IsInt32.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid(true).IsBoolean.Should().BeTrue(); + new TestUnion_class_string_int_bool_guid(true).IsGuid.Should().BeFalse(); + + new TestUnion_class_string_int_bool_guid(new Guid("4161A501-D501-462E-85B5-034960B133D6")).IsString.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid(new Guid("4161A501-D501-462E-85B5-034960B133D6")).IsInt32.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid(new Guid("4161A501-D501-462E-85B5-034960B133D6")).IsBoolean.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid(new Guid("4161A501-D501-462E-85B5-034960B133D6")).IsGuid.Should().BeTrue(); + } + + [Fact] + public void Should_use_correct_index_having_5_types() + { + new TestUnion_class_string_int_bool_guid_char("text").IsString.Should().BeTrue(); + new TestUnion_class_string_int_bool_guid_char("text").IsInt32.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid_char("text").IsBoolean.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid_char("text").IsGuid.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid_char("text").IsChar.Should().BeFalse(); + + new TestUnion_class_string_int_bool_guid_char(1).IsString.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid_char(1).IsInt32.Should().BeTrue(); + new TestUnion_class_string_int_bool_guid_char(1).IsBoolean.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid_char(1).IsGuid.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid_char(1).IsChar.Should().BeFalse(); + + new TestUnion_class_string_int_bool_guid_char(true).IsString.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid_char(true).IsInt32.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid_char(true).IsBoolean.Should().BeTrue(); + new TestUnion_class_string_int_bool_guid_char(true).IsChar.Should().BeFalse(); + + new TestUnion_class_string_int_bool_guid_char(new Guid("4161A501-D501-462E-85B5-034960B133D6")).IsString.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid_char(new Guid("4161A501-D501-462E-85B5-034960B133D6")).IsInt32.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid_char(new Guid("4161A501-D501-462E-85B5-034960B133D6")).IsBoolean.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid_char(new Guid("4161A501-D501-462E-85B5-034960B133D6")).IsGuid.Should().BeTrue(); + new TestUnion_class_string_int_bool_guid_char(new Guid("4161A501-D501-462E-85B5-034960B133D6")).IsChar.Should().BeFalse(); + + new TestUnion_class_string_int_bool_guid_char('A').IsString.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid_char('A').IsInt32.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid_char('A').IsBoolean.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid_char('A').IsGuid.Should().BeFalse(); + new TestUnion_class_string_int_bool_guid_char('A').IsChar.Should().BeTrue(); + } +} diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/Map.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/Map.cs new file mode 100644 index 00000000..03146c90 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/Map.cs @@ -0,0 +1,98 @@ +using System; +using Thinktecture.Runtime.Tests.TestUnions; + +namespace Thinktecture.Runtime.Tests.UnionTests; + +// ReSharper disable once InconsistentNaming +public class Map +{ + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + public void Should_use_correct_arg_having_2_values(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int("text"), + 2 => new TestUnion_class_string_int(42), + _ => throw new Exception() + }; + + var calledActionOn = value.Map(@string: (object)"text", + int32: 42); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + public void Should_use_correct_arg_having_3_values(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool("text"), + 2 => new TestUnion_class_string_int_bool(42), + 3 => new TestUnion_class_string_int_bool(true), + _ => throw new Exception() + }; + + var calledActionOn = value.Map(@string: (object)"text", + int32: 42, + boolean: true); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "4CB8C761-434B-4E34-83E0-C2E1BD4FAA0B")] + public void Should_use_correct_arg_having_4_values(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid("text"), + 2 => new TestUnion_class_string_int_bool_guid(42), + 3 => new TestUnion_class_string_int_bool_guid(true), + 4 => new TestUnion_class_string_int_bool_guid(new Guid("4CB8C761-434B-4E34-83E0-C2E1BD4FAA0B")), + _ => throw new Exception() + }; + + var calledActionOn = value.Map(@string: (object)"text", + int32: 42, + boolean: true, + guid: new Guid("4CB8C761-434B-4E34-83E0-C2E1BD4FAA0B")); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "4CB8C761-434B-4E34-83E0-C2E1BD4FAA0B")] + [InlineData(5, 'A')] + public void Should_use_correct_arg_having_5_values(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid_char("text"), + 2 => new TestUnion_class_string_int_bool_guid_char(42), + 3 => new TestUnion_class_string_int_bool_guid_char(true), + 4 => new TestUnion_class_string_int_bool_guid_char(new Guid("4CB8C761-434B-4E34-83E0-C2E1BD4FAA0B")), + 5 => new TestUnion_class_string_int_bool_guid_char('A'), + _ => throw new Exception() + }; + + var calledActionOn = value.Map(@string: (object)"text", + int32: 42, + boolean: true, + guid: new Guid("4CB8C761-434B-4E34-83E0-C2E1BD4FAA0B"), + @char: 'A'); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } +} diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/MapPartially.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/MapPartially.cs new file mode 100644 index 00000000..66149fab --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/MapPartially.cs @@ -0,0 +1,192 @@ +using System; +using Thinktecture.Runtime.Tests.TestUnions; + +namespace Thinktecture.Runtime.Tests.UnionTests; + +// ReSharper disable once InconsistentNaming +public class MapPartially +{ + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + public void Should_use_correct_arg_having_2_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int("text"), + 2 => new TestUnion_class_string_int(42), + _ => throw new Exception() + }; + + var calledActionOn = value.MapPartially(@default: (object)"default", + @string: "text", + int32: 42); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "default")] + [InlineData(2, 42)] + public void Should_use_default_having_2_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int("text"), + 2 => new TestUnion_class_string_int(42), + _ => throw new Exception() + }; + + var calledActionOn = value.MapPartially(@default: (object)"default", + int32: 42); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + public void Should_use_correct_arg_having_3_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool("text"), + 2 => new TestUnion_class_string_int_bool(42), + 3 => new TestUnion_class_string_int_bool(true), + _ => throw new Exception() + }; + + var calledActionOn = value.MapPartially(@default: (object)"default", + @string: "text", + int32: 42, + boolean: true); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "default")] + [InlineData(2, 42)] + [InlineData(3, true)] + public void Should_use_default_having_3_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool("text"), + 2 => new TestUnion_class_string_int_bool(42), + 3 => new TestUnion_class_string_int_bool(true), + _ => throw new Exception() + }; + + var calledActionOn = value.MapPartially(@default: (object)"default", + int32: 42, + boolean: true); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "FCF57B31-DA82-475A-8418-5161AA1A4280")] + public void Should_use_correct_arg_having_4_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid("text"), + 2 => new TestUnion_class_string_int_bool_guid(42), + 3 => new TestUnion_class_string_int_bool_guid(true), + 4 => new TestUnion_class_string_int_bool_guid(new Guid("FCF57B31-DA82-475A-8418-5161AA1A4280")), + _ => throw new Exception() + }; + + var calledActionOn = value.MapPartially(@default: (object)"default", + @string: "text", + int32: 42, + boolean: true, + guid: new Guid("FCF57B31-DA82-475A-8418-5161AA1A4280")); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + + [Theory] + [InlineData(1, "default")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "FCF57B31-DA82-475A-8418-5161AA1A4280")] + public void Should_use_default_having_4_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid("text"), + 2 => new TestUnion_class_string_int_bool_guid(42), + 3 => new TestUnion_class_string_int_bool_guid(true), + 4 => new TestUnion_class_string_int_bool_guid(new Guid("FCF57B31-DA82-475A-8418-5161AA1A4280")), + _ => throw new Exception() + }; + + var calledActionOn = value.MapPartially(@default: (object)"default", + int32: 42, + boolean: true, + guid: new Guid("FCF57B31-DA82-475A-8418-5161AA1A4280")); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "FCF57B31-DA82-475A-8418-5161AA1A4280")] + [InlineData(5, 'A')] + public void Should_use_correct_arg_having_5_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid_char("text"), + 2 => new TestUnion_class_string_int_bool_guid_char(42), + 3 => new TestUnion_class_string_int_bool_guid_char(true), + 4 => new TestUnion_class_string_int_bool_guid_char(new Guid("FCF57B31-DA82-475A-8418-5161AA1A4280")), + 5 => new TestUnion_class_string_int_bool_guid_char('A'), + _ => throw new Exception() + }; + + var calledActionOn = value.MapPartially(@default: (object)"default", + @string: "text", + int32: 42, + boolean: true, + guid: new Guid("FCF57B31-DA82-475A-8418-5161AA1A4280"), + @char: 'A'); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + + [Theory] + [InlineData(1, "default")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "FCF57B31-DA82-475A-8418-5161AA1A4280")] + [InlineData(5, 'A')] + public void Should_use_default_having_5_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid_char("text"), + 2 => new TestUnion_class_string_int_bool_guid_char(42), + 3 => new TestUnion_class_string_int_bool_guid_char(true), + 4 => new TestUnion_class_string_int_bool_guid_char(new Guid("FCF57B31-DA82-475A-8418-5161AA1A4280")), + 5 => new TestUnion_class_string_int_bool_guid_char('A'), + _ => throw new Exception() + }; + + var calledActionOn = value.MapPartially(@default: (object)"default", + int32: 42, + boolean: true, + guid: new Guid("FCF57B31-DA82-475A-8418-5161AA1A4280"), + @char: 'A'); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } +} diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/Switch.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/Switch.cs new file mode 100644 index 00000000..16c9a3d9 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/Switch.cs @@ -0,0 +1,566 @@ +using System; +using Thinktecture.Runtime.Tests.TestUnions; + +namespace Thinktecture.Runtime.Tests.UnionTests; + +// ReSharper disable once InconsistentNaming +public class Switch +{ + public class WithAction + { + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + public void Should_use_correct_arg_having_2_values(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int("text"), + 2 => new TestUnion_class_string_int(42), + _ => throw new Exception() + }; + object calledActionOn = null; + + value.Switch(@string: v => + { + calledActionOn = v; + }, + int32: v => + { + calledActionOn = v; + }); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + public void Should_use_correct_arg_having_3_values(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool("text"), + 2 => new TestUnion_class_string_int_bool(42), + 3 => new TestUnion_class_string_int_bool(true), + _ => throw new Exception() + }; + object calledActionOn = null; + + value.Switch(@string: v => + { + calledActionOn = v; + }, + int32: v => + { + calledActionOn = v; + }, + boolean: v => + { + calledActionOn = v; + }); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "2A986EEB-1B82-46F8-A7F3-401ADC22BE33")] + public void Should_use_correct_arg_having_4_values(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid("text"), + 2 => new TestUnion_class_string_int_bool_guid(42), + 3 => new TestUnion_class_string_int_bool_guid(true), + 4 => new TestUnion_class_string_int_bool_guid(new Guid("2A986EEB-1B82-46F8-A7F3-401ADC22BE33")), + _ => throw new Exception() + }; + object calledActionOn = null; + + value.Switch(@string: v => + { + calledActionOn = v; + }, + int32: v => + { + calledActionOn = v; + }, + boolean: v => + { + calledActionOn = v; + }, + guid: v => + { + calledActionOn = v; + }); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "2A986EEB-1B82-46F8-A7F3-401ADC22BE33")] + [InlineData(5, 'A')] + public void Should_use_correct_arg_having_5_values(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid_char("text"), + 2 => new TestUnion_class_string_int_bool_guid_char(42), + 3 => new TestUnion_class_string_int_bool_guid_char(true), + 4 => new TestUnion_class_string_int_bool_guid_char(new Guid("2A986EEB-1B82-46F8-A7F3-401ADC22BE33")), + 5 => new TestUnion_class_string_int_bool_guid_char('A'), + _ => throw new Exception() + }; + object calledActionOn = null; + + value.Switch(@string: v => + { + calledActionOn = v; + }, + int32: v => + { + calledActionOn = v; + }, + boolean: v => + { + calledActionOn = v; + }, + guid: v => + { + calledActionOn = v; + }, + @char: v => + { + calledActionOn = v; + }); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + } + + public class WithActionAndContext + { + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + public void Should_pass_context_having_2_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int("text"), + 2 => new TestUnion_class_string_int(42), + _ => throw new Exception() + }; + + var context = new object(); + + object calledActionOn = null; + + value.Switch(context, + @string: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + public void Should_pass_context_having_3_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool("text"), + 2 => new TestUnion_class_string_int_bool(42), + 3 => new TestUnion_class_string_int_bool(true), + _ => throw new Exception() + }; + + var context = new object(); + + object calledActionOn = null; + + value.Switch(context, + @string: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + boolean: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "3E85ABD4-621A-4F58-8926-A842D71BB230")] + public void Should_pass_context_having_4_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid("text"), + 2 => new TestUnion_class_string_int_bool_guid(42), + 3 => new TestUnion_class_string_int_bool_guid(true), + 4 => new TestUnion_class_string_int_bool_guid(new Guid("3E85ABD4-621A-4F58-8926-A842D71BB230")), + _ => throw new Exception() + }; + + var context = new object(); + + object calledActionOn = null; + + value.Switch(context, + @string: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + boolean: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + guid: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "3E85ABD4-621A-4F58-8926-A842D71BB230")] + [InlineData(5, 'A')] + public void Should_pass_context_having_5_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid_char("text"), + 2 => new TestUnion_class_string_int_bool_guid_char(42), + 3 => new TestUnion_class_string_int_bool_guid_char(true), + 4 => new TestUnion_class_string_int_bool_guid_char(new Guid("3E85ABD4-621A-4F58-8926-A842D71BB230")), + 5 => new TestUnion_class_string_int_bool_guid_char('A'), + _ => throw new Exception() + }; + + var context = new object(); + + object calledActionOn = null; + + value.Switch(context, + @string: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + boolean: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + guid: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + @char: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + } + + public class WithFunc + { + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + public void Should_call_correct_arg_having_2_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int("text"), + 2 => new TestUnion_class_string_int(42), + _ => throw new Exception() + }; + + var calledActionOn = value.Switch(@string: v => (object)v, + int32: v => v); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + public void Should_call_correct_arg_having_3_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool("text"), + 2 => new TestUnion_class_string_int_bool(42), + 3 => new TestUnion_class_string_int_bool(true), + _ => throw new Exception() + }; + + var calledActionOn = value.Switch(@string: v => (object)v, + int32: v => v, + boolean: v => v); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "F8002E79-5465-4797-AD3F-A6503ADF066E")] + public void Should_call_correct_arg_having_4_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid("text"), + 2 => new TestUnion_class_string_int_bool_guid(42), + 3 => new TestUnion_class_string_int_bool_guid(true), + 4 => new TestUnion_class_string_int_bool_guid(new Guid("F8002E79-5465-4797-AD3F-A6503ADF066E")), + _ => throw new Exception() + }; + + var calledActionOn = value.Switch(@string: v => (object)v, + int32: v => v, + boolean: v => v, + guid: v => v); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "F8002E79-5465-4797-AD3F-A6503ADF066E")] + [InlineData(5, 'A')] + public void Should_call_correct_arg_having_5_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid_char("text"), + 2 => new TestUnion_class_string_int_bool_guid_char(42), + 3 => new TestUnion_class_string_int_bool_guid_char(true), + 4 => new TestUnion_class_string_int_bool_guid_char(new Guid("F8002E79-5465-4797-AD3F-A6503ADF066E")), + 5 => new TestUnion_class_string_int_bool_guid_char('A'), + _ => throw new Exception() + }; + + var calledActionOn = value.Switch(@string: v => (object)v, + int32: v => v, + boolean: v => v, + guid: v => v, + @char: v => v); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + } + + public class WithFuncAndContext + { + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + public void Should_pass_context_having_2_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int("text"), + 2 => new TestUnion_class_string_int(42), + _ => throw new Exception() + }; + + var context = new object(); + var calledActionOn = value.Switch(context, + @string: (ctx, v) => + { + ctx.Should().Be(context); + return (object)v; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + public void Should_pass_context_having_3_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool("text"), + 2 => new TestUnion_class_string_int_bool(42), + 3 => new TestUnion_class_string_int_bool(true), + _ => throw new Exception() + }; + + var context = new object(); + var calledActionOn = value.Switch(context, + @string: (ctx, v) => + { + ctx.Should().Be(context); + return (object)v; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + boolean: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "6EF10862-7FC4-4AEB-BC92-21E798AC54D0")] + public void Should_pass_context_having_4_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid("text"), + 2 => new TestUnion_class_string_int_bool_guid(42), + 3 => new TestUnion_class_string_int_bool_guid(true), + 4 => new TestUnion_class_string_int_bool_guid(new Guid("6EF10862-7FC4-4AEB-BC92-21E798AC54D0")), + _ => throw new Exception() + }; + + var context = new object(); + var calledActionOn = value.Switch(context, + @string: (ctx, v) => + { + ctx.Should().Be(context); + return (object)v; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + boolean: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + guid: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "6EF10862-7FC4-4AEB-BC92-21E798AC54D0")] + [InlineData(5, 'A')] + public void Should_pass_context_having_5_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid_char("text"), + 2 => new TestUnion_class_string_int_bool_guid_char(42), + 3 => new TestUnion_class_string_int_bool_guid_char(true), + 4 => new TestUnion_class_string_int_bool_guid_char(new Guid("6EF10862-7FC4-4AEB-BC92-21E798AC54D0")), + 5 => new TestUnion_class_string_int_bool_guid_char('A'), + _ => throw new Exception() + }; + + var context = new object(); + var calledActionOn = value.Switch(context, + @string: (ctx, v) => + { + ctx.Should().Be(context); + return (object)v; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + boolean: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + guid: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + @char: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/SwitchPartially.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/SwitchPartially.cs new file mode 100644 index 00000000..519c5a22 --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/SwitchPartially.cs @@ -0,0 +1,1085 @@ +using System; +using Thinktecture.Runtime.Tests.TestUnions; + +namespace Thinktecture.Runtime.Tests.UnionTests; + +// ReSharper disable once InconsistentNaming +public class SwitchPartially +{ + public class WithAction + { + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + public void Should_use_correct_arg_having_2_values(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int("text"), + 2 => new TestUnion_class_string_int(42), + _ => throw new Exception() + }; + object calledActionOn = null; + + value.SwitchPartially(@string: v => + { + calledActionOn = v; + }, + int32: v => + { + calledActionOn = v; + }); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + public void Should_use_correct_arg_having_3_values(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool("text"), + 2 => new TestUnion_class_string_int_bool(42), + 3 => new TestUnion_class_string_int_bool(true), + _ => throw new Exception() + }; + object calledActionOn = null; + + value.SwitchPartially(@string: v => + { + calledActionOn = v; + }, + int32: v => + { + calledActionOn = v; + }, + boolean: v => + { + calledActionOn = v; + }); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "2A986EEB-1B82-46F8-A7F3-401ADC22BE33")] + public void Should_use_correct_arg_having_4_values(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid("text"), + 2 => new TestUnion_class_string_int_bool_guid(42), + 3 => new TestUnion_class_string_int_bool_guid(true), + 4 => new TestUnion_class_string_int_bool_guid(new Guid("2A986EEB-1B82-46F8-A7F3-401ADC22BE33")), + _ => throw new Exception() + }; + object calledActionOn = null; + + value.SwitchPartially(@string: v => + { + calledActionOn = v; + }, + int32: v => + { + calledActionOn = v; + }, + boolean: v => + { + calledActionOn = v; + }, + guid: v => + { + calledActionOn = v; + }); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "2A986EEB-1B82-46F8-A7F3-401ADC22BE33")] + [InlineData(5, 'A')] + public void Should_use_correct_arg_having_5_values(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid_char("text"), + 2 => new TestUnion_class_string_int_bool_guid_char(42), + 3 => new TestUnion_class_string_int_bool_guid_char(true), + 4 => new TestUnion_class_string_int_bool_guid_char(new Guid("2A986EEB-1B82-46F8-A7F3-401ADC22BE33")), + 5 => new TestUnion_class_string_int_bool_guid_char('A'), + _ => throw new Exception() + }; + object calledActionOn = null; + + value.SwitchPartially(@string: v => + { + calledActionOn = v; + }, + int32: v => + { + calledActionOn = v; + }, + boolean: v => + { + calledActionOn = v; + }, + guid: v => + { + calledActionOn = v; + }, + @char: v => + { + calledActionOn = v; + }); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + + [Theory] + [InlineData(1, "default:text")] + [InlineData(2, 42)] + public void Should_use_default_arg_having_2_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int("text"), + 2 => new TestUnion_class_string_int(42), + _ => throw new Exception() + }; + object calledActionOn = null; + + value.SwitchPartially(@default: v => + { + calledActionOn = $"default:{v}"; + }, + int32: v => + { + calledActionOn = v; + }); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "default:text")] + [InlineData(2, 42)] + [InlineData(3, true)] + public void Should_use_default_arg_having_3_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool("text"), + 2 => new TestUnion_class_string_int_bool(42), + 3 => new TestUnion_class_string_int_bool(true), + _ => throw new Exception() + }; + object calledActionOn = null; + + value.SwitchPartially(@default: v => + { + calledActionOn = $"default:{v}"; + }, + int32: v => + { + calledActionOn = v; + }, + boolean: v => + { + calledActionOn = v; + }); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "default:text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "DA9B90DD-AF66-4856-B084-1B1BB21DEA9B")] + public void Should_use_default_arg_having_4_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid("text"), + 2 => new TestUnion_class_string_int_bool_guid(42), + 3 => new TestUnion_class_string_int_bool_guid(true), + 4 => new TestUnion_class_string_int_bool_guid(new Guid("DA9B90DD-AF66-4856-B084-1B1BB21DEA9B")), + _ => throw new Exception() + }; + object calledActionOn = null; + + value.SwitchPartially(@default: v => + { + calledActionOn = $"default:{v}"; + }, + int32: v => + { + calledActionOn = v; + }, + boolean: v => + { + calledActionOn = v; + }, + guid: v => + { + calledActionOn = v; + }); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + + [Theory] + [InlineData(1, "default:text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "DA9B90DD-AF66-4856-B084-1B1BB21DEA9B")] + [InlineData(5, 'A')] + public void Should_use_default_arg_having_5_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid_char("text"), + 2 => new TestUnion_class_string_int_bool_guid_char(42), + 3 => new TestUnion_class_string_int_bool_guid_char(true), + 4 => new TestUnion_class_string_int_bool_guid_char(new Guid("DA9B90DD-AF66-4856-B084-1B1BB21DEA9B")), + 5 => new TestUnion_class_string_int_bool_guid_char('A'), + _ => throw new Exception() + }; + object calledActionOn = null; + + value.SwitchPartially(@default: v => + { + calledActionOn = $"default:{v}"; + }, + int32: v => + { + calledActionOn = v; + }, + boolean: v => + { + calledActionOn = v; + }, + guid: v => + { + calledActionOn = v; + }, + @char: v => + { + calledActionOn = v; + }); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + } + + public class WithActionAndContext + { + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + public void Should_pass_context_having_2_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int("text"), + 2 => new TestUnion_class_string_int(42), + _ => throw new Exception() + }; + + var context = new object(); + object calledActionOn = null; + + value.SwitchPartially(context, + @string: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + public void Should_pass_context_having_3_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool("text"), + 2 => new TestUnion_class_string_int_bool(42), + 3 => new TestUnion_class_string_int_bool(true), + _ => throw new Exception() + }; + + var context = new object(); + object calledActionOn = null; + + value.SwitchPartially(context, + @string: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + boolean: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "9FCEBE8E-AEED-4ADE-B597-562AFB9C9733")] + public void Should_pass_context_having_5_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid("text"), + 2 => new TestUnion_class_string_int_bool_guid(42), + 3 => new TestUnion_class_string_int_bool_guid(true), + 4 => new TestUnion_class_string_int_bool_guid(new Guid("9FCEBE8E-AEED-4ADE-B597-562AFB9C9733")), + _ => throw new Exception() + }; + + var context = new object(); + object calledActionOn = null; + + value.SwitchPartially(context, + @string: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + boolean: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + guid: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + + [Theory] + [InlineData(1, "default:text")] + [InlineData(2, 42)] + public void Should_pass_context_to_default_having_2_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int("text"), + 2 => new TestUnion_class_string_int(42), + _ => throw new Exception() + }; + + var context = new object(); + object calledActionOn = null; + + value.SwitchPartially(context, + @default: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = $"default:{v}"; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "default:text")] + [InlineData(2, 42)] + [InlineData(3, true)] + public void Should_pass_context_to_default_having_3_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool("text"), + 2 => new TestUnion_class_string_int_bool(42), + 3 => new TestUnion_class_string_int_bool(true), + _ => throw new Exception() + }; + + var context = new object(); + object calledActionOn = null; + + value.SwitchPartially(context, + @default: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = $"default:{v}"; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + boolean: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "default:text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "81F697B0-5B9B-4441-89EB-2970A85C1069")] + public void Should_pass_context_to_default_having_4_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid("text"), + 2 => new TestUnion_class_string_int_bool_guid(42), + 3 => new TestUnion_class_string_int_bool_guid(true), + 4 => new TestUnion_class_string_int_bool_guid(new Guid("81F697B0-5B9B-4441-89EB-2970A85C1069")), + _ => throw new Exception() + }; + + var context = new object(); + object calledActionOn = null; + + value.SwitchPartially(context, + @default: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = $"default:{v}"; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + boolean: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + guid: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + + [Theory] + [InlineData(1, "default:text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "81F697B0-5B9B-4441-89EB-2970A85C1069")] + [InlineData(5, 'A')] + public void Should_pass_context_to_default_having_5_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid_char("text"), + 2 => new TestUnion_class_string_int_bool_guid_char(42), + 3 => new TestUnion_class_string_int_bool_guid_char(true), + 4 => new TestUnion_class_string_int_bool_guid_char(new Guid("81F697B0-5B9B-4441-89EB-2970A85C1069")), + 5 => new TestUnion_class_string_int_bool_guid_char('A'), + _ => throw new Exception() + }; + + var context = new object(); + object calledActionOn = null; + + value.SwitchPartially(context, + @default: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = $"default:{v}"; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + boolean: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + guid: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }, + @char: (ctx, v) => + { + ctx.Should().Be(context); + calledActionOn = v; + }); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + } + + public class WithFunc + { + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + public void Should_use_correct_arg_having_2_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int("text"), + 2 => new TestUnion_class_string_int(42), + _ => throw new Exception() + }; + + var calledActionOn = value.SwitchPartially(@default: v => $"default:{v}", + @string: v => (object)v, + int32: v => v); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + public void Should_use_correct_arg_having_3_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool("text"), + 2 => new TestUnion_class_string_int_bool(42), + 3 => new TestUnion_class_string_int_bool(true), + _ => throw new Exception() + }; + + var calledActionOn = value.SwitchPartially(@default: v => $"default:{v}", + @string: v => (object)v, + int32: v => v, + boolean: v => v); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "D4EF64BB-730B-4D5C-94A1-C019F83EF945")] + public void Should_use_correct_arg_having_4_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid("text"), + 2 => new TestUnion_class_string_int_bool_guid(42), + 3 => new TestUnion_class_string_int_bool_guid(true), + 4 => new TestUnion_class_string_int_bool_guid(new Guid("D4EF64BB-730B-4D5C-94A1-C019F83EF945")), + _ => throw new Exception() + }; + + var calledActionOn = value.SwitchPartially(@default: v => $"default:{v}", + @string: v => (object)v, + int32: v => v, + boolean: v => v, + guid: v => v); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "D4EF64BB-730B-4D5C-94A1-C019F83EF945")] + [InlineData(5, 'A')] + public void Should_use_correct_arg_having_5_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid_char("text"), + 2 => new TestUnion_class_string_int_bool_guid_char(42), + 3 => new TestUnion_class_string_int_bool_guid_char(true), + 4 => new TestUnion_class_string_int_bool_guid_char(new Guid("D4EF64BB-730B-4D5C-94A1-C019F83EF945")), + 5 => new TestUnion_class_string_int_bool_guid_char('A'), + _ => throw new Exception() + }; + + var calledActionOn = value.SwitchPartially(@default: v => $"default:{v}", + @string: v => (object)v, + int32: v => v, + boolean: v => v, + guid: v => v, + @char: v => v); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + + [Theory] + [InlineData(1, "default:text")] + [InlineData(2, 42)] + public void Should_use_default_arg_having_2_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int("text"), + 2 => new TestUnion_class_string_int(42), + _ => throw new Exception() + }; + + var calledActionOn = value.SwitchPartially(@default: v => (object)$"default:{v}", + int32: v => v); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "default:text")] + [InlineData(2, 42)] + [InlineData(3, true)] + public void Should_use_default_arg_having_3_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool("text"), + 2 => new TestUnion_class_string_int_bool(42), + 3 => new TestUnion_class_string_int_bool(true), + _ => throw new Exception() + }; + + var calledActionOn = value.SwitchPartially(@default: v => (object)$"default:{v}", + int32: v => v, + boolean: v => v); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "default:text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "5C392E64-2AEC-401F-98A9-35E5913B369A")] + public void Should_use_default_arg_having_4_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid("text"), + 2 => new TestUnion_class_string_int_bool_guid(42), + 3 => new TestUnion_class_string_int_bool_guid(true), + 4 => new TestUnion_class_string_int_bool_guid(new Guid("5C392E64-2AEC-401F-98A9-35E5913B369A")), + _ => throw new Exception() + }; + + var calledActionOn = value.SwitchPartially(@default: v => (object)$"default:{v}", + int32: v => v, + boolean: v => v, + guid: v => v); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + + [Theory] + [InlineData(1, "default:text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "5C392E64-2AEC-401F-98A9-35E5913B369A")] + [InlineData(5, 'A')] + public void Should_use_default_arg_having_5_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid_char("text"), + 2 => new TestUnion_class_string_int_bool_guid_char(42), + 3 => new TestUnion_class_string_int_bool_guid_char(true), + 4 => new TestUnion_class_string_int_bool_guid_char(new Guid("5C392E64-2AEC-401F-98A9-35E5913B369A")), + 5 => new TestUnion_class_string_int_bool_guid_char('A'), + _ => throw new Exception() + }; + + var calledActionOn = value.SwitchPartially(@default: v => (object)$"default:{v}", + int32: v => v, + boolean: v => v, + guid: v => v, + @char: v => v); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + } + + public class WithFuncAndContext + { + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + public void Should_pass_context_having_2_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int("text"), + 2 => new TestUnion_class_string_int(42), + _ => throw new Exception() + }; + + var context = new object(); + + var calledActionOn = value.SwitchPartially(context, + @default: (ctx, v) => + { + ctx.Should().Be(context); + return (object)$"default:{v}"; + }, + @string: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + public void Should_pass_context_having_3_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool("text"), + 2 => new TestUnion_class_string_int_bool(42), + 3 => new TestUnion_class_string_int_bool(true), + _ => throw new Exception() + }; + + var context = new object(); + + var calledActionOn = value.SwitchPartially(context, + @default: (ctx, v) => + { + ctx.Should().Be(context); + return (object)$"default:{v}"; + }, + @string: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + boolean: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "10C287C8-4D64-45CC-859E-873024D53DE3")] + public void Should_pass_context_having_4_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid("text"), + 2 => new TestUnion_class_string_int_bool_guid(42), + 3 => new TestUnion_class_string_int_bool_guid(true), + 4 => new TestUnion_class_string_int_bool_guid(new Guid("10C287C8-4D64-45CC-859E-873024D53DE3")), + _ => throw new Exception() + }; + + var context = new object(); + + var calledActionOn = value.SwitchPartially(context, + @default: (ctx, v) => + { + ctx.Should().Be(context); + return (object)$"default:{v}"; + }, + @string: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + boolean: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + guid: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + + [Theory] + [InlineData(1, "text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "10C287C8-4D64-45CC-859E-873024D53DE3")] + [InlineData(5, 'A')] + public void Should_pass_context_having_5_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid_char("text"), + 2 => new TestUnion_class_string_int_bool_guid_char(42), + 3 => new TestUnion_class_string_int_bool_guid_char(true), + 4 => new TestUnion_class_string_int_bool_guid_char(new Guid("10C287C8-4D64-45CC-859E-873024D53DE3")), + 5 => new TestUnion_class_string_int_bool_guid_char('A'), + _ => throw new Exception() + }; + + var context = new object(); + + var calledActionOn = value.SwitchPartially(context, + @default: (ctx, v) => + { + ctx.Should().Be(context); + return (object)$"default:{v}"; + }, + @string: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + boolean: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + guid: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + @char: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + + [Theory] + [InlineData(1, "default:text")] + [InlineData(2, 42)] + public void Should_pass_context_to_default_having_2_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int("text"), + 2 => new TestUnion_class_string_int(42), + _ => throw new Exception() + }; + + var context = new object(); + + var calledActionOn = value.SwitchPartially(context, + @default: (ctx, v) => + { + ctx.Should().Be(context); + return (object)$"default:{v}"; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "default:text")] + [InlineData(2, 42)] + [InlineData(3, true)] + public void Should_pass_context_to_default_having_3_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool("text"), + 2 => new TestUnion_class_string_int_bool(42), + 3 => new TestUnion_class_string_int_bool(true), + _ => throw new Exception() + }; + + var context = new object(); + + var calledActionOn = value.SwitchPartially(context, + @default: (ctx, v) => + { + ctx.Should().Be(context); + return (object)$"default:{v}"; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + boolean: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }); + + calledActionOn.Should().Be(expected); + } + + [Theory] + [InlineData(1, "default:text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "1F99EE8B-862C-4068-B5E1-015EA81AA470")] + public void Should_pass_context_to_default_having_4_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid("text"), + 2 => new TestUnion_class_string_int_bool_guid(42), + 3 => new TestUnion_class_string_int_bool_guid(true), + 4 => new TestUnion_class_string_int_bool_guid(new Guid("1F99EE8B-862C-4068-B5E1-015EA81AA470")), + _ => throw new Exception() + }; + + var context = new object(); + + var calledActionOn = value.SwitchPartially(context, + @default: (ctx, v) => + { + ctx.Should().Be(context); + return (object)$"default:{v}"; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + boolean: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + guid: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + + [Theory] + [InlineData(1, "default:text")] + [InlineData(2, 42)] + [InlineData(3, true)] + [InlineData(4, "1F99EE8B-862C-4068-B5E1-015EA81AA470")] + [InlineData(5, 'A')] + public void Should_pass_context_to_default_having_5_types(int index, object expected) + { + var value = index switch + { + 1 => new TestUnion_class_string_int_bool_guid_char("text"), + 2 => new TestUnion_class_string_int_bool_guid_char(42), + 3 => new TestUnion_class_string_int_bool_guid_char(true), + 4 => new TestUnion_class_string_int_bool_guid_char(new Guid("1F99EE8B-862C-4068-B5E1-015EA81AA470")), + 5 => new TestUnion_class_string_int_bool_guid_char('A'), + _ => throw new Exception() + }; + + var context = new object(); + + var calledActionOn = value.SwitchPartially(context, + @default: (ctx, v) => + { + ctx.Should().Be(context); + return (object)$"default:{v}"; + }, + int32: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + boolean: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + guid: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }, + @char: (ctx, v) => + { + ctx.Should().Be(context); + return v; + }); + + calledActionOn.Should().Be(index == 4 ? new Guid((string)expected) : expected); + } + } +} diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ToString.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ToString.cs new file mode 100644 index 00000000..5c3eb25d --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/ToString.cs @@ -0,0 +1,41 @@ +using System; +using Thinktecture.Runtime.Tests.TestUnions; + +namespace Thinktecture.Runtime.Tests.UnionTests; + +public class ToString +{ + [Fact] + public void Should_return_string_representation_of_the_inner_value_having_2_types() + { + new TestUnion_class_string_int("text").ToString().Should().Be("text"); + new TestUnion_class_string_int(42).ToString().Should().Be("42"); + } + + [Fact] + public void Should_return_string_representation_of_the_inner_value_having_3_types() + { + new TestUnion_class_string_int_bool("text").ToString().Should().Be("text"); + new TestUnion_class_string_int_bool(42).ToString().Should().Be("42"); + new TestUnion_class_string_int_bool(true).ToString().Should().Be("True"); + } + + [Fact] + public void Should_return_string_representation_of_the_inner_value_having_4_types() + { + new TestUnion_class_string_int_bool_guid("text").ToString().Should().Be("text"); + new TestUnion_class_string_int_bool_guid(42).ToString().Should().Be("42"); + new TestUnion_class_string_int_bool_guid(true).ToString().Should().Be("True"); + new TestUnion_class_string_int_bool_guid(new Guid("ED91613B-C9A5-4762-A5A7-A3F615F81CA6")).ToString().Should().Be("ed91613b-c9a5-4762-a5a7-a3f615f81ca6"); + } + + [Fact] + public void Should_return_string_representation_of_the_inner_value_having_5_types() + { + new TestUnion_class_string_int_bool_guid_char("text").ToString().Should().Be("text"); + new TestUnion_class_string_int_bool_guid_char(42).ToString().Should().Be("42"); + new TestUnion_class_string_int_bool_guid_char(true).ToString().Should().Be("True"); + new TestUnion_class_string_int_bool_guid_char(new Guid("ED91613B-C9A5-4762-A5A7-A3F615F81CA6")).ToString().Should().Be("ed91613b-c9a5-4762-a5a7-a3f615f81ca6"); + new TestUnion_class_string_int_bool_guid_char('A').ToString().Should().Be("A"); + } +} diff --git a/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/Value.cs b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/Value.cs new file mode 100644 index 00000000..a204164f --- /dev/null +++ b/test/Thinktecture.Runtime.Extensions.Tests/UnionTests/Value.cs @@ -0,0 +1,51 @@ +#nullable enable +using System; +using Thinktecture.Runtime.Tests.TestUnions; + +namespace Thinktecture.Runtime.Tests.UnionTests; + +public class Value +{ + [Fact] + public void Should_return_correct_value_having_2_types() + { + new TestUnion_class_string_int("text").Value.Should().Be("text"); + new TestUnion_class_string_int(1).Value.Should().Be(1); + + new TestUnion_class_nullable_string_int(@string: null).Value.Should().BeNull(); + new TestUnion_class_nullable_string_int("text").Value.Should().Be("text"); + new TestUnion_class_nullable_string_int(1).Value.Should().Be(1); + + new TestUnion_class_nullable_string_nullable_int(@string: null).Value.Should().BeNull(); + new TestUnion_class_nullable_string_nullable_int("text").Value.Should().Be("text"); + new TestUnion_class_nullable_string_nullable_int(1).Value.Should().Be(1); + new TestUnion_class_nullable_string_nullable_int(nullableInt32: null).Value.Should().BeNull(); + } + + [Fact] + public void Should_return_correct_value_having_3_types() + { + new TestUnion_class_string_int_bool("text").Value.Should().Be("text"); + new TestUnion_class_string_int_bool(1).Value.Should().Be(1); + new TestUnion_class_string_int_bool(true).Value.Should().Be(true); + } + + [Fact] + public void Should_return_correct_value_having_4_types() + { + new TestUnion_class_string_int_bool_guid("text").Value.Should().Be("text"); + new TestUnion_class_string_int_bool_guid(1).Value.Should().Be(1); + new TestUnion_class_string_int_bool_guid(true).Value.Should().Be(true); + new TestUnion_class_string_int_bool_guid(new Guid("04F2DA71-1E0F-4AA4-AD1E-CE56BEDED52B")).Value.Should().Be(new Guid("04F2DA71-1E0F-4AA4-AD1E-CE56BEDED52B")); + } + + [Fact] + public void Should_return_correct_value_having_5_types() + { + new TestUnion_class_string_int_bool_guid_char("text").Value.Should().Be("text"); + new TestUnion_class_string_int_bool_guid_char(1).Value.Should().Be(1); + new TestUnion_class_string_int_bool_guid_char(true).Value.Should().Be(true); + new TestUnion_class_string_int_bool_guid_char(new Guid("04F2DA71-1E0F-4AA4-AD1E-CE56BEDED52B")).Value.Should().Be(new Guid("04F2DA71-1E0F-4AA4-AD1E-CE56BEDED52B")); + new TestUnion_class_string_int_bool_guid_char('A').Value.Should().Be('A'); + } +}