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

1470 enum naming strategy #1486

Merged
merged 4 commits into from
Sep 29, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
57 changes: 57 additions & 0 deletions docs/docs/configuration/enum.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,67 @@ 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 +162,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,
latonz marked this conversation as resolved.
Show resolved Hide resolved

/// <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
Loading