Skip to content

Commit

Permalink
feat: pr fixes + new naming strategies
Browse files Browse the repository at this point in the history
  • Loading branch information
BeeTwin committed Sep 6, 2024
1 parent 189ba4a commit 711c763
Show file tree
Hide file tree
Showing 18 changed files with 621 additions and 81 deletions.
17 changes: 11 additions & 6 deletions docs/docs/configuration/enum.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,18 @@ Available strategies:
The `IgnoreCase` property allows to opt in for case insensitive mappings (defaults to `false`).

Enum from/to strings mappings can be customized by setting the enum naming strategy to be used.
You can specify the naming strategy using `NamingStrategy` in `MapEnumAttribute` or `MapperAttribute`.
You can specify the naming strategy using `NamingStrategy` in `MapEnumAttribute` or `EnumNamingStrategy` in `MapperAttribute` and `MapperDefaultsAttribute`.
Available naming strategies:

| Name | Description |
| -----------| ------------------------------------------------|
| PascalCase | Matches enum values using PascalCase. (default) |
| SnakeCase | Matches enum values using snake_case. |
| KebabCase | Matches enum values using kebab-case. |
| Name | Description |
| ----------------| ------------------------------------------------|
| MemberName | Matches enum values using their name. (default) |
| CamelCase | Matches enum values using camelCase. |
| PascalCase | Matches enum values using PascalCase. |
| SnakeCase | Matches enum values using snake_case. |
| UpperSnakeCase | Matches enum values using UPPER_SNAKE_CASE. |
| KebabCase | Matches enum values using kebab-case. |
| UpperKebabCase | Matches enum values using UPPER-KEBAB-CASE. |

<Tabs>
<TabItem value="global" label="Global (mapper level)" default>
Expand Down Expand Up @@ -111,6 +115,7 @@ To map to a fallback value instead of throwing when encountering an unknown valu
the `FallbackValue` property on the `MapEnum` attribute can be used.

`FallbackValue` is supported by `ByName` and `ByValueCheckDefined`.
`FallbackValue` is not affected by enum naming strategies.

