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: support enum naming strategies for DescriptionAttribute and Enu… #1507

Merged
merged 1 commit into from
Sep 29, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
20 changes: 11 additions & 9 deletions docs/docs/configuration/enum.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,17 @@ Enum from/to strings mappings can be customized by setting the enum naming strat
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. |
| 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. |
| ComponentModelDescriptionAttribute | Matches enum values using the `Description` property of the `System.ComponentModel.DescriptionAttribute`. If the attribute is not present or the property is null, the member name is used. |
| SerializationEnumMemberAttribute | Matches enum values using the `Value` property of the `System.Runtime.Serialization.EnumMemberAttribute`. If the attribute is not present or the property is null, the member name is used. |

Note that explicit enum mappings (`MapEnumValue`) and fallback values (`FallbackValue` in `MapEnum`)
are not affected by naming strategies.
Expand Down
15 changes: 15 additions & 0 deletions src/Riok.Mapperly.Abstractions/EnumNamingStrategy.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System.ComponentModel;
using System.Runtime.Serialization;

namespace Riok.Mapperly.Abstractions;

/// <summary>
Expand Down Expand Up @@ -39,4 +42,16 @@ public enum EnumNamingStrategy
/// Matches enum values using UPPER-KEBAB-CASE.
/// </summary>
UpperKebabCase,

/// <summary>
/// Matches enum values using <see cref="DescriptionAttribute.Description"/>
/// or <see cref="MemberName"/> if the attribute is not present on the enum member.
/// </summary>
ComponentModelDescriptionAttribute,

/// <summary>
/// Matches enum values using <see cref="EnumMemberAttribute.Value"/>
/// or <see cref="MemberName"/> if the attribute is not present on the enum member.
/// </summary>
SerializationEnumMemberAttribute,
}
2 changes: 1 addition & 1 deletion src/Riok.Mapperly.Abstractions/MapperAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ public class MapperAttribute : Attribute
public bool AutoUserMappings { get; set; } = true;

/// <summary>
/// The default enum naming strategy.
/// Defines the strategy to use when mapping an enum from/to string.
/// Can be overwritten on specific enums via mapping method configurations.
/// </summary>
public EnumNamingStrategy EnumNamingStrategy { get; set; } = EnumNamingStrategy.MemberName;
Expand Down
2 changes: 2 additions & 0 deletions src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,5 @@ Riok.Mapperly.Abstractions.MapEnumAttribute.NamingStrategy.get -> Riok.Mapperly.
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.EnumNamingStrategy.ComponentModelDescriptionAttribute = 7 -> Riok.Mapperly.Abstractions.EnumNamingStrategy
Riok.Mapperly.Abstractions.EnumNamingStrategy.SerializationEnumMemberAttribute = 8 -> Riok.Mapperly.Abstractions.EnumNamingStrategy
5 changes: 4 additions & 1 deletion src/Riok.Mapperly/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,10 @@ 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
RMG085 | Mapper | Error | Invalid usage of fallback value
RMG086 | Mapper | Error | The source of the explicit mapping from a string to an enum is not of type string
RMG087 | Mapper | Error | The target of the explicit mapping from an enum to a string is not of type string
RMG088 | Mapper | Info | The attribute to build the name of the enum member is missing

### Removed Rules

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Riok.Mapperly.Configuration;

