Skip to content

Commit

Permalink
feat: custom enum naming strategies (#1486)
Browse files Browse the repository at this point in the history
* feat: custom enum naming strategies

---------

Co-authored-by: latonz <lars@riok.ch>
  • Loading branch information
BeeTwin and latonz authored Sep 29, 2024
1 parent c65d873 commit 5932a4f
Show file tree
Hide file tree
Showing 26 changed files with 1,147 additions and 117 deletions.
58 changes: 58 additions & 0 deletions docs/docs/configuration/enum.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,68 @@ The `IgnoreCase` property allows to opt in for case insensitive mappings (defaul
</TabItem>
</Tabs>

## Enum from/to string naming strategies

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 `EnumNamingStrategy` in `MapperAttribute` and `MapperDefaultsAttribute`.
Available naming strategies:

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

Note that explicit enum mappings (`MapEnumValue`) and fallback values (`FallbackValue` in `MapEnum`)
are not affected by naming strategies.

<Tabs>
<TabItem value="global" label="Global (mapper level)" default>

Applied to all enums mapped inside this mapper.

```csharp
// highlight-start
[Mapper(EnumNamingStrategy = EnumNamingStrategy.SnakeCase)]
// highlight-end
public partial class CarMapper
{
...
}
```

</TabItem>
<TabItem value="enum" label="Enum (mapping method level)">

Applied to the specific enum mapped by this method.
Attribute is only valid on mapping method with enums as parameters.

```csharp
[Mapper]
public partial class CarMapper
{
// highlight-start
[MapEnum(EnumMappingStrategy.ByName, NamingStrategy = EnumNamingStrategy.SnakeCase)]
// highlight-end
public partial CarMakeDto MapMake(CarMake make);
}
```

</TabItem>

</Tabs>

## Manually mapped enum values

To explicitly map enum values the `MapEnumValueAttibute` can be used.
Attribute is only valid on enum-to-enum, enum-to-string and string-to-enum mappings.

Explicit enum mappings are not affected by enum naming strategies.

```csharp
[Mapper]
public partial class CarMapper
Expand Down Expand Up @@ -106,6 +163,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
42 changes: 42 additions & 0 deletions src/Riok.Mapperly.Abstractions/EnumNamingStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace Riok.Mapperly.Abstractions;

/// <summary>
/// Defines the strategy to use when mapping an enum from/to string.
/// </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>
PascalCase,

/// <summary>
/// Matches enum values using snake_case.
/// </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,
}
7 changes: 6 additions & 1 deletion src/Riok.Mapperly.Abstractions/MapEnumAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public MapEnumAttribute(EnumMappingStrategy strategy)
}

/// <summary>
/// The strategy to be used to map enums.
/// The strategy to be used to map enums to enums.
/// </summary>
public EnumMappingStrategy Strategy { get; }

Expand All @@ -32,4 +32,9 @@ public MapEnumAttribute(EnumMappingStrategy strategy)
/// The fallback value if an enum cannot be mapped, used instead of throwing.
/// </summary>
public object? FallbackValue { get; set; }

/// <summary>
/// The strategy to be used to map enums from/to strings.
/// </summary>
public EnumNamingStrategy NamingStrategy { get; set; }
}
6 changes: 6 additions & 0 deletions src/Riok.Mapperly.Abstractions/MapperAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,10 @@ public class MapperAttribute : Attribute
/// partial methods are discovered.
/// </summary>
public bool AutoUserMappings { get; set; } = true;

/// <summary>
/// The default enum naming strategy.
/// Can be overwritten on specific enums via mapping method configurations.
/// </summary>
public EnumNamingStrategy EnumNamingStrategy { get; set; } = EnumNamingStrategy.MemberName;
}
12 changes: 12 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,15 @@ Riok.Mapperly.Abstractions.MapperAttribute.IncludedConstructors.get -> Riok.Mapp
Riok.Mapperly.Abstractions.MapperAttribute.IncludedConstructors.set -> void
Riok.Mapperly.Abstractions.MapEnumValueAttribute.Source.get -> object!
Riok.Mapperly.Abstractions.MapEnumValueAttribute.Target.get -> object!
Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.EnumNamingStrategy.MemberName = 0 -> Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.EnumNamingStrategy.CamelCase = 1 -> 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.UpperSnakeCase = 4 -> Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.EnumNamingStrategy.KebabCase = 5 -> Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.EnumNamingStrategy.UpperKebabCase = 6 -> 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
1 change: 1 addition & 0 deletions src/Riok.Mapperly/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ RMG081 | Mapper | Error | A mapping method with additional parameters cann
RMG082 | Mapper | Warning | An additional mapping method parameter is not mapped
RMG083 | Mapper | Info | Cannot map to read only type
RMG084 | Mapper | Error | Multiple mappings are configured for the same source string
RMG085 | Mapper | Error | Invalid usage of Fallback value

### Removed Rules

Expand Down
10 changes: 7 additions & 3 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 @@ -10,7 +9,7 @@ namespace Riok.Mapperly.Configuration;
public class EnumConfiguration(EnumMappingStrategy strategy)
{
/// <summary>
/// The strategy to be used to map enums.
/// The strategy to be used to map enums to enums.
/// </summary>
public EnumMappingStrategy Strategy { get; } = strategy;

Expand All @@ -22,5 +21,10 @@ 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.
/// </summary>
public EnumNamingStrategy NamingStrategy { get; set; }
}
5 changes: 3 additions & 2 deletions src/Riok.Mapperly/Configuration/EnumMappingConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ namespace Riok.Mapperly.Configuration;
public record EnumMappingConfiguration(
EnumMappingStrategy Strategy,
bool IgnoreCase,
IFieldSymbol? FallbackValue,
AttributeValue? FallbackValue,
IReadOnlyCollection<IFieldSymbol> IgnoredSourceMembers,
IReadOnlyCollection<IFieldSymbol> IgnoredTargetMembers,
IReadOnlyCollection<EnumValueMappingConfiguration> ExplicitMappings,
RequiredMappingStrategy RequiredMappingStrategy
RequiredMappingStrategy RequiredMappingStrategy,
EnumNamingStrategy NamingStrategy
)
{
public bool HasExplicitConfigurations => ExplicitMappings.Count > 0 || IgnoredSourceMembers.Count > 0 || IgnoredTargetMembers.Count > 0;
Expand Down
6 changes: 6 additions & 0 deletions src/Riok.Mapperly/Configuration/MapperConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,10 @@ public record MapperConfiguration
/// Whether to consider non-partial methods in a mapper as user implemented mapping methods.
/// </summary>
public bool? AutoUserMappings { get; init; }

/// <summary>
/// The default enum naming strategy.
/// Can be overwritten on specific enums via mapping method configurations.
/// </summary>
public EnumNamingStrategy? EnumNamingStrategy { get; init; }
}
3 changes: 3 additions & 0 deletions src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ public static MapperAttribute Merge(MapperConfiguration mapperConfiguration, Map
mapper.AutoUserMappings =
mapperConfiguration.AutoUserMappings ?? defaultMapperConfiguration.AutoUserMappings ?? mapper.AutoUserMappings;

mapper.EnumNamingStrategy =
mapperConfiguration.EnumNamingStrategy ?? defaultMapperConfiguration.EnumNamingStrategy ?? mapper.EnumNamingStrategy;

return mapper;
}
}
6 changes: 4 additions & 2 deletions src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ MapperConfiguration defaultMapperConfiguration
[],
[],
[],
mapper.RequiredMappingStrategy
mapper.RequiredMappingStrategy,
mapper.EnumNamingStrategy
),
new MembersMappingConfiguration([], [], [], [], [], mapper.IgnoreObsoleteMembersStrategy, mapper.RequiredMappingStrategy),
[]
Expand Down Expand Up @@ -162,7 +163,8 @@ private EnumMappingConfiguration BuildEnumConfig(MappingConfigurationReference c
ignoredSources,
ignoredTargets,
explicitMappings,
requiredMapping ?? MapperConfiguration.Enum.RequiredMappingStrategy
requiredMapping ?? MapperConfiguration.Enum.RequiredMappingStrategy,
configData?.NamingStrategy ?? MapperConfiguration.Enum.NamingStrategy
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ IEnumerable<IFieldSymbol> targetMembers
if (!ctx.Configuration.Enum.RequiredMappingStrategy.HasFlag(RequiredMappingStrategy.Target))
return;

var fallbackValue = ctx.Configuration.Enum.FallbackValue?.ConstantValue.Value;
var missingTargetMembers = targetMembers.Where(field =>
!mappings.Contains(field) && ctx.Configuration.Enum.FallbackValue?.ConstantValue?.Equals(field.ConstantValue) != true
!mappings.Contains(field) && fallbackValue?.Equals(field.ConstantValue) is not true
);
foreach (var member in missingTargetMembers)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext;
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 @@ -68,7 +72,7 @@ private static INewInstanceMapping BuildEnumToEnumCastMapping(
EqualityComparer<object>.Default
);
var fallbackMapping = BuildFallbackMapping(ctx);
if (fallbackMapping.FallbackMember != null && !checkTargetDefined)
if (fallbackMapping.FallbackExpression is not null && !checkTargetDefined)
{
ctx.ReportDiagnostic(DiagnosticDescriptors.EnumFallbackValueRequiresByValueCheckDefinedStrategy);
checkTargetDefined = true;
Expand Down Expand Up @@ -173,17 +177,31 @@ params IEqualityComparer<T>[] propertyComparer
private static EnumFallbackValueMapping BuildFallbackMapping(MappingBuilderContext ctx)
{
var fallbackValue = ctx.Configuration.Enum.FallbackValue;
if (fallbackValue == null)
if (fallbackValue is null)
{
return new EnumFallbackValueMapping(ctx.Source, ctx.Target);
}

if (fallbackValue is not { Expression: MemberAccessExpressionSyntax memberAccessExpression })
{
ctx.ReportDiagnostic(DiagnosticDescriptors.InvalidFallbackValue, fallbackValue.Value.Expression.ToFullString());
return new EnumFallbackValueMapping(ctx.Source, ctx.Target);
}

var fallbackExpression = MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
FullyQualifiedIdentifier(ctx.Target),
memberAccessExpression.Name
);

if (SymbolEqualityComparer.Default.Equals(ctx.Target, fallbackValue.Type))
return new EnumFallbackValueMapping(ctx.Source, ctx.Target, fallbackMember: fallbackValue);
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
Loading

0 comments on commit 5932a4f

Please sign in to comment.