Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added MapEnumValueAttribute and support for explicit enum value mapping #468

Merged
merged 23 commits into from
Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0895532
chore: added visual studio code coverage results files to gitignore
peteraritchie May 29, 2023
5f37f75
chore: Changed GetReflectionType to return null if reflection type ca…
peteraritchie May 29, 2023
542978e
chore: Added MapEnumValueAttribute and MapEnumValue
peteraritchie May 29, 2023
3326e65
feat: Added support for MapEnumValueAttribute and tests
peteraritchie May 29, 2023
f13482d
docs: added docs for MapEnumValueAttribute
peteraritchie May 29, 2023
c574e47
chore: revert line-endings to LF in EnumMappingBuilder.cs
peteraritchie May 30, 2023
0d47560
chore: added new section in docs for MapEnumValueAttribute
peteraritchie May 30, 2023
7a58fe0
chore: clarified param XML docs for MapEnumValueAttribute constructor
peteraritchie May 30, 2023
5e0758b
chore: moved new PublicAPI entires from unshipped to shipped.
peteraritchie May 30, 2023
4f50a74
chore: added diagnostic check to MapEnumValue test
peteraritchie May 30, 2023
9dfb5ad
chore: added diagnostic and test for multiple explicit mappings of sa…
peteraritchie May 30, 2023
5bf352a
chore: updated TestMapper with accurate explicit enum value test, upd…
peteraritchie May 30, 2023
8672241
chore: changed MapEnumValue example to use car features instead of makes
peteraritchie May 30, 2023
e7b09b8
docs: add csharpier linting docs (#471)
latonz May 30, 2023
aacf4d5
chore(deps): bump actions/setup-dotnet from 3.0.3 to 3.2.0 (#469)
dependabot[bot] May 30, 2023
d32f1e6
chore(deps): bump Verify.XUnit from 20.0.0 to 20.3.0 (#470)
dependabot[bot] May 30, 2023
be71840
chore: Added rule RMG039 to analyzer release info
peteraritchie May 31, 2023
1086e80
docs: document common contributing tasks (#472)
latonz May 31, 2023
8c654da
Merge branch 'main' into main
peteraritchie May 31, 2023
8e30b59
chore: corrected MapToEnumDtoByNameWithExplicit to use enum types tha…
peteraritchie May 31, 2023
805595e
chore: added diagnostics RMG040-1 to represent enum values that do no…
peteraritchie May 31, 2023
fc70417
chore: added snapshots for NET_48 and Roslyn_4_4_OR_LOWER
peteraritchie Jun 4, 2023
2e4fc79
Merge branch 'main' into main
peteraritchie Jun 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ obj/

# dot user settings
*.DotSettings.user

# Visual Studio code coverage results
*.coverage
*.coveragexml
21 changes: 21 additions & 0 deletions docs/docs/02-configuration/04-enum.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,27 @@ public partial class CarMapper
</TabItem>
</Tabs>

## 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.

```csharp
[Mapper]
public partial class CarMapper
{
[MapEnum(EnumMappingStrategy.ByName, IgnoreCase = true)]
// highlight-start
[MapEnumValue(CarFeature.AWD, CarFeatureDto.AllWheelDrive)]
// highlight-end
public partial CarFeatureDto MapFeature(CarFeature feature);
}
```

### Strict enum mappings

To enforce strict enum mappings
Expand Down
29 changes: 29 additions & 0 deletions src/Riok.Mapperly.Abstractions/MapEnumValueAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace Riok.Mapperly.Abstractions;

/// <summary>
/// Customizes how enum values are mapped
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class MapEnumValueAttribute : Attribute
{
/// <summary>
/// Customizes how enum values are mapped
/// </summary>
/// <param name="source">The enum value to map from</param>
/// <param name="target">The enum value to map to</param>
public MapEnumValueAttribute(object source, object target)
{
Source = (Enum)source;
Target = (Enum)target;
}

/// <summary>
/// What to map to
/// </summary>
public Enum Target { get; }

/// <summary>
/// What to map from
/// </summary>
public Enum Source { get; }
}
4 changes: 4 additions & 0 deletions src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Riok.Mapperly.Abstractions.MapEnumAttribute.IgnoreCase.get -> bool
Riok.Mapperly.Abstractions.MapEnumAttribute.IgnoreCase.set -> void
Riok.Mapperly.Abstractions.MapEnumAttribute.MapEnumAttribute(Riok.Mapperly.Abstractions.EnumMappingStrategy strategy) -> void
Riok.Mapperly.Abstractions.MapEnumAttribute.Strategy.get -> Riok.Mapperly.Abstractions.EnumMappingStrategy
Riok.Mapperly.Abstractions.MapEnumValueAttribute
Riok.Mapperly.Abstractions.MapEnumValueAttribute.MapEnumValueAttribute(object! source, object! target) -> void
Riok.Mapperly.Abstractions.MapEnumValueAttribute.Source.get -> System.Enum!
Riok.Mapperly.Abstractions.MapEnumValueAttribute.Target.get -> System.Enum!
Riok.Mapperly.Abstractions.MapperAttribute
Riok.Mapperly.Abstractions.MapperAttribute.EnabledConversions.get -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MapperAttribute.EnabledConversions.set -> void
Expand Down
5 changes: 4 additions & 1 deletion src/Riok.Mapperly/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,7 @@ RMG034 | Mapper | Error | Derived source type is specified multiple times,
RMG035 | Mapper | Error | Derived source type is not assignable to parameter type
RMG036 | Mapper | Error | Derived target type is not assignable to return type
RMG037 | Mapper | Info | An enum member could not be found on the source enum
RMG038 | Mapper | Info | An enum member could not be found on the target enum
RMG038 | Mapper | Info | An enum member could not be found on the target enum
RMG039 | Mapper | Error | Enum source value is specified multiple times, a source enum value may only be specified once
RMG040 | Mapper | Error | A target enum member value does not match the target enum type
RMG041 | Mapper | Error | A source enum member value does not match the source enum type
7 changes: 5 additions & 2 deletions src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,20 @@ arg.Type as IArrayTypeSymbol
private static object? GetEnumValue(TypedConstant arg)
{
var enumType = GetReflectionType(arg.Type ?? throw new InvalidOperationException("Type is null"));
// if we can't get the enum type then it's not available to reflection (only accessible by Roslyn) so return the TypedConstant
if (enumType == null)
return arg;
return arg.Value == null ? null : Enum.ToObject(enumType, arg.Value);
}

private static Type GetReflectionType(ITypeSymbol type)
private static Type? GetReflectionType(ITypeSymbol type)
{
// other special types not yet supported since they are not used yet.
if (type.SpecialType == SpecialType.System_String)
return typeof(string);

var assemblyName = type.ContainingAssembly.Name;
var qualifiedTypeName = Assembly.CreateQualifiedName(assemblyName, type.ToDisplayString());
return Type.GetType(qualifiedTypeName) ?? throw new InvalidOperationException($"Type {qualifiedTypeName} not found");
return Type.GetType(qualifiedTypeName);
}
}
12 changes: 12 additions & 0 deletions src/Riok.Mapperly/Configuration/MapEnumValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;

namespace Riok.Mapperly.Configuration;

/// <summary>
/// Roslyn representation of <see cref="MapEnumValueAttribute"/>
/// Keep in sync with <see cref="MapEnumValueAttribute"/>
/// </summary>
/// <param name="Source">The source constant of the enum value mapping.</param>
/// <param name="Target">The target constant of the enum value mapping.</param>
public record MapEnumValue(TypedConstant Source, TypedConstant Target);
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Configuration;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;
Expand Down Expand Up @@ -76,6 +77,7 @@ private static TypeMapping BuildEnumToEnumCastMapping(MappingBuilderContext ctx)

private static TypeMapping BuildNameMapping(MappingBuilderContext ctx, 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 @@ -86,11 +88,14 @@ 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 =>
targetFieldsByName.GetValueOrDefault(source.Name) ?? targetFieldsByNameIgnoreCase.GetValueOrDefault(source.Name);
targetFieldsByExplicitValue.GetValueOrDefault(source)
?? targetFieldsByName.GetValueOrDefault(source.Name)
?? targetFieldsByNameIgnoreCase.GetValueOrDefault(source.Name);
}
else
{
getTargetField = source => targetFieldsByName.GetValueOrDefault(source.Name);
getTargetField = source =>
targetFieldsByExplicitValue.GetValueOrDefault(source) ?? targetFieldsByName.GetValueOrDefault(source.Name);
}

var enumMemberMappings = ctx.Source
Expand Down Expand Up @@ -131,4 +136,51 @@ private static TypeMapping BuildNameMapping(MappingBuilderContext ctx, bool igno

return new EnumNameMapping(ctx.Source, ctx.Target, enumMemberMappings);
}

private static Dictionary<IFieldSymbol, IFieldSymbol> BuildExplicitValueMapping(MappingBuilderContext ctx)
{
var values = ctx.ListConfiguration<MapEnumValueAttribute, MapEnumValue>();
var targetFieldsByExplicitValue = new Dictionary<IFieldSymbol, IFieldSymbol>(SymbolEqualityComparer.Default);
foreach (var (sourceConstant, targetConstant) in values)
{
var source = sourceConstant.Type!.GetMembers().OfType<IFieldSymbol>().First(e => sourceConstant.Value!.Equals(e.ConstantValue));
peteraritchie marked this conversation as resolved.
Show resolved Hide resolved
var target = targetConstant.Type!.GetMembers().OfType<IFieldSymbol>().First(e => targetConstant.Value!.Equals(e.ConstantValue));
peteraritchie marked this conversation as resolved.
Show resolved Hide resolved
if (SymbolEqualityComparer.Default.Equals(sourceConstant.Type, ctx.Source))
{
if (SymbolEqualityComparer.Default.Equals(targetConstant.Type, ctx.Target))
{
if (targetFieldsByExplicitValue.ContainsKey(source))
{
ctx.ReportDiagnostic(DiagnosticDescriptors.EnumSourceValueDuplicated, source, ctx.Source, ctx.Target);
}
else
{
targetFieldsByExplicitValue.Add(source, target);
}
}
else
{
ctx.ReportDiagnostic(
DiagnosticDescriptors.TargetEnumValueDoesNotMatchTargetEnumType,
target,
targetConstant.Value ?? 0,
target.Type,
ctx.Target
);
}
}
else
{
ctx.ReportDiagnostic(
DiagnosticDescriptors.SourceEnumValueDoesNotMatchSourceEnumType,
source,
sourceConstant.Value ?? 0,
source.Type,
ctx.Source
);
}
}

return targetFieldsByExplicitValue;
}
}
27 changes: 27 additions & 0 deletions src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -338,4 +338,31 @@ internal static class DiagnosticDescriptors
DiagnosticSeverity.Info,
true
);

public static readonly DiagnosticDescriptor EnumSourceValueDuplicated = new DiagnosticDescriptor(
peteraritchie marked this conversation as resolved.
Show resolved Hide resolved
"RMG039",
"Enum source value is specified multiple times, a source enum value may only be specified once",
"Enum source value {0} is specified multiple times, a source enum value may only be specified once",
DiagnosticCategories.Mapper,
DiagnosticSeverity.Error,
true
);

public static readonly DiagnosticDescriptor TargetEnumValueDoesNotMatchTargetEnumType = new DiagnosticDescriptor(
"RMG040",
"A target enum member value does not match the target enum type",
"Enum member {0} ({1}) on {2} does not match type of target enum {3}",
DiagnosticCategories.Mapper,
DiagnosticSeverity.Error,
true
);

public static readonly DiagnosticDescriptor SourceEnumValueDoesNotMatchSourceEnumType = new DiagnosticDescriptor(
"RMG041",
"A source enum member value does not match the source enum type",
"Enum member {0} ({1}) on {2} does not match type of source enum {3}",
DiagnosticCategories.Mapper,
DiagnosticSeverity.Error,
true
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Riok.Mapperly.IntegrationTests.Dto
{
public enum TestEnumDtoExplicitLarger
{
Value10 = 10_000,
Value20 = 20_000,
Value30 = 30_000,
Value40 = 30_000
}
}
4 changes: 4 additions & 0 deletions test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ public TestObjectDto MapToDto(TestObject src)
[MapEnum(EnumMappingStrategy.ByName)]
public partial TestEnumDtoByName MapToEnumDtoByName(TestEnum v);

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

[MapperIgnoreTarget(nameof(TestObjectDto.IgnoredIntValue))]
[MapperIgnoreSource(nameof(TestObject.IgnoredStringValue))]
public partial void UpdateDto(TestObject source, TestObjectDto target);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,17 @@ public partial int ParseableInt(string value)
};
}