/// <summary>
/// Configuration class to represent <see cref="System.ComponentModel.DescriptionAttribute"/>
/// </summary>
public record ComponentModelDescriptionAttributeConfiguration(string? Description)
{
public ComponentModelDescriptionAttributeConfiguration()
: this((string?)null) { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.ComponentModel;
using System.Runtime.Serialization;
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Configuration;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext;

public static class EnumMappingBuilder
{
internal static string GetMemberName(MappingBuilderContext ctx, IFieldSymbol field) =>
GetMemberName(ctx, field, ctx.Configuration.Enum.NamingStrategy);

private static string GetMemberName(MappingBuilderContext ctx, IFieldSymbol field, EnumNamingStrategy namingStrategy)
{
return namingStrategy switch
{
EnumNamingStrategy.MemberName => field.Name,
EnumNamingStrategy.CamelCase => field.Name.ToCamelCase(),
EnumNamingStrategy.PascalCase => field.Name.ToPascalCase(),
EnumNamingStrategy.SnakeCase => field.Name.ToSnakeCase(),
EnumNamingStrategy.UpperSnakeCase => field.Name.ToUpperSnakeCase(),
EnumNamingStrategy.KebabCase => field.Name.ToKebabCase(),
EnumNamingStrategy.UpperKebabCase => field.Name.ToUpperKebabCase(),
EnumNamingStrategy.ComponentModelDescriptionAttribute => GetComponentModelDescription(ctx, field),
EnumNamingStrategy.SerializationEnumMemberAttribute => GetEnumMemberValue(ctx, field),
_ => throw new ArgumentOutOfRangeException($"{nameof(namingStrategy)} has an unknown value {namingStrategy}"),
};
}

private static string GetEnumMemberValue(MappingBuilderContext ctx, IFieldSymbol field)
{
var name = ctx.AttributeAccessor.AccessFirstOrDefault<EnumMemberAttribute>(field)?.Value;
if (name != null)
return name;

ctx.ReportDiagnostic(
DiagnosticDescriptors.EnumNamingAttributeMissing,
nameof(EnumMemberAttribute),
field.Name,
field.ConstantValue ?? "<unknown>"
);
return GetMemberName(ctx, field, EnumNamingStrategy.MemberName);
}

private static string GetComponentModelDescription(MappingBuilderContext ctx, IFieldSymbol field)
{
var name = ctx
.AttributeAccessor.AccessFirstOrDefault<DescriptionAttribute, ComponentModelDescriptionAttributeConfiguration>(field)
?.Description;
if (name != null)
return name;

ctx.ReportDiagnostic(
DiagnosticDescriptors.EnumNamingAttributeMissing,
nameof(DescriptionAttribute),
field.Name,
field.ConstantValue ?? "<unknown>"
);
return GetMemberName(ctx, field, EnumNamingStrategy.MemberName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,19 @@ private static EnumFallbackValueMapping BuildFallbackMapping(MappingBuilderConte

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

if (!SymbolEqualityComparer.Default.Equals(ctx.Target, fallbackValue.Value.ConstantValue.Type))
{
ctx.ReportDiagnostic(
DiagnosticDescriptors.EnumFallbackValueTypeDoesNotMatchTargetEnumType,
fallbackValue,
fallbackValue.Value.ConstantValue.Value ?? 0,
fallbackValue.Value.ConstantValue.Type?.Name ?? "unknown",
ctx.Target
);
return new EnumFallbackValueMapping(ctx.Source, ctx.Target);
}

Expand All @@ -193,25 +205,14 @@ private static EnumFallbackValueMapping BuildFallbackMapping(MappingBuilderConte
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.Value.ConstantValue.Value ?? 0,
fallbackValue.Value.ConstantValue.Type?.Name ?? "unknown",
ctx.Target
);
return new EnumFallbackValueMapping(ctx.Source, ctx.Target);
return new EnumFallbackValueMapping(ctx.Source, ctx.Target, fallbackExpression: fallbackExpression);
}

private static IReadOnlyDictionary<IFieldSymbol, IFieldSymbol> BuildExplicitValueMappings(MappingBuilderContext ctx)
{
var explicitMappings = new Dictionary<IFieldSymbol, IFieldSymbol>(SymbolEqualityComparer.Default);
var sourceFields = ctx.SymbolAccessor.GetEnumFields(ctx.Source);
var targetFields = ctx.SymbolAccessor.GetEnumFields(ctx.Target);
var sourceFields = ctx.SymbolAccessor.GetEnumFieldsByValue(ctx.Source);
var targetFields = ctx.SymbolAccessor.GetEnumFieldsByValue(ctx.Target);
foreach (var (source, target) in ctx.Configuration.Enum.ExplicitMappings)
{
if (source.ConstantValue.Kind is not TypedConstantKind.Enum)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext;
using Riok.Mapperly.Descriptors.Mappings;
Expand Down Expand Up @@ -29,21 +28,11 @@ private static EnumToStringMapping BuildEnumToStringMapping(MappingBuilderContex
{
var fallbackMapping = BuildFallbackMapping(ctx, out var fallbackStringValue);
var enumMemberMappings = BuildEnumMemberMappings(ctx, fallbackStringValue);

if (fallbackStringValue is not null)
{
enumMemberMappings = enumMemberMappings.Where(m =>
!m.TargetSyntax.ToString().Equals(fallbackStringValue, StringComparison.Ordinal)
);
}

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

private static IEnumerable<EnumMemberMapping> BuildEnumMemberMappings(MappingBuilderContext ctx, string? fallbackStringValue)
{
var namingStrategy = ctx.Configuration.Enum.NamingStrategy;

var ignoredSourceMembers = ctx.Configuration.Enum.IgnoredSourceMembers.ToHashSet(SymbolTypeEqualityComparer.FieldDefault);
EnumMappingDiagnosticReporter.AddUnmatchedSourceIgnoredMembers(ctx, ignoredSourceMembers);

Expand All @@ -54,37 +43,45 @@ private static IEnumerable<EnumMemberMapping> BuildEnumMemberMappings(MappingBui
{
// source.Value1
var sourceSyntax = MemberAccess(FullyQualifiedIdentifier(ctx.Source), sourceField.Name);

var name = sourceField.GetName(namingStrategy);
if (string.Equals(name, fallbackStringValue, StringComparison.Ordinal))
continue;

if (explicitValueMappings.TryGetValue(sourceField, out var explicitMapping))
if (explicitValueMappings.TryGetValue(sourceField, out var memberName))
{
if (string.Equals(fallbackStringValue, memberName, StringComparison.Ordinal))
continue;

// "explicit_value1"
yield return new EnumMemberMapping(sourceSyntax, explicitMapping);
yield return new EnumMemberMapping(sourceSyntax, StringLiteral(memberName));
continue;
}

if (namingStrategy is not EnumNamingStrategy.MemberName)
var name = EnumMappingBuilder.GetMemberName(ctx, sourceField);
if (string.Equals(name, fallbackStringValue, StringComparison.Ordinal))
continue;

if (ctx.Configuration.Enum.NamingStrategy == EnumNamingStrategy.MemberName)
{
// "value1"
yield return new EnumMemberMapping(sourceSyntax, StringLiteral(name));
// nameof(source.Value1)
yield return new EnumMemberMapping(sourceSyntax, NameOf(sourceSyntax));
continue;
}

// nameof(source.Value1)
yield return new EnumMemberMapping(sourceSyntax, NameOf(sourceSyntax));
// "value1"
yield return new EnumMemberMapping(sourceSyntax, StringLiteral(name));
}
}

private static IReadOnlyDictionary<IFieldSymbol, ExpressionSyntax> BuildExplicitValueMappings(MappingBuilderContext ctx)
private static IReadOnlyDictionary<IFieldSymbol, string> BuildExplicitValueMappings(MappingBuilderContext ctx)
{
var explicitMappings = new Dictionary<IFieldSymbol, ExpressionSyntax>(SymbolEqualityComparer.Default);
var sourceFields = ctx.SymbolAccessor.GetEnumFields(ctx.Source);
var explicitMappings = new Dictionary<IFieldSymbol, string>(SymbolEqualityComparer.Default);
if (!ctx.Configuration.Enum.HasExplicitConfigurations)
return explicitMappings;

var sourceFields = ctx.SymbolAccessor.GetEnumFieldsByValue(ctx.Source);
foreach (var (source, target) in ctx.Configuration.Enum.ExplicitMappings)
{
if (!SymbolEqualityComparer.Default.Equals(source.ConstantValue.Type, ctx.Source))
if (
!SymbolEqualityComparer.Default.Equals(source.ConstantValue.Type, ctx.Source)
|| !sourceFields.TryGetValue(source.ConstantValue.Value!, out var sourceField)
)
{
ctx.ReportDiagnostic(
DiagnosticDescriptors.SourceEnumValueDoesNotMatchSourceEnumType,
Expand All @@ -96,12 +93,13 @@ private static IReadOnlyDictionary<IFieldSymbol, ExpressionSyntax> BuildExplicit
continue;
}

if (!sourceFields.TryGetValue(source.ConstantValue.Value!, out var sourceField))
if (target.ConstantValue.Value is not string targetStringValue)
{
ctx.ReportDiagnostic(DiagnosticDescriptors.EnumExplicitMappingTargetNotString);
continue;
}

if (!explicitMappings.TryAdd(sourceField, target.Expression))
if (!explicitMappings.TryAdd(sourceField, targetStringValue))
{
ctx.ReportDiagnostic(DiagnosticDescriptors.EnumSourceValueDuplicated, sourceField, ctx.Source, ctx.Target);
}
Expand All @@ -121,7 +119,7 @@ private static EnumFallbackValueMapping BuildFallbackMapping(MappingBuilderConte

if (fallbackValue.Value.ConstantValue.Value is not string fallbackString)
{
ctx.ReportDiagnostic(DiagnosticDescriptors.InvalidFallbackValue, fallbackValue.Value.Expression.ToFullString());
ctx.ReportDiagnostic(DiagnosticDescriptors.InvalidEnumMappingFallbackValue, fallbackValue.Value.Expression.ToFullString());
return new EnumFallbackValueMapping(ctx.Source, ctx.Target, new ToStringMapping(ctx.Source, ctx.Target));
}

Expand Down
Loading
Loading