Skip to content

Commit

Permalink
feat: explicit enum mappings for byValue enum mappings (#489)
Browse files Browse the repository at this point in the history
  • Loading branch information
latonz committed Jun 12, 2023
1 parent 38c71e7 commit 029dae2
Show file tree
Hide file tree
Showing 20 changed files with 494 additions and 236 deletions.
10 changes: 3 additions & 7 deletions docs/docs/02-configuration/04-enum.mdx
Expand Up @@ -52,14 +52,10 @@ public partial class CarMapper
</TabItem>
</Tabs>

## Manually Mapped Enum Values
## Manually mapped enum values

When `MapEnumAttribute.ByName` is used, explicit enum values may be overridden using
the `MapEnumValueAttibute` to the mapping method.

Applied to an explicit enum value pair mapped by this method.
Attribute is only valid on mapping method with enums as parameters and
configured with the `ByName` strategy.
To explicitly map enum values the `MapEnumValueAttibute` can be used.
Attribute is only valid on mapping methods with enums as parameters.

```csharp
[Mapper]
Expand Down
52 changes: 36 additions & 16 deletions src/Riok.Mapperly/Descriptors/MappingBuilders/EnumMappingBuilder.cs
Expand Up @@ -28,20 +28,18 @@ public static class EnumMappingBuilder
: null;
}

// since enums are immutable they can be directly assigned if they are of the same type
if (SymbolEqualityComparer.IncludeNullability.Equals(ctx.Source, ctx.Target))
return new DirectAssignmentMapping(ctx.Source);

if (!ctx.IsConversionEnabled(MappingConversionType.EnumToEnum))
return null;

// map enums by strategy
var config = ctx.GetConfigurationOrDefault<MapEnumAttribute>();
var explicitMappings = BuildExplicitValueMapping(ctx);
return config.Strategy switch
{
EnumMappingStrategy.ByName when ctx.IsExpression => BuildCastMappingAndDiagnostic(ctx),
EnumMappingStrategy.ByName => BuildNameMapping(ctx, config.IgnoreCase),
_ => BuildEnumToEnumCastMapping(ctx),
EnumMappingStrategy.ByValue when ctx.IsExpression && explicitMappings.Count > 0 => BuildCastMappingAndDiagnostic(ctx),
EnumMappingStrategy.ByName => BuildNameMapping(ctx, explicitMappings, config.IgnoreCase),
_ => BuildEnumToEnumCastMapping(ctx, explicitMappings),
};
}

Expand All @@ -52,13 +50,26 @@ private static TypeMapping BuildCastMappingAndDiagnostic(MappingBuilderContext c
ctx.Source.ToDisplayString(),
ctx.Target.ToDisplayString()
);
return BuildEnumToEnumCastMapping(ctx);
return BuildEnumToEnumCastMapping(ctx, new Dictionary<IFieldSymbol, IFieldSymbol>(SymbolEqualityComparer.Default));
}

private static TypeMapping BuildEnumToEnumCastMapping(MappingBuilderContext ctx)
private static TypeMapping BuildEnumToEnumCastMapping(
MappingBuilderContext ctx,
IReadOnlyDictionary<IFieldSymbol, IFieldSymbol> explicitMappings
)
{
var sourceValues = ctx.Source.GetMembers().OfType<IFieldSymbol>().ToDictionary(field => field.Name, field => field.ConstantValue);
var targetValues = ctx.Target.GetMembers().OfType<IFieldSymbol>().ToDictionary(field => field.Name, field => field.ConstantValue);
var explicitMappingSourceNames = explicitMappings.Keys.Select(x => x.Name).ToHashSet();
var explicitMappingTargetNames = explicitMappings.Values.Select(x => x.Name).ToHashSet();
var sourceValues = ctx.Source
.GetMembers()
.OfType<IFieldSymbol>()
.Where(x => !explicitMappingSourceNames.Contains(x.Name))
.ToDictionary(field => field.Name, field => field.ConstantValue);
var targetValues = ctx.Target
.GetMembers()
.OfType<IFieldSymbol>()
.Where(x => !explicitMappingTargetNames.Contains(x.Name))
.ToDictionary(field => field.Name, field => field.ConstantValue);

var missingTargetValues = targetValues.Where(field => !sourceValues.ContainsValue(field.Value));
foreach (var member in missingTargetValues)
Expand All @@ -72,12 +83,22 @@ private static TypeMapping BuildEnumToEnumCastMapping(MappingBuilderContext ctx)
ctx.ReportDiagnostic(DiagnosticDescriptors.SourceEnumValueNotMapped, member.Key, member.Value!, ctx.Source, ctx.Target);
}

return new CastMapping(ctx.Source, ctx.Target);
var fallbackMapping = new CastMapping(ctx.Source, ctx.Target);
if (explicitMappings.Count == 0)
return fallbackMapping;

var explicitNameMappings = explicitMappings
.Where(x => !x.Value.ConstantValue!.Equals(x.Key.ConstantValue))
.ToDictionary(x => x.Key.Name, x => x.Value.Name);
return new EnumNameMapping(ctx.Source, ctx.Target, explicitNameMappings, fallbackMapping);
}

private static TypeMapping BuildNameMapping(MappingBuilderContext ctx, bool ignoreCase)
private static EnumNameMapping BuildNameMapping(
MappingBuilderContext ctx,
IReadOnlyDictionary<IFieldSymbol, IFieldSymbol> explicitMappings,
bool ignoreCase
)
{
var targetFieldsByExplicitValue = BuildExplicitValueMapping(ctx);
var targetFieldsByName = ctx.Target.GetMembers().OfType<IFieldSymbol>().ToDictionary(x => x.Name);
var sourceFieldsByName = ctx.Source.GetMembers().OfType<IFieldSymbol>().ToDictionary(x => x.Name);

Expand All @@ -88,14 +109,13 @@ private static TypeMapping BuildNameMapping(MappingBuilderContext ctx, bool igno
.DistinctBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
.ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase);
getTargetField = source =>
targetFieldsByExplicitValue.GetValueOrDefault(source)
explicitMappings.GetValueOrDefault(source)
?? targetFieldsByName.GetValueOrDefault(source.Name)
?? targetFieldsByNameIgnoreCase.GetValueOrDefault(source.Name);
}
else
{
getTargetField = source =>
targetFieldsByExplicitValue.GetValueOrDefault(source) ?? targetFieldsByName.GetValueOrDefault(source.Name);
getTargetField = source => explicitMappings.GetValueOrDefault(source) ?? targetFieldsByName.GetValueOrDefault(source.Name);
}

var enumMemberMappings = ctx.Source
Expand Down
18 changes: 13 additions & 5 deletions src/Riok.Mapperly/Descriptors/Mappings/EnumNameMapping.cs
Expand Up @@ -13,27 +13,35 @@ namespace Riok.Mapperly.Descriptors.Mappings;
public class EnumNameMapping : MethodMapping
{
private readonly IReadOnlyDictionary<string, string> _enumMemberMappings;

public EnumNameMapping(ITypeSymbol source, ITypeSymbol target, IReadOnlyDictionary<string, string> enumMemberMappings)
private readonly ITypeMapping? _fallbackMapping;

public EnumNameMapping(
ITypeSymbol source,
ITypeSymbol target,
IReadOnlyDictionary<string, string> enumMemberMappings,
ITypeMapping? fallbackMapping = null
)
: base(source, target)
{
_enumMemberMappings = enumMemberMappings;
_fallbackMapping = fallbackMapping;
}

public override IEnumerable<StatementSyntax> BuildBody(TypeMappingBuildContext ctx)
{
// fallback switch arm: _ => throw new ArgumentOutOfRangeException(nameof(source), source, message);
// fallback switch arm with _fallbackMapping: _ => Map(src);
// fallback switch arm without _fallbackMapping: _ => throw new ArgumentOutOfRangeException(nameof(source), source, message);
var fallbackArm = SwitchExpressionArm(
DiscardPattern(),
ThrowArgumentOutOfRangeException(ctx.Source, $"The value of enum {SourceType.Name} is not supported")
_fallbackMapping?.Build(ctx)
?? ThrowArgumentOutOfRangeException(ctx.Source, $"The value of enum {SourceType.Name} is not supported")
);

// switch for each name to the enum value
// eg: Enum1.Value1 => Enum2.Value1,
var arms = _enumMemberMappings.Select(BuildArm).Append(fallbackArm);

var switchExpr = SwitchExpression(ctx.Source).WithArms(CommaSeparatedList(arms, true));

yield return ReturnStatement(switchExpr);
}

Expand Down
4 changes: 2 additions & 2 deletions src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
Expand Up @@ -278,8 +278,8 @@ internal static class DiagnosticDescriptors

public static readonly DiagnosticDescriptor EnumMappingStrategyByNameNotSupportedInProjectionMappings = new DiagnosticDescriptor(
"RMG032",
"The enum mapping strategy ByName cannot be used in projection mappings",
"The enum mapping strategy ByName cannot be used in projection mappings to map from {0} to {1}",
"The enum mapping strategy ByName and explicit enum mappings cannot be used in projection mappings",
"The enum mapping strategy ByName and explicit enum mappings cannot be used in projection mappings to map from {0} to {1}",
DiagnosticCategories.Mapper,
DiagnosticSeverity.Warning,
true
Expand Down
6 changes: 0 additions & 6 deletions src/Riok.Mapperly/Helpers/DictionaryExtensions.cs
Expand Up @@ -18,10 +18,4 @@ public static class DictionaryExtensions
dict.Remove(key);
}
}

public static TValue? GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key)
{
dict.TryGetValue(key, out var value);
return value;
}
}
10 changes: 10 additions & 0 deletions src/Riok.Mapperly/Helpers/ReadOnlyDictionaryExtensions.cs
@@ -0,0 +1,10 @@
namespace Riok.Mapperly.Helpers;

internal static class ReadOnlyDictionaryExtensions
{
public static TValue? GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dict, TKey key)
{
dict.TryGetValue(key, out var value);
return value;
}
}
2 changes: 2 additions & 0 deletions src/Riok.Mapperly/Helpers/SymbolExtensions.cs
Expand Up @@ -64,6 +64,8 @@ internal static bool TryGetEnumUnderlyingType(this ITypeSymbol t, [NotNullWhen(t
internal static IEnumerable<IPropertySymbol> GetAllProperties(this ITypeSymbol symbol, string name) =>
symbol.GetAllMembers(name).OfType<IPropertySymbol>();

internal static IEnumerable<IFieldSymbol> GetFields(this ITypeSymbol symbol) => symbol.GetMembers().OfType<IFieldSymbol>();

internal static IEnumerable<ISymbol> GetAllMembers(this ITypeSymbol symbol)
{
var members = symbol.GetMembers();
Expand Down
@@ -0,0 +1,10 @@
namespace Riok.Mapperly.IntegrationTests.Dto
{
public enum TestEnumDtoAdditionalValue
{
Value10 = 10,
Value20 = 20,
Value30 = 30,
Value40 = 40,
}
}

This file was deleted.

8 changes: 6 additions & 2 deletions test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs
Expand Up @@ -63,8 +63,12 @@ public TestObjectDto MapToDto(TestObject src)
public partial TestEnumDtoByName MapToEnumDtoByName(TestEnum v);

[MapEnum(EnumMappingStrategy.ByName)]
[MapEnumValue(TestEnumDtoExplicitLarger.Value40, TestEnum.Value30)]
public partial TestEnum MapToEnumDtoByNameWithExplicit(TestEnumDtoExplicitLarger v);
[MapEnumValue(TestEnumDtoAdditionalValue.Value40, TestEnum.Value30)]
public partial TestEnum MapToEnumByNameWithExplicit(TestEnumDtoAdditionalValue v);

[MapEnum(EnumMappingStrategy.ByValue)]
[MapEnumValue(TestEnumDtoAdditionalValue.Value40, TestEnum.Value30)]
public partial TestEnum MapToEnumByValueWithExplicit(TestEnumDtoAdditionalValue v);

[MapperIgnoreTarget(nameof(TestObjectDto.IgnoredIntValue))]
[MapperIgnoreSource(nameof(TestObject.IgnoredStringValue))]
Expand Down
Expand Up @@ -185,14 +185,24 @@ public partial class TestMapper
};
}

public partial global::Riok.Mapperly.IntegrationTests.Models.TestEnum MapToEnumDtoByNameWithExplicit(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoExplicitLarger v)
public partial global::Riok.Mapperly.IntegrationTests.Models.TestEnum MapToEnumByNameWithExplicit(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue v)
{
return v switch
{
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoExplicitLarger.Value10 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10,
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoExplicitLarger.Value20 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20,
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoExplicitLarger.Value30 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30,
_ => throw new System.ArgumentOutOfRangeException(nameof(v), v, "The value of enum TestEnumDtoExplicitLarger is not supported"),
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value10 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10,
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value20 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20,
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value30 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30,
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value40 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30,
_ => throw new System.ArgumentOutOfRangeException(nameof(v), v, "The value of enum TestEnumDtoAdditionalValue is not supported"),
};
}

public partial global::Riok.Mapperly.IntegrationTests.Models.TestEnum MapToEnumByValueWithExplicit(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue v)
{
return v switch
{
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value40 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30,
_ => (global::Riok.Mapperly.IntegrationTests.Models.TestEnum)v,
};
}

Expand Down Expand Up @@ -349,4 +359,4 @@ private string MapToString1(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumD
return target;
}
}
}
}
Expand Up @@ -179,14 +179,24 @@ public partial class TestMapper
};
}

public partial global::Riok.Mapperly.IntegrationTests.Models.TestEnum MapToEnumDtoByNameWithExplicit(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoExplicitLarger v)
public partial global::Riok.Mapperly.IntegrationTests.Models.TestEnum MapToEnumByNameWithExplicit(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue v)
{
return v switch
{
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoExplicitLarger.Value10 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10,
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoExplicitLarger.Value20 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20,
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoExplicitLarger.Value30 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30,
_ => throw new System.ArgumentOutOfRangeException(nameof(v), v, "The value of enum TestEnumDtoExplicitLarger is not supported"),
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value10 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10,
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value20 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20,
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value30 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30,
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value40 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30,
_ => throw new System.ArgumentOutOfRangeException(nameof(v), v, "The value of enum TestEnumDtoAdditionalValue is not supported"),
};
}

public partial global::Riok.Mapperly.IntegrationTests.Models.TestEnum MapToEnumByValueWithExplicit(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue v)
{
return v switch
{
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value40 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30,
_ => (global::Riok.Mapperly.IntegrationTests.Models.TestEnum)v,
};
}

Expand Down Expand Up @@ -343,4 +353,4 @@ private string MapToString1(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumD
return target;
}
}
}
}
Expand Up @@ -185,14 +185,24 @@ public partial class TestMapper
};
}

public partial global::Riok.Mapperly.IntegrationTests.Models.TestEnum MapToEnumDtoByNameWithExplicit(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoExplicitLarger v)
public partial global::Riok.Mapperly.IntegrationTests.Models.TestEnum MapToEnumByNameWithExplicit(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue v)
{
return v switch
{
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoExplicitLarger.Value10 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10,
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoExplicitLarger.Value20 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20,
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoExplicitLarger.Value30 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30,
_ => throw new System.ArgumentOutOfRangeException(nameof(v), v, "The value of enum TestEnumDtoExplicitLarger is not supported"),
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value10 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10,
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value20 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20,
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value30 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30,
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value40 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30,
_ => throw new System.ArgumentOutOfRangeException(nameof(v), v, "The value of enum TestEnumDtoAdditionalValue is not supported"),
};
}

public partial global::Riok.Mapperly.IntegrationTests.Models.TestEnum MapToEnumByValueWithExplicit(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue v)
{
return v switch
{
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoAdditionalValue.Value40 => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30,
_ => (global::Riok.Mapperly.IntegrationTests.Models.TestEnum)v,
};
}

Expand Down
2 changes: 1 addition & 1 deletion test/Riok.Mapperly.Tests/DiagnosticMatcher.cs
Expand Up @@ -4,7 +4,7 @@ namespace Riok.Mapperly.Tests;

public record DiagnosticMatcher(DiagnosticDescriptor Descriptor, string? Message = null)
{
public bool Matches(Diagnostic diagnostic) => Descriptor.Equals(diagnostic.Descriptor);
public bool MatchesDescriptor(Diagnostic diagnostic) => Descriptor.Equals(diagnostic.Descriptor);

public void EnsureMatches(Diagnostic diagnostic)
{
Expand Down
21 changes: 0 additions & 21 deletions test/Riok.Mapperly.Tests/Helpers/DictionaryExtensionsTest.cs
Expand Up @@ -34,25 +34,4 @@ public void RemoveRangeShouldRemoveEntries()
d.RemoveRange(new[] { "a", "c" });
d.Keys.Should().BeEquivalentTo("b");
}

[Fact]
public void GetValueOrDefaultShouldReturnValueIfFound()
{
var d = new Dictionary<string, int> { ["a"] = 10, ["b"] = 20, };
DictionaryExtensions.GetValueOrDefault(d, "a").Should().Be(10);
}

[Fact]
public void GetValueOrDefaultShouldReturnDefaultForPrimitiveIfNotFound()
{
var d = new Dictionary<string, int> { ["a"] = 10, ["b"] = 20, };
DictionaryExtensions.GetValueOrDefault(d, "c").Should().Be(0);
}

[Fact]
public void GetValueOrDefaultShouldReturnDefaultForReferenceTypeIfNotFound()
{
var d = new Dictionary<string, Version> { ["a"] = new(), ["b"] = new(), };
DictionaryExtensions.GetValueOrDefault(d, "c").Should().BeNull();
}
}

0 comments on commit 029dae2

Please sign in to comment.