```csharp
[Mapper]
Expand Down
20 changes: 20 additions & 0 deletions src/Riok.Mapperly.Abstractions/EnumNamingStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ namespace Riok.Mapperly.Abstractions;
/// </summary>
public enum EnumNamingStrategy
{
/// <summary>
/// Matches enum values using their name.
/// </summary>
MemberName,

/// <summary>
/// Matches enum values using camelCase.
/// </summary>
CamelCase,

/// <summary>
/// Matches enum values using PascalCase.
/// </summary>
Expand All @@ -15,8 +25,18 @@ public enum EnumNamingStrategy
/// </summary>
SnakeCase,

/// <summary>
/// Matches enum values using UPPER_SNAKE_CASE.
/// </summary>
UpperSnakeCase,

/// <summary>
/// Matches enum values using kebab-case.
/// </summary>
KebabCase,

/// <summary>
/// Matches enum values using UPPER-KEBAB-CASE.
/// </summary>
UpperKebabCase,
}
8 changes: 7 additions & 1 deletion src/Riok.Mapperly.Abstractions/MapperDefaultsAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,10 @@ namespace Riok.Mapperly.Abstractions;
/// </summary>
[AttributeUsage(AttributeTargets.Assembly)]
[Conditional("MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME")]
public sealed class MapperDefaultsAttribute : MapperAttribute;
public sealed class MapperDefaultsAttribute : MapperAttribute
{
/// <summary>
/// The strategy to be used to map enums from/to strings.
/// </summary>
public EnumNamingStrategy NamingStrategy { get; set; }
}
14 changes: 14 additions & 0 deletions src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,17 @@ Riok.Mapperly.Abstractions.MapPropertyAttribute.MapPropertyAttribute(string! sou
Riok.Mapperly.Abstractions.MapPropertyAttribute.MapPropertyAttribute(string![]! source, string! target) -> void
Riok.Mapperly.Abstractions.MapperAttribute.IncludedConstructors.get -> Riok.Mapperly.Abstractions.MemberVisibility
Riok.Mapperly.Abstractions.MapperAttribute.IncludedConstructors.set -> void
Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.EnumNamingStrategy.CamelCase = 1 -> Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.EnumNamingStrategy.KebabCase = 5 -> Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.EnumNamingStrategy.MemberName = 0 -> Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.EnumNamingStrategy.PascalCase = 2 -> Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.EnumNamingStrategy.SnakeCase = 3 -> Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.EnumNamingStrategy.UpperKebabCase = 6 -> Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.EnumNamingStrategy.UpperSnakeCase = 4 -> Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.MapEnumAttribute.NamingStrategy.get -> Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.MapEnumAttribute.NamingStrategy.set -> void
Riok.Mapperly.Abstractions.MapperAttribute.EnumNamingStrategy.get -> Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.MapperAttribute.EnumNamingStrategy.set -> void
Riok.Mapperly.Abstractions.MapperDefaultsAttribute.NamingStrategy.get -> Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.MapperDefaultsAttribute.NamingStrategy.set -> void
8 changes: 0 additions & 8 deletions src/Riok.Mapperly.Abstractions/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1 @@
#nullable enable
Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.EnumNamingStrategy.KebabCase = 2 -> Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.EnumNamingStrategy.PascalCase = 0 -> Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.EnumNamingStrategy.SnakeCase = 1 -> Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.MapEnumAttribute.NamingStrategy.get -> Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.MapEnumAttribute.NamingStrategy.set -> void
Riok.Mapperly.Abstractions.MapperAttribute.EnumNamingStrategy.get -> Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.MapperAttribute.EnumNamingStrategy.set -> void
3 changes: 1 addition & 2 deletions src/Riok.Mapperly/Configuration/EnumConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;

namespace Riok.Mapperly.Configuration;
Expand All @@ -22,7 +21,7 @@ public class EnumConfiguration(EnumMappingStrategy strategy)
/// <summary>
/// The fallback value if an enum cannot be mapped, used instead of throwing.
/// </summary>
public IFieldSymbol? FallbackValue { get; set; }
public AttributeValue? FallbackValue { get; set; }

/// <summary>
/// The strategy to be used to map enums from/to strings.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Riok.Mapperly.Configuration;
public record EnumMappingConfiguration(
EnumMappingStrategy Strategy,
bool IgnoreCase,
IFieldSymbol? FallbackValue,
AttributeValue? FallbackValue,
IReadOnlyCollection<IFieldSymbol> IgnoredSourceMembers,
IReadOnlyCollection<IFieldSymbol> IgnoredTargetMembers,
IReadOnlyCollection<EnumValueMappingConfiguration> ExplicitMappings,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.Mappings.Enums;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;

namespace Riok.Mapperly.Descriptors.MappingBuilders;

Expand Down Expand Up @@ -67,7 +71,7 @@ private static INewInstanceMapping BuildEnumToEnumCastMapping(
EqualityComparer<object>.Default
);
var fallbackMapping = BuildFallbackMapping(ctx);
if (fallbackMapping.FallbackMember != null && !checkTargetDefined)
if (fallbackMapping.FallbackExpression != null && !checkTargetDefined)
{
ctx.ReportDiagnostic(DiagnosticDescriptors.EnumFallbackValueRequiresByValueCheckDefinedStrategy);
checkTargetDefined = true;
Expand Down Expand Up @@ -226,7 +230,8 @@ IEnumerable<IFieldSymbol> targetMembers
return;

var missingTargetMembers = targetMembers.Where(field =>
!mappedTargetMembers.Contains(field) && ctx.Configuration.Enum.FallbackValue?.ConstantValue?.Equals(field.ConstantValue) != true
!mappedTargetMembers.Contains(field)
&& ctx.Configuration.Enum.FallbackValue?.ConstantValue.Value?.Equals(field.ConstantValue) != true
);
foreach (var member in missingTargetMembers)
{
Expand Down Expand Up @@ -265,17 +270,23 @@ IEnumerable<IFieldSymbol> sourceMembers
private static EnumFallbackValueMapping BuildFallbackMapping(MappingBuilderContext ctx)
{
var fallbackValue = ctx.Configuration.Enum.FallbackValue;
if (fallbackValue == null)
if (fallbackValue is not { Expression: MemberAccessExpressionSyntax memberAccessExpression })
return new EnumFallbackValueMapping(ctx.Source, ctx.Target);

if (SymbolEqualityComparer.Default.Equals(ctx.Target, fallbackValue.Type))
return new EnumFallbackValueMapping(ctx.Source, ctx.Target, fallbackMember: fallbackValue);
var fallbackExpression = MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
FullyQualifiedIdentifier(ctx.Target),
memberAccessExpression.Name
);

if (SymbolEqualityComparer.Default.Equals(ctx.Target, fallbackValue.Value.ConstantValue.Type))
return new EnumFallbackValueMapping(ctx.Source, ctx.Target, fallbackExpression: fallbackExpression);

ctx.ReportDiagnostic(
DiagnosticDescriptors.EnumFallbackValueTypeDoesNotMatchTargetEnumType,
fallbackValue,
fallbackValue.ConstantValue ?? 0,
fallbackValue.Type,
fallbackValue.Value.ConstantValue.Value ?? 0,
fallbackValue.Value.ConstantValue.Type?.Name ?? "unknown",
ctx.Target
);
return new EnumFallbackValueMapping(ctx.Source, ctx.Target);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.Mappings.Enums;
Expand All @@ -19,11 +20,33 @@ public static class EnumToStringMappingBuilder
return BuildEnumToStringMapping(ctx);
}

private static EnumFallbackValueMapping BuildFallbackMapping(MappingBuilderContext ctx)
{
var fallbackValue = ctx.Configuration.Enum.FallbackValue;
if (fallbackValue is not { Expression: LiteralExpressionSyntax literalExpressionSyntax })
{
return new EnumFallbackValueMapping(ctx.Source, ctx.Target, new EnumFallbackToStringMapping(ctx.Source, ctx.Target));
}

if (SymbolEqualityComparer.Default.Equals(ctx.Target, fallbackValue.Value.ConstantValue.Type))
return new EnumFallbackValueMapping(ctx.Source, ctx.Target, fallbackExpression: literalExpressionSyntax);

return new EnumFallbackValueMapping(ctx.Source, ctx.Target, new EnumFallbackToStringMapping(ctx.Source, ctx.Target));
}

private static EnumToStringMapping BuildEnumToStringMapping(MappingBuilderContext ctx)
{
var customNameMappings = ctx.Source.BuildCustomNameStrategyMappings(ctx.Configuration.Enum.NamingStrategy);
var customNameMappings = ctx.BuildCustomNameStrategyMappings(ctx.Source);
var fallbackMapping = BuildFallbackMapping(ctx);

// to string => use an optimized method of Enum.ToString which would use slow reflection
// use Enum.ToString as fallback (for ex. for flags)
return new EnumToStringMapping(ctx.Source, ctx.Target, ctx.SymbolAccessor.GetAllFields(ctx.Source), customNameMappings);
return new EnumToStringMapping(
ctx.Source,
ctx.Target,
ctx.SymbolAccessor.GetAllFields(ctx.Source),
customNameMappings,
fallbackMapping
);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.Mappings.Enums;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;

namespace Riok.Mapperly.Descriptors.MappingBuilders;

Expand Down Expand Up @@ -38,17 +42,18 @@ private static INewInstanceMapping BuildEnumToStringMapping(MappingBuilderContex
);
}

var customNameMappings = ctx.Target.BuildCustomNameStrategyMappings(ctx.Configuration.Enum.NamingStrategy);
var customNameMappings = ctx.BuildCustomNameStrategyMappings(ctx.Target);

// from string => use an optimized method of Enum.Parse which would use slow reflection
// however we currently don't support all features of Enum.Parse yet (ex. flags)
// therefore we use Enum.Parse as fallback.
var fallbackMapping = BuildFallbackParseMapping(ctx, genericEnumParseMethodSupported);
var members = ctx.SymbolAccessor.GetAllFields(ctx.Target);
if (fallbackMapping.FallbackMember != null)
if (fallbackMapping.FallbackExpression is not null)
{
// no need to explicitly map fallback value
members = members.Where(x => fallbackMapping.FallbackMember.ConstantValue?.Equals(x.ConstantValue) != true);
var fallback = (MemberAccessExpressionSyntax)fallbackMapping.FallbackExpression;
members = members.Where(x => !fallback.Name.ToString().Equals(x.Name, StringComparison.OrdinalIgnoreCase));
}

return new EnumFromStringSwitchMapping(
Expand All @@ -64,7 +69,7 @@ private static INewInstanceMapping BuildEnumToStringMapping(MappingBuilderContex
private static EnumFallbackValueMapping BuildFallbackParseMapping(MappingBuilderContext ctx, bool genericEnumParseMethodSupported)
{
var fallbackValue = ctx.Configuration.Enum.FallbackValue;
if (fallbackValue == null)
if (fallbackValue is not { Expression: MemberAccessExpressionSyntax memberAccessExpression })
{
return new EnumFallbackValueMapping(
ctx.Source,
Expand All @@ -73,14 +78,20 @@ private static EnumFallbackValueMapping BuildFallbackParseMapping(MappingBuilder
);
}

if (SymbolEqualityComparer.Default.Equals(ctx.Target, fallbackValue.Type))
return new EnumFallbackValueMapping(ctx.Source, ctx.Target, fallbackMember: fallbackValue);
var fallbackExpression = MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
FullyQualifiedIdentifier(ctx.Target),
memberAccessExpression.Name
);

if (SymbolEqualityComparer.Default.Equals(ctx.Target, fallbackValue.Value.ConstantValue.Type))
return new EnumFallbackValueMapping(ctx.Source, ctx.Target, fallbackExpression: fallbackExpression);

ctx.ReportDiagnostic(
DiagnosticDescriptors.EnumFallbackValueTypeDoesNotMatchTargetEnumType,
fallbackValue,
fallbackValue.ConstantValue ?? 0,
fallbackValue.Type,
fallbackValue.Value.ConstantValue.Value ?? 0,
fallbackValue.Value.ConstantValue.Type?.Name ?? "unknown",
ctx.Target
);
return new EnumFallbackValueMapping(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;

namespace Riok.Mapperly.Descriptors.Mappings.Enums;

public class EnumFallbackToStringMapping(ITypeSymbol source, ITypeSymbol target) : NewInstanceMapping(source, target)
{
private const string ToStringMethodName = nameof(Enum.ToString);

public override ExpressionSyntax Build(TypeMappingBuildContext ctx) =>
ctx.SyntaxFactory.Invocation(MemberAccess(ctx.Source, ToStringMethodName));
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ public class EnumFallbackValueMapping(
ITypeSymbol source,
ITypeSymbol target,
INewInstanceMapping? fallbackMapping = null,
IFieldSymbol? fallbackMember = null
ExpressionSyntax? fallbackExpression = null
) : NewInstanceMapping(source, target)
{
public IFieldSymbol? FallbackMember { get; } = fallbackMember;
public ExpressionSyntax? FallbackExpression { get; } = fallbackExpression;

public SwitchExpressionArmSyntax BuildDiscardArm(TypeMappingBuildContext ctx) => SwitchArm(DiscardPattern(), Build(ctx));

Expand All @@ -25,9 +25,9 @@ public override ExpressionSyntax Build(TypeMappingBuildContext ctx)
if (fallbackMapping != null)
return fallbackMapping.Build(ctx);

if (FallbackMember == null)
if (FallbackExpression == null)
return ThrowArgumentOutOfRangeException(ctx.Source, $"The value of enum {SourceType.Name} is not supported");

return MemberAccess(FullyQualifiedIdentifier(TargetType), FallbackMember.Name);
return FallbackExpression;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class EnumFromStringSwitchMapping(
IEnumerable<IFieldSymbol> enumMembers,
bool ignoreCase,
EnumFallbackValueMapping fallbackMapping,
Dictionary<IFieldSymbol, string> customNameMappings
IReadOnlyDictionary<IFieldSymbol, string> customNameMappings
) : NewInstanceMethodMapping(sourceType, targetType)
{
private const string IgnoreCaseSwitchDesignatedVariableName = "s";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ public class EnumToStringMapping(
ITypeSymbol sourceType,
ITypeSymbol targetType,
IEnumerable<IFieldSymbol> enumMembers,
IDictionary<IFieldSymbol, string> customNameMappings
IReadOnlyDictionary<IFieldSymbol, string> customNameMappings,
EnumFallbackValueMapping fallbackMapping
) : NewInstanceMethodMapping(sourceType, targetType)
{
private const string ToStringMethodName = nameof(Enum.ToString);

public override IEnumerable<StatementSyntax> BuildBody(TypeMappingBuildContext ctx)
{
// fallback switch arm: _ => source.ToString()
var fallbackArm = SwitchArm(DiscardPattern(), ctx.SyntaxFactory.Invocation(MemberAccess(ctx.Source, ToStringMethodName)));
// or: _ => "fallback_value" (if fallback mapping exists)
var fallbackArm = fallbackMapping.BuildDiscardArm(ctx);

// switch for each name to the enum value
// eg: Enum1.Value1 => "Value1"
Expand Down
Loading

0 comments on commit 711c763

Please sign in to comment.