Skip to content

Commit

Permalink
Conversion operators of a keyed value objects can be configured
Browse files Browse the repository at this point in the history
  • Loading branch information
PawelGerr committed Jan 23, 2025
1 parent 6d6703f commit 7deffed
Show file tree
Hide file tree
Showing 91 changed files with 4,961 additions and 53 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<Copyright>(c) $([System.DateTime]::Now.Year), Pawel Gerr. All rights reserved.</Copyright>
<VersionPrefix>8.1.0</VersionPrefix>
<VersionPrefix>8.2.0</VersionPrefix>
<Authors>Pawel Gerr</Authors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageProjectUrl>https://github.com/PawelGerr/Thinktecture.Runtime.Extensions</PackageProjectUrl>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public CustomerId CustomerId_NewId()
}
}

[ValueObject<Guid>]
[ValueObject<Guid>(ConversionToKeyMemberType = ConversionOperatorsGeneration.Explicit)]
public partial struct CustomerId
{
public static CustomerId NewId() => new(Guid.NewGuid());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public sealed class AllValueObjectSettings : IEquatable<AllValueObjectSettings>,
public OperatorsGeneration EqualityComparisonOperators { get; }
public string DefaultInstancePropertyName { get; }
public bool AllowDefaultStructs { get; }
public ConversionOperatorsGeneration ConversionFromKeyMemberType { get; }
public ConversionOperatorsGeneration UnsafeConversionToKeyMemberType { get; }
public ConversionOperatorsGeneration ConversionToKeyMemberType { get; }

public AllValueObjectSettings(AttributeData valueObjectAttribute)
{
Expand All @@ -49,6 +52,9 @@ public AllValueObjectSettings(AttributeData valueObjectAttribute)
ComparisonOperators = valueObjectAttribute.FindComparisonOperators();
DefaultInstancePropertyName = valueObjectAttribute.FindDefaultInstancePropertyName() ?? "Empty";
AllowDefaultStructs = valueObjectAttribute.FindAllowDefaultStructs();
ConversionToKeyMemberType = valueObjectAttribute.FindConversionToKeyMemberType() ?? ConversionOperatorsGeneration.Implicit;
UnsafeConversionToKeyMemberType = valueObjectAttribute.FindUnsafeConversionToKeyMemberType() ?? ConversionOperatorsGeneration.Explicit;
ConversionFromKeyMemberType = valueObjectAttribute.FindConversionFromKeyMemberType() ?? ConversionOperatorsGeneration.Explicit;

// Comparison operators depend on the equality comparison operators
if (ComparisonOperators > EqualityComparisonOperators)
Expand Down Expand Up @@ -88,7 +94,10 @@ public bool Equals(AllValueObjectSettings? other)
&& ComparisonOperators == other.ComparisonOperators
&& EqualityComparisonOperators == other.EqualityComparisonOperators
&& DefaultInstancePropertyName == other.DefaultInstancePropertyName
&& AllowDefaultStructs == other.AllowDefaultStructs;
&& AllowDefaultStructs == other.AllowDefaultStructs
&& ConversionToKeyMemberType == other.ConversionToKeyMemberType
&& UnsafeConversionToKeyMemberType == other.UnsafeConversionToKeyMemberType
&& ConversionFromKeyMemberType == other.ConversionFromKeyMemberType;
}

public override int GetHashCode()
Expand Down Expand Up @@ -117,6 +126,9 @@ public override int GetHashCode()
hashCode = (hashCode * 397) ^ (int)EqualityComparisonOperators;
hashCode = (hashCode * 397) ^ DefaultInstancePropertyName.GetHashCode();
hashCode = (hashCode * 397) ^ AllowDefaultStructs.GetHashCode();
hashCode = (hashCode * 397) ^ (int)ConversionToKeyMemberType;
hashCode = (hashCode * 397) ^ (int)UnsafeConversionToKeyMemberType;
hashCode = (hashCode * 397) ^ (int)ConversionFromKeyMemberType;

return hashCode;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,11 @@ private void GenerateValueObject(CancellationToken cancellationToken)
}

