diff --git a/docs/docs/configuration/enum.mdx b/docs/docs/configuration/enum.mdx
index 20db9a82c3..295165b522 100644
--- a/docs/docs/configuration/enum.mdx
+++ b/docs/docs/configuration/enum.mdx
@@ -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.
diff --git a/src/Riok.Mapperly.Abstractions/EnumNamingStrategy.cs b/src/Riok.Mapperly.Abstractions/EnumNamingStrategy.cs
index ecbc9711b1..d962805e95 100644
--- a/src/Riok.Mapperly.Abstractions/EnumNamingStrategy.cs
+++ b/src/Riok.Mapperly.Abstractions/EnumNamingStrategy.cs
@@ -1,3 +1,6 @@
+using System.ComponentModel;
+using System.Runtime.Serialization;
+
namespace Riok.Mapperly.Abstractions;
///
@@ -39,4 +42,16 @@ public enum EnumNamingStrategy
/// Matches enum values using UPPER-KEBAB-CASE.
///
UpperKebabCase,
+
+ ///
+ /// Matches enum values using
+ /// or if the attribute is not present on the enum member.
+ ///
+ ComponentModelDescriptionAttribute,
+
+ ///
+ /// Matches enum values using
+ /// or if the attribute is not present on the enum member.
+ ///
+ SerializationEnumMemberAttribute,
}
diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
index 664bcc5227..c3004431a7 100644
--- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
+++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
@@ -127,7 +127,7 @@ public class MapperAttribute : Attribute
public bool AutoUserMappings { get; set; } = true;
///
- /// 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.
///
public EnumNamingStrategy EnumNamingStrategy { get; set; } = EnumNamingStrategy.MemberName;
diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
index 01a2c66689..661fac8783 100644
--- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
+++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
@@ -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
diff --git a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md
index dd7ae0cd63..dfc07857f8 100644
--- a/src/Riok.Mapperly/AnalyzerReleases.Shipped.md
+++ b/src/Riok.Mapperly/AnalyzerReleases.Shipped.md
@@ -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
diff --git a/src/Riok.Mapperly/Configuration/ComponentModelDescriptionAttributeConfiguration.cs b/src/Riok.Mapperly/Configuration/ComponentModelDescriptionAttributeConfiguration.cs
new file mode 100644
index 0000000000..da9b94ddb0
--- /dev/null
+++ b/src/Riok.Mapperly/Configuration/ComponentModelDescriptionAttributeConfiguration.cs
@@ -0,0 +1,10 @@
+namespace Riok.Mapperly.Configuration;
+
+///
+/// Configuration class to represent
+///
+public record ComponentModelDescriptionAttributeConfiguration(string? Description)
+{
+ public ComponentModelDescriptionAttributeConfiguration()
+ : this((string?)null) { }
+}
diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/EnumMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/EnumMappingBuilder.cs
new file mode 100644
index 0000000000..63b4f648c4
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/EnumMappingBuilder.cs
@@ -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(field)?.Value;
+ if (name != null)
+ return name;
+
+ ctx.ReportDiagnostic(
+ DiagnosticDescriptors.EnumNamingAttributeMissing,
+ nameof(EnumMemberAttribute),
+ field.Name,
+ field.ConstantValue ?? ""
+ );
+ return GetMemberName(ctx, field, EnumNamingStrategy.MemberName);
+ }
+
+ private static string GetComponentModelDescription(MappingBuilderContext ctx, IFieldSymbol field)
+ {
+ var name = ctx
+ .AttributeAccessor.AccessFirstOrDefault(field)
+ ?.Description;
+ if (name != null)
+ return name;
+
+ ctx.ReportDiagnostic(
+ DiagnosticDescriptors.EnumNamingAttributeMissing,
+ nameof(DescriptionAttribute),
+ field.Name,
+ field.ConstantValue ?? ""
+ );
+ return GetMemberName(ctx, field, EnumNamingStrategy.MemberName);
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToEnumMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToEnumMappingBuilder.cs
index a532739fe4..92e6002e6e 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToEnumMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToEnumMappingBuilder.cs
@@ -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);
}
@@ -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 BuildExplicitValueMappings(MappingBuilderContext ctx)
{
var explicitMappings = new Dictionary(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)
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToStringMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToStringMappingBuilder.cs
index 21940abaf2..6fbe7870c3 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToStringMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToStringMappingBuilder.cs
@@ -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;
@@ -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 BuildEnumMemberMappings(MappingBuilderContext ctx, string? fallbackStringValue)
{
- var namingStrategy = ctx.Configuration.Enum.NamingStrategy;
-
var ignoredSourceMembers = ctx.Configuration.Enum.IgnoredSourceMembers.ToHashSet(SymbolTypeEqualityComparer.FieldDefault);
EnumMappingDiagnosticReporter.AddUnmatchedSourceIgnoredMembers(ctx, ignoredSourceMembers);
@@ -54,37 +43,45 @@ private static IEnumerable 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 BuildExplicitValueMappings(MappingBuilderContext ctx)
+ private static IReadOnlyDictionary BuildExplicitValueMappings(MappingBuilderContext ctx)
{
- var explicitMappings = new Dictionary(SymbolEqualityComparer.Default);
- var sourceFields = ctx.SymbolAccessor.GetEnumFields(ctx.Source);
+ var explicitMappings = new Dictionary(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,
@@ -96,12 +93,13 @@ private static IReadOnlyDictionary 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);
}
@@ -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));
}
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/StringToEnumMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/StringToEnumMappingBuilder.cs
index 6956ebd5b8..0aa5ebd794 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilders/StringToEnumMappingBuilder.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/StringToEnumMappingBuilder.cs
@@ -46,19 +46,11 @@ private static EnumFromStringSwitchMapping BuildEnumFromStringSwitchMapping(
bool genericEnumParseMethodSupported
)
{
- var fallbackMapping = BuildFallbackParseMapping(ctx, genericEnumParseMethodSupported, out var fallbackMember);
- var enumMemberMappings = BuildEnumMemberMappings(ctx, fallbackMember);
// 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.
- if (fallbackMember is not null)
- {
- // no need to explicitly map fallback value
- enumMemberMappings = enumMemberMappings.Where(m =>
- !m.TargetSyntax.ToString().Equals(fallbackMember.Name, StringComparison.Ordinal)
- );
- }
-
+ var fallbackMapping = BuildFallbackParseMapping(ctx, genericEnumParseMethodSupported, out var fallbackMember);
+ var enumMemberMappings = BuildEnumMemberMappings(ctx, fallbackMember);
return new EnumFromStringSwitchMapping(
ctx.Source,
ctx.Target,
@@ -70,8 +62,6 @@ bool genericEnumParseMethodSupported
private static IEnumerable BuildEnumMemberMappings(MappingBuilderContext ctx, IFieldSymbol? fallbackMember)
{
- var namingStrategy = ctx.Configuration.Enum.NamingStrategy;
-
var ignoredTargetMembers = ctx.Configuration.Enum.IgnoredTargetMembers.ToHashSet(SymbolTypeEqualityComparer.FieldDefault);
EnumMappingDiagnosticReporter.AddUnmatchedTargetIgnoredMembers(ctx, ignoredTargetMembers);
if (fallbackMember is not null)
@@ -81,23 +71,37 @@ private static IEnumerable BuildEnumMemberMappings(MappingBui
var targetFields = ctx.SymbolAccessor.GetFieldsExcept(ctx.Target, ignoredTargetMembers);
var explicitValueMappings = BuildExplicitValueMappings(ctx);
+ var processedSources = new HashSet