public partial global::Riok.Mapperly.IntegrationTests.Models.TestEnum MapToEnumDtoByNameWithExplicit(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoExplicitLarger 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"),
};
}

public partial void UpdateDto(global::Riok.Mapperly.IntegrationTests.Models.TestObject source, global::Riok.Mapperly.IntegrationTests.Dto.TestObjectDto target)
{
if (source.NullableFlattening != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,17 @@ public partial int ParseableInt(string value)
};
}

public partial global::Riok.Mapperly.IntegrationTests.Models.TestEnum MapToEnumDtoByNameWithExplicit(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoExplicitLarger 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"),
};
}

public partial void UpdateDto(global::Riok.Mapperly.IntegrationTests.Models.TestObject source, global::Riok.Mapperly.IntegrationTests.Dto.TestObjectDto target)
{
if (source.NullableFlattening != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,17 @@ public partial int ParseableInt(string value)
};
}

public partial global::Riok.Mapperly.IntegrationTests.Models.TestEnum MapToEnumDtoByNameWithExplicit(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoExplicitLarger 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"),
};
}

public partial void UpdateDto(global::Riok.Mapperly.IntegrationTests.Models.TestObject source, global::Riok.Mapperly.IntegrationTests.Dto.TestObjectDto target)
{
if (source.NullableFlattening != null)
Expand Down Expand Up @@ -338,4 +349,4 @@ private string MapToString1(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumD
return target;
}
}
}
}
Loading