GenerateToValue();
GenerateImplicitConversionToKey();
GenerateExplicitConversionToKey();
GenerateSafeConversionToKey();
GenerateUnsafeConversionToKey();

if (!_state.Settings.SkipFactoryMethods)
GenerateExplicitConversion(emptyStringYieldsNull);
GenerateConversionFromKey(emptyStringYieldsNull);

cancellationToken.ThrowIfCancellationRequested();

Expand Down Expand Up @@ -197,22 +197,22 @@ internal static void ModuleInit()
");
}

private void GenerateImplicitConversionToKey()
private void GenerateSafeConversionToKey()
{
var keyMember = _state.KeyMember;

if (keyMember.IsInterface)
if (_state.Settings.ConversionToKeyMemberType == ConversionOperatorsGeneration.None || keyMember.IsInterface)
return;

_sb.Append(@"
/// <summary>
/// Implicit conversion to the type ").AppendTypeForXmlComment(keyMember).Append(@".
/// ").Append(_state.Settings.ConversionToKeyMemberType == ConversionOperatorsGeneration.Implicit ? "Implicit" : "Explicit").Append(" conversion to the type ").AppendTypeForXmlComment(keyMember).Append(@".
/// </summary>
/// <param name=""obj"">Object to covert.</param>
/// <returns>The <see cref=""").Append(keyMember.Name).Append(@"""/> of provided <paramref name=""obj""/> or <c>default</c> if <paramref name=""obj""/> is <c>null</c>.</returns>
[return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(""obj"")]
public static implicit operator ").AppendTypeFullyQualifiedNullable(keyMember).Append("(").AppendTypeFullyQualifiedNullable(_state).Append(@" obj)
public static ").AppendConversionOperator(_state.Settings.ConversionToKeyMemberType).Append(" operator ").AppendTypeFullyQualifiedNullable(keyMember).Append("(").AppendTypeFullyQualifiedNullable(_state).Append(@" obj)
{
return obj?.").Append(keyMember.Name).Append(@";
}");
Expand All @@ -225,33 +225,33 @@ private void GenerateImplicitConversionToKey()
_sb.Append(@"
/// <summary>
/// Implicit conversion to the type ").AppendTypeForXmlComment(keyMember).Append(@".
/// ").Append(_state.Settings.ConversionToKeyMemberType == ConversionOperatorsGeneration.Implicit ? "Implicit" : "Explicit").Append(" conversion to the type ").AppendTypeForXmlComment(keyMember).Append(@".
/// </summary>
/// <param name=""obj"">Object to covert.</param>
/// <returns>The <see cref=""").Append(keyMember.Name).Append(@"""/> of provided <paramref name=""obj""/>.</returns>
public static implicit operator ").AppendTypeFullyQualified(keyMember).Append("(").AppendTypeFullyQualified(_state).Append(@" obj)
public static ").AppendConversionOperator(_state.Settings.ConversionToKeyMemberType).Append(" operator ").AppendTypeFullyQualified(keyMember).Append("(").AppendTypeFullyQualified(_state).Append(@" obj)
{
return obj.").Append(keyMember.Name).Append(@";
}");
}

private void GenerateExplicitConversionToKey()
private void GenerateUnsafeConversionToKey()
{
var keyMember = _state.KeyMember;

if (keyMember.IsInterface || keyMember.IsReferenceType || !_state.IsReferenceType)
if (_state.Settings.UnsafeConversionToKeyMemberType == ConversionOperatorsGeneration.None || keyMember.IsInterface || keyMember.IsReferenceType || !_state.IsReferenceType)
return;

_sb.Append(@"
/// <summary>
/// Explicit conversion to the type ").AppendTypeForXmlComment(keyMember).Append(@".
/// ").Append(_state.Settings.UnsafeConversionToKeyMemberType == ConversionOperatorsGeneration.Implicit ? "Implicit" : "Explicit").Append(" conversion to the type ").AppendTypeForXmlComment(keyMember).Append(@".
/// </summary>
/// <param name=""obj"">Object to covert.</param>
/// <returns>The <see cref=""").Append(keyMember.Name).Append(@"""/> of provided <paramref name=""obj""/>.</returns>
/// <exception cref=""System.NullReferenceException"">If <paramref name=""obj""/> is <c>null</c>.</exception>
[return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull(""obj"")]
public static explicit operator ").AppendTypeFullyQualified(keyMember).Append("(").AppendTypeFullyQualified(_state).Append(@" obj)
public static ").AppendConversionOperator(_state.Settings.UnsafeConversionToKeyMemberType).Append(" operator ").AppendTypeFullyQualified(keyMember).Append("(").AppendTypeFullyQualified(_state).Append(@" obj)
{
if(obj is null)
throw new global::System.NullReferenceException();
Expand All @@ -260,11 +260,11 @@ private void GenerateExplicitConversionToKey()
}");
}

private void GenerateExplicitConversion(bool emptyStringYieldsNull)
private void GenerateConversionFromKey(bool emptyStringYieldsNull)
{
var keyMember = _state.KeyMember;

if (keyMember.IsInterface)
if (_state.Settings.ConversionFromKeyMemberType == ConversionOperatorsGeneration.None || keyMember.IsInterface)
return;

var bothAreReferenceTypes = _state.IsReferenceType && keyMember.IsReferenceType;
Expand All @@ -273,10 +273,10 @@ private void GenerateExplicitConversion(bool emptyStringYieldsNull)
_sb.Append(@"
/// <summary>
/// Explicit conversion from the type ").AppendTypeForXmlComment(keyMember).Append(@".
/// ").Append(_state.Settings.ConversionFromKeyMemberType == ConversionOperatorsGeneration.Implicit ? "Implicit" : "Explicit").Append(" conversion from the type ").AppendTypeForXmlComment(keyMember).Append(@".
/// </summary>
/// <param name=""").Append(keyMember.ArgumentName).Append(@""">Value to covert.</param>
/// <returns>An instance of ").AppendTypeForXmlComment(_state).Append(@".</returns>");
/// <returns>An instance of ").AppendTypeForXmlComment(_state).Append(".</returns>");

if (bothAreReferenceTypes && !emptyStringYieldsNull)
{
Expand All @@ -285,7 +285,7 @@ private void GenerateExplicitConversion(bool emptyStringYieldsNull)
}

_sb.Append(@"
public static explicit operator ").AppendTypeFullyQualified(_state).Append(nullableQuestionMark).Append("(").AppendTypeFullyQualified(keyMember).Append(nullableQuestionMark).Append(" ").AppendEscaped(keyMember.ArgumentName).Append(@")
public static ").AppendConversionOperator(_state.Settings.ConversionFromKeyMemberType).Append(" operator ").AppendTypeFullyQualified(_state).Append(nullableQuestionMark).Append("(").AppendTypeFullyQualified(keyMember).Append(nullableQuestionMark).Append(" ").AppendEscaped(keyMember.ArgumentName).Append(@")
{");

if (bothAreReferenceTypes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ namespace Thinktecture.CodeAnalysis.ValueObjects;
public bool NullInFactoryMethodsYieldsNull => _allSettings.NullInFactoryMethodsYieldsNull;
public string DefaultInstancePropertyName => _allSettings.DefaultInstancePropertyName;
public bool AllowDefaultStructs => _allSettings.AllowDefaultStructs;
public ConversionOperatorsGeneration ConversionToKeyMemberType => _allSettings.ConversionToKeyMemberType;
public ConversionOperatorsGeneration UnsafeConversionToKeyMemberType => _allSettings.UnsafeConversionToKeyMemberType;
public ConversionOperatorsGeneration ConversionFromKeyMemberType => _allSettings.ConversionFromKeyMemberType;
public bool HasStructLayoutAttribute => _attributeInfo.HasStructLayoutAttribute;
public string? KeyMemberEqualityComparerAccessor => _attributeInfo.KeyMemberEqualityComparerAccessor;
public ImmutableArray<DesiredFactory> DesiredFactories => _attributeInfo.DesiredFactories;
Expand All @@ -39,6 +42,9 @@ public bool Equals(ValueObjectSettings other)
&& NullInFactoryMethodsYieldsNull == other.NullInFactoryMethodsYieldsNull
&& DefaultInstancePropertyName == other.DefaultInstancePropertyName
&& AllowDefaultStructs == other.AllowDefaultStructs
&& ConversionToKeyMemberType == other.ConversionToKeyMemberType
&& UnsafeConversionToKeyMemberType == other.UnsafeConversionToKeyMemberType
&& ConversionFromKeyMemberType == other.ConversionFromKeyMemberType
&& HasStructLayoutAttribute == other.HasStructLayoutAttribute
&& KeyMemberEqualityComparerAccessor == other.KeyMemberEqualityComparerAccessor
&& DesiredFactories.SequenceEqual(other.DesiredFactories);
Expand All @@ -63,6 +69,9 @@ public override int GetHashCode()
hashCode = (hashCode * 397) ^ NullInFactoryMethodsYieldsNull.GetHashCode();
hashCode = (hashCode * 397) ^ DefaultInstancePropertyName.GetHashCode();
hashCode = (hashCode * 397) ^ AllowDefaultStructs.GetHashCode();
hashCode = (hashCode * 397) ^ (int)ConversionToKeyMemberType;
hashCode = (hashCode * 397) ^ (int)UnsafeConversionToKeyMemberType;
hashCode = (hashCode * 397) ^ (int)ConversionFromKeyMemberType;
hashCode = (hashCode * 397) ^ HasStructLayoutAttribute.GetHashCode();
hashCode = (hashCode * 397) ^ (KeyMemberEqualityComparerAccessor?.GetHashCode() ?? 0);
hashCode = (hashCode * 397) ^ DesiredFactories.ComputeHashCode();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ public static SwitchMapMethodsGeneration FindMapMethods(this AttributeData attri
return GetConversionOperatorsGeneration(attributeData, "ConversionToKeyMemberType");
}

public static ConversionOperatorsGeneration? FindUnsafeConversionToKeyMemberType(this AttributeData attributeData)
{
return GetConversionOperatorsGeneration(attributeData, "UnsafeConversionToKeyMemberType");
}

public static ConversionOperatorsGeneration? FindConversionFromKeyMemberType(this AttributeData attributeData)
{
return GetConversionOperatorsGeneration(attributeData, "ConversionFromKeyMemberType");
Expand Down
22 changes: 22 additions & 0 deletions src/Thinktecture.Runtime.Extensions/ValueObjectAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,34 @@ public override OperatorsGeneration EqualityComparisonOperators
/// </summary>
public bool SkipIFormattable { get; set; }

/// <summary>
/// Indication whether and how the generator should generate the conversion operators from value object to <see cref="KeyMemberType"/>.
/// Default is <see cref="ConversionOperatorsGeneration.Implicit"/>.
/// </summary>
public ConversionOperatorsGeneration ConversionToKeyMemberType { get; set; }

/// <summary>
/// Indication whether and how the generator should generate "unsafe" conversion operators from value object to <see cref="KeyMemberType"/>.
/// Conversion is considered unsafe if it can throw an <see cref="Exception"/>.
/// Default is <see cref="ConversionOperatorsGeneration.Explicit"/>.
/// </summary>
public ConversionOperatorsGeneration UnsafeConversionToKeyMemberType { get; set; }

/// <summary>
/// Indication whether and how the generator should generate the conversion operators from <see cref="KeyMemberType"/> to enum type.
/// Default is <see cref="ConversionOperatorsGeneration.Explicit"/>.
/// </summary>
public ConversionOperatorsGeneration ConversionFromKeyMemberType { get; set; }

/// <summary>
/// Initializes new instance of <see cref="ValueObjectAttribute{TKey}"/>.
/// </summary>
public ValueObjectAttribute()
{
KeyMemberType = typeof(TKey);
KeyMemberAccessModifier = ValueObjectAccessModifier.Private;
ConversionToKeyMemberType = ConversionOperatorsGeneration.Implicit;
UnsafeConversionToKeyMemberType = ConversionOperatorsGeneration.Explicit;
ConversionFromKeyMemberType = ConversionOperatorsGeneration.Explicit;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,36 @@ public async Task Should_change_conversion_from_key(
var source = $$"""
using System;
namespace Thinktecture.Tests
{
[SmartEnum<string>(ConversionFromKeyMemberType = ConversionOperatorsGeneration.{{operatorsGeneration}})]
public abstract partial class TestEnum
{
public static readonly TestEnum Item1 = null!;
}
}
""";
var outputs = GetGeneratedOutputs<SmartEnumSourceGenerator>(source, typeof(IEnum<>).Assembly);

await VerifyAsync(operatorsGeneration.ToString(),
outputs,
"Thinktecture.Tests.TestEnum.g.cs",
"Thinktecture.Tests.TestEnum.Comparable.g.cs",
"Thinktecture.Tests.TestEnum.Parsable.g.cs",
"Thinktecture.Tests.TestEnum.ComparisonOperators.g.cs",
"Thinktecture.Tests.TestEnum.EqualityComparisonOperators.g.cs");
}

[Theory]
[InlineData(ConversionOperatorsGeneration.None)]
[InlineData(ConversionOperatorsGeneration.Implicit)]
[InlineData(ConversionOperatorsGeneration.Explicit)]
public async Task Should_change_conversion_to_key(
ConversionOperatorsGeneration operatorsGeneration)
{
var source = $$"""
using System;
namespace Thinktecture.Tests
{
[SmartEnum<string>(ConversionToKeyMemberType = ConversionOperatorsGeneration.{{operatorsGeneration}})]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,12 @@ public static bool TryGet(global::System.ReadOnlySpan<char> @key, [global::Syste
#endif

/// <summary>
/// Explicit conversion to the type <see cref="string"/>.
/// Implicit conversion to the type <see cref="string"/>.
/// </summary>
/// <param name="item">Item to covert.</param>
/// <returns>The <see cref="TestEnum.Key"/> of provided <paramref name="item"/> or <c>default</c> if <paramref name="item"/> is <c>null</c>.</returns>
[return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull("item")]
public static explicit operator string?(global::Thinktecture.Tests.TestEnum? item)
public static implicit operator string?(global::Thinktecture.Tests.TestEnum? item)
{
return item is null ? default : item.Key;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,12 +195,12 @@ public static bool TryGet(global::System.ReadOnlySpan<char> @key, [global::Syste
}

/// <summary>
/// Explicit conversion from the type <see cref="string"/>.
/// Implicit conversion from the type <see cref="string"/>.
/// </summary>
/// <param name="key">Value to covert.</param>
/// <returns>An instance of <see cref="TestEnum"/> if the <paramref name="key"/> is a known item or implements <see cref="Thinktecture.IValidatableEnum"/>.</returns>
[return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull("key")]
public static explicit operator global::Thinktecture.Tests.TestEnum?(string? @key)
public static implicit operator global::Thinktecture.Tests.TestEnum?(string? @key)
{
return global::Thinktecture.Tests.TestEnum.Get(@key);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,14 @@ public static bool TryGet(global::System.ReadOnlySpan<char> @key, [global::Syste
#endif

/// <summary>
/// Explicit conversion from the type <see cref="string"/>.
/// Implicit conversion to the type <see cref="string"/>.
/// </summary>
/// <param name="key">Value to covert.</param>
/// <returns>An instance of <see cref="TestEnum"/> if the <paramref name="key"/> is a known item or implements <see cref="Thinktecture.IValidatableEnum"/>.</returns>
[return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull("key")]
public static explicit operator global::Thinktecture.Tests.TestEnum?(string? @key)
/// <param name="item">Item to covert.</param>
/// <returns>The <see cref="TestEnum.Key"/> of provided <paramref name="item"/> or <c>default</c> if <paramref name="item"/> is <c>null</c>.</returns>
[return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull("item")]
public static implicit operator string?(global::Thinktecture.Tests.TestEnum? item)
{
return global::Thinktecture.Tests.TestEnum.Get(@key);
return item is null ? default : item.Key;
}

/// <inheritdoc />
Expand Down
Loading

0 comments on commit 7deffed

Please sign in to comment.