From 711c7633a890e0fe5501d2d7527fd18235684ebb Mon Sep 17 00:00:00 2001 From: BeeTwin Date: Fri, 6 Sep 2024 23:29:54 +0400 Subject: [PATCH] feat: pr fixes + new naming strategies --- docs/docs/configuration/enum.mdx | 17 +- .../EnumNamingStrategy.cs | 20 ++ .../MapperDefaultsAttribute.cs | 8 +- .../PublicAPI.Shipped.txt | 14 + .../PublicAPI.Unshipped.txt | 8 - .../Configuration/EnumConfiguration.cs | 3 +- .../Configuration/EnumMappingConfiguration.cs | 2 +- .../MappingBuilders/EnumMappingBuilder.cs | 25 +- .../EnumToStringMappingBuilder.cs | 27 +- .../StringToEnumMappingBuilder.cs | 27 +- .../Enums/EnumFallbackToStringMapping.cs | 13 + .../Enums/EnumFallbackValueMapping.cs | 8 +- .../Enums/EnumFromStringSwitchMapping.cs | 2 +- .../Mappings/Enums/EnumToStringMapping.cs | 8 +- .../Helpers/EnumNamingStrategyHelper.cs | 44 +-- src/Riok.Mapperly/Helpers/MemberNamingUtil.cs | 123 ++++++++ .../Helpers/MemberNamingUtilTests.cs | 66 ++++ .../Mapping/EnumNamingStrategyTest.cs | 287 +++++++++++++++++- 18 files changed, 621 insertions(+), 81 deletions(-) create mode 100644 src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumFallbackToStringMapping.cs create mode 100644 src/Riok.Mapperly/Helpers/MemberNamingUtil.cs create mode 100644 test/Riok.Mapperly.Tests/Helpers/MemberNamingUtilTests.cs diff --git a/docs/docs/configuration/enum.mdx b/docs/docs/configuration/enum.mdx index 2f4bbaa594..34f6c0e00a 100644 --- a/docs/docs/configuration/enum.mdx +++ b/docs/docs/configuration/enum.mdx @@ -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. | @@ -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] diff --git a/src/Riok.Mapperly.Abstractions/EnumNamingStrategy.cs b/src/Riok.Mapperly.Abstractions/EnumNamingStrategy.cs index ef0ae19c41..ecbc9711b1 100644 --- a/src/Riok.Mapperly.Abstractions/EnumNamingStrategy.cs +++ b/src/Riok.Mapperly.Abstractions/EnumNamingStrategy.cs @@ -5,6 +5,16 @@ namespace Riok.Mapperly.Abstractions; /// public enum EnumNamingStrategy { + /// + /// Matches enum values using their name. + /// + MemberName, + + /// + /// Matches enum values using camelCase. + /// + CamelCase, + /// /// Matches enum values using PascalCase. /// @@ -15,8 +25,18 @@ public enum EnumNamingStrategy /// SnakeCase, + /// + /// Matches enum values using UPPER_SNAKE_CASE. + /// + UpperSnakeCase, + /// /// Matches enum values using kebab-case. /// KebabCase, + + /// + /// Matches enum values using UPPER-KEBAB-CASE. + /// + UpperKebabCase, } diff --git a/src/Riok.Mapperly.Abstractions/MapperDefaultsAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperDefaultsAttribute.cs index 0017c49036..918dede8be 100644 --- a/src/Riok.Mapperly.Abstractions/MapperDefaultsAttribute.cs +++ b/src/Riok.Mapperly.Abstractions/MapperDefaultsAttribute.cs @@ -7,4 +7,10 @@ namespace Riok.Mapperly.Abstractions; /// [AttributeUsage(AttributeTargets.Assembly)] [Conditional("MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME")] -public sealed class MapperDefaultsAttribute : MapperAttribute; +public sealed class MapperDefaultsAttribute : MapperAttribute +{ + /// + /// The strategy to be used to map enums from/to strings. + /// + public EnumNamingStrategy NamingStrategy { get; set; } +} diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt index abdaf0953c..82691fa630 100644 --- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt +++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt @@ -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 diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Unshipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Unshipped.txt index f152c5d528..7dc5c58110 100644 --- a/src/Riok.Mapperly.Abstractions/PublicAPI.Unshipped.txt +++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Unshipped.txt @@ -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 diff --git a/src/Riok.Mapperly/Configuration/EnumConfiguration.cs b/src/Riok.Mapperly/Configuration/EnumConfiguration.cs index d3e0f4768f..212c140432 100644 --- a/src/Riok.Mapperly/Configuration/EnumConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/EnumConfiguration.cs @@ -1,4 +1,3 @@ -using Microsoft.CodeAnalysis; using Riok.Mapperly.Abstractions; namespace Riok.Mapperly.Configuration; @@ -22,7 +21,7 @@ public class EnumConfiguration(EnumMappingStrategy strategy) /// /// The fallback value if an enum cannot be mapped, used instead of throwing. /// - public IFieldSymbol? FallbackValue { get; set; } + public AttributeValue? FallbackValue { get; set; } /// /// The strategy to be used to map enums from/to strings. diff --git a/src/Riok.Mapperly/Configuration/EnumMappingConfiguration.cs b/src/Riok.Mapperly/Configuration/EnumMappingConfiguration.cs index 0be03e130f..b2af7e1b35 100644 --- a/src/Riok.Mapperly/Configuration/EnumMappingConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/EnumMappingConfiguration.cs @@ -6,7 +6,7 @@ namespace Riok.Mapperly.Configuration; public record EnumMappingConfiguration( EnumMappingStrategy Strategy, bool IgnoreCase, - IFieldSymbol? FallbackValue, + AttributeValue? FallbackValue, IReadOnlyCollection IgnoredSourceMembers, IReadOnlyCollection IgnoredTargetMembers, IReadOnlyCollection ExplicitMappings, diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumMappingBuilder.cs index 437852ddd8..347e9bc980 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumMappingBuilder.cs @@ -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; @@ -67,7 +71,7 @@ private static INewInstanceMapping BuildEnumToEnumCastMapping( EqualityComparer.Default ); var fallbackMapping = BuildFallbackMapping(ctx); - if (fallbackMapping.FallbackMember != null && !checkTargetDefined) + if (fallbackMapping.FallbackExpression != null && !checkTargetDefined) { ctx.ReportDiagnostic(DiagnosticDescriptors.EnumFallbackValueRequiresByValueCheckDefinedStrategy); checkTargetDefined = true; @@ -226,7 +230,8 @@ IEnumerable 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) { @@ -265,17 +270,23 @@ IEnumerable 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); diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToStringMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToStringMappingBuilder.cs index 8a2e211886..f014e270ce 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToStringMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumToStringMappingBuilder.cs @@ -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; @@ -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 + ); } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/StringToEnumMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/StringToEnumMappingBuilder.cs index 0fd559228b..da7e853242 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/StringToEnumMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/StringToEnumMappingBuilder.cs @@ -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; @@ -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( @@ -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, @@ -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( diff --git a/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumFallbackToStringMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumFallbackToStringMapping.cs new file mode 100644 index 0000000000..2ce86201c0 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumFallbackToStringMapping.cs @@ -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)); +} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumFallbackValueMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumFallbackValueMapping.cs index 3d95e85d55..8dc9d360b8 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumFallbackValueMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumFallbackValueMapping.cs @@ -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)); @@ -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; } } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumFromStringSwitchMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumFromStringSwitchMapping.cs index eb418a8307..ec39257979 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumFromStringSwitchMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumFromStringSwitchMapping.cs @@ -18,7 +18,7 @@ public class EnumFromStringSwitchMapping( IEnumerable enumMembers, bool ignoreCase, EnumFallbackValueMapping fallbackMapping, - Dictionary customNameMappings + IReadOnlyDictionary customNameMappings ) : NewInstanceMethodMapping(sourceType, targetType) { private const string IgnoreCaseSwitchDesignatedVariableName = "s"; diff --git a/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumToStringMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumToStringMapping.cs index 9d3e57fd1d..79df1490a7 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumToStringMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/Enums/EnumToStringMapping.cs @@ -15,15 +15,15 @@ public class EnumToStringMapping( ITypeSymbol sourceType, ITypeSymbol targetType, IEnumerable enumMembers, - IDictionary customNameMappings + IReadOnlyDictionary customNameMappings, + EnumFallbackValueMapping fallbackMapping ) : NewInstanceMethodMapping(sourceType, targetType) { - private const string ToStringMethodName = nameof(Enum.ToString); - public override IEnumerable 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" diff --git a/src/Riok.Mapperly/Helpers/EnumNamingStrategyHelper.cs b/src/Riok.Mapperly/Helpers/EnumNamingStrategyHelper.cs index aac62eb49a..8418380646 100644 --- a/src/Riok.Mapperly/Helpers/EnumNamingStrategyHelper.cs +++ b/src/Riok.Mapperly/Helpers/EnumNamingStrategyHelper.cs @@ -1,23 +1,19 @@ -using System.Globalization; -using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Descriptors; namespace Riok.Mapperly.Helpers; public static class EnumNamingStrategyHelper { - public static Dictionary BuildCustomNameStrategyMappings( - this ITypeSymbol enumSymbol, - EnumNamingStrategy namingStrategy - ) + public static Dictionary BuildCustomNameStrategyMappings(this MappingBuilderContext ctx, ITypeSymbol enumSymbol) { var customNameMappings = new Dictionary(SymbolEqualityComparer.Default); - var values = enumSymbol.GetMembers().OfType(); + var values = ctx.SymbolAccessor.GetAllFields(enumSymbol); foreach (var value in values) { - var valueString = ConvertEnumValueNameToString(value.Name, namingStrategy); + var valueString = ConvertEnumValueNameToString(value.Name, ctx.Configuration.Enum.NamingStrategy); customNameMappings.Add(value, valueString); } @@ -28,32 +24,14 @@ private static string ConvertEnumValueNameToString(string enumValueName, EnumNam { return namingStrategy switch { - EnumNamingStrategy.PascalCase => enumValueName, - EnumNamingStrategy.SnakeCase => JoinWordsFrom("_", enumValueName), - EnumNamingStrategy.KebabCase => JoinWordsFrom("-", enumValueName), + EnumNamingStrategy.MemberName => enumValueName, + EnumNamingStrategy.CamelCase => enumValueName.ToCamelCase(), + EnumNamingStrategy.PascalCase => enumValueName.ToPascalCase(), + EnumNamingStrategy.SnakeCase => enumValueName.ToSnakeCase(), + EnumNamingStrategy.UpperSnakeCase => enumValueName.ToUpperSnakeCase(), + EnumNamingStrategy.KebabCase => enumValueName.ToKebabCase(), + EnumNamingStrategy.UpperKebabCase => enumValueName.ToUpperKebabCase(), _ => throw new ArgumentOutOfRangeException($"{nameof(namingStrategy)} has an unknown value {namingStrategy}"), }; } - - private static string[] SplitEnumValueNameIntoWords(string enumValueName) - { - // Matches (or): - // [A-Z][a-z]+ - // - A word starting with an uppercase letter, followed by lowercase letters ("Word" or "Word123") - // [A-Z]+(?![a-z]) - // - One or more uppercase letters, with no lowercase letters after ("ID" or "C1") - // \d+ - // - One or more digits ("123") - // "Pascal1CaseID2_3" = "Pascal1" + "Case" + "ID2" + "3" -#pragma warning disable MA0009 - var words = Regex.Matches(enumValueName, @"[A-Z][a-z]+|[A-Z]+(?![a-z])|\d+"); -#pragma warning restore MA0009 - return (from Match word in words select word.Value).ToArray(); - } - - private static string JoinWordsFrom(string separator, string enumValueName) - { - var lowerCaseWords = SplitEnumValueNameIntoWords(enumValueName).Select(w => w.ToLower(CultureInfo.InvariantCulture)); - return string.Join(separator, lowerCaseWords); - } } diff --git a/src/Riok.Mapperly/Helpers/MemberNamingUtil.cs b/src/Riok.Mapperly/Helpers/MemberNamingUtil.cs new file mode 100644 index 0000000000..358a7db0c0 --- /dev/null +++ b/src/Riok.Mapperly/Helpers/MemberNamingUtil.cs @@ -0,0 +1,123 @@ +namespace Riok.Mapperly.Helpers; + +public static class MemberNamingUtil +{ + private enum Naming + { + Lower, + Upper, + } + + public static string ToCamelCase(this string str) => str.ToPascalCase().ToLowerFirstChar(); + + public static string ToPascalCase(this string str) => + str.JoinUsingNaming("", wordsNaming: Naming.Lower, firstLetterNaming: Naming.Upper); + + public static string ToSnakeCase(this string str) => + str.JoinUsingNaming("_", wordsNaming: Naming.Lower, firstLetterNaming: Naming.Lower); + + public static string ToUpperSnakeCase(this string str) => str.ToSnakeCase().ToUpperInvariant(); + + public static string ToKebabCase(this string str) => + str.JoinUsingNaming("-", wordsNaming: Naming.Lower, firstLetterNaming: Naming.Lower); + + public static string ToUpperKebabCase(this string str) => str.ToKebabCase().ToUpperInvariant(); + + private static string ToLowerFirstChar(this string str) => char.ToLowerInvariant(str[0]) + (str.Length is 1 ? "" : str[1..]); + + private static string ToUpperFirstChar(this string str) => char.ToUpperInvariant(str[0]) + (str.Length is 1 ? "" : str[1..]); + + private static string JoinUsingNaming(this string memberName, string separator, Naming wordsNaming, Naming firstLetterNaming) + { + if (string.IsNullOrWhiteSpace(memberName)) + { + return memberName; + } + + var words = new MemberNameToWordsSplitter(memberName).SplitIntoWords(); + var wordsInCorrectNaming = wordsNaming switch + { + Naming.Lower => words.Select(w => w.ToLowerInvariant()), + Naming.Upper => words.Select(w => w.ToUpperInvariant()), + _ => throw new ArgumentOutOfRangeException($"{nameof(wordsNaming)} has an unknown value {wordsNaming}"), + }; + + var wordsWithCorrectFirstLetters = firstLetterNaming switch + { + Naming.Lower => wordsInCorrectNaming.Select(w => w.ToLowerFirstChar()), + Naming.Upper => wordsInCorrectNaming.Select(w => w.ToUpperFirstChar()), + _ => throw new ArgumentOutOfRangeException($"{nameof(firstLetterNaming)} has an unknown value {firstLetterNaming}"), + }; + + return string.Join(separator, wordsWithCorrectFirstLetters); + } + + private class MemberNameToWordsSplitter(string memberName) + { + private string _acc = string.Empty; + + public IEnumerable SplitIntoWords() + { + for (var i = 0; i < memberName.Length; i++) + { + var current = memberName[i]; + if (current is '_') + { + if (AccIsEmpty) + continue; + + yield return _acc; + Clear(); + continue; + } + + if (char.IsDigit(current)) + { + if (AccIsEmpty || char.IsDigit(memberName[i - 1])) + { + Append(current); + continue; + } + + yield return _acc; + Clear(); + Append(current); + continue; + } + + if (char.IsLetter(current)) + { + if (AccIsEmpty) + { + Append(current); + continue; + } + + if (char.IsDigit(memberName[i - 1]) || StartOfNewWord(current, i) || EndOfCapitalized(current, i)) + { + yield return _acc; + Clear(); + Append(current); + continue; + } + + Append(current); + } + } + + if (!AccIsEmpty) + yield return _acc; + } + + private bool AccIsEmpty => string.IsNullOrWhiteSpace(_acc); + + private void Clear() => _acc = string.Empty; + + private void Append(char chr) => _acc += chr; + + private bool StartOfNewWord(char current, int i) => char.IsUpper(current) && char.IsLower(memberName[i - 1]); + + private bool EndOfCapitalized(char current, int i) => + i + 1 < memberName.Length && char.IsUpper(memberName[i - 1]) && char.IsUpper(current) && char.IsLower(memberName[i + 1]); + } +} diff --git a/test/Riok.Mapperly.Tests/Helpers/MemberNamingUtilTests.cs b/test/Riok.Mapperly.Tests/Helpers/MemberNamingUtilTests.cs new file mode 100644 index 0000000000..755416cb53 --- /dev/null +++ b/test/Riok.Mapperly.Tests/Helpers/MemberNamingUtilTests.cs @@ -0,0 +1,66 @@ +using Riok.Mapperly.Helpers; + +namespace Riok.Mapperly.Tests.Helpers; + +public class MemberNamingUtilTests +{ + [Theory] + [InlineData("OneTwoThree", "oneTwoThree")] + [InlineData("oneTwoThree", "oneTwoThree")] + [InlineData("one_two_three", "oneTwoThree")] + [InlineData("ONE_TWO_THREE", "oneTwoThree")] + [InlineData("OneTWOThree", "oneTwoThree")] + [InlineData("One1Two12Three123", "one1Two12Three123")] + [InlineData("One1TWO12Three123", "one1Two12Three123")] + public void ToCamelCaseTest(string input, string expected) => input.ToCamelCase().Should().Be(expected); + + [Theory] + [InlineData("OneTwoThree", "OneTwoThree")] + [InlineData("oneTwoThree", "OneTwoThree")] + [InlineData("one_two_three", "OneTwoThree")] + [InlineData("ONE_TWO_THREE", "OneTwoThree")] + [InlineData("OneTWOThree", "OneTwoThree")] + [InlineData("One1Two12Three123", "One1Two12Three123")] + [InlineData("One1TWO12Three123", "One1Two12Three123")] + public void ToPascalCaseTest(string input, string expected) => input.ToPascalCase().Should().Be(expected); + + [Theory] + [InlineData("OneTwoThree", "one_two_three")] + [InlineData("oneTwoThree", "one_two_three")] + [InlineData("one_two_three", "one_two_three")] + [InlineData("ONE_TWO_THREE", "one_two_three")] + [InlineData("OneTWOThree", "one_two_three")] + [InlineData("One1Two12Three123", "one_1_two_12_three_123")] + [InlineData("One1TWO12Three123", "one_1_two_12_three_123")] + public void ToSnakeCaseTest(string input, string expected) => input.ToSnakeCase().Should().Be(expected); + + [Theory] + [InlineData("OneTwoThree", "ONE_TWO_THREE")] + [InlineData("oneTwoThree", "ONE_TWO_THREE")] + [InlineData("one_two_three", "ONE_TWO_THREE")] + [InlineData("ONE_TWO_THREE", "ONE_TWO_THREE")] + [InlineData("OneTWOThree", "ONE_TWO_THREE")] + [InlineData("One1Two12Three123", "ONE_1_TWO_12_THREE_123")] + [InlineData("One1TWO12Three123", "ONE_1_TWO_12_THREE_123")] + public void ToUpperSnakeCaseTest(string input, string expected) => input.ToUpperSnakeCase().Should().Be(expected); + + [Theory] + [InlineData("OneTwoThree", "one-two-three")] + [InlineData("oneTwoThree", "one-two-three")] + [InlineData("one_two_three", "one-two-three")] + [InlineData("ONE_TWO_THREE", "one-two-three")] + [InlineData("OneTWOThree", "one-two-three")] + [InlineData("One1Two12Three123", "one-1-two-12-three-123")] + [InlineData("One1TWO12Three123", "one-1-two-12-three-123")] + public void ToKebabCaseTest(string input, string expected) => input.ToKebabCase().Should().Be(expected); + + [Theory] + [InlineData("OneTwoThree", "ONE-TWO-THREE")] + [InlineData("oneTwoThree", "ONE-TWO-THREE")] + [InlineData("one_two_three", "ONE-TWO-THREE")] + [InlineData("ONE_TWO_THREE", "ONE-TWO-THREE")] + [InlineData("OneTWOThree", "ONE-TWO-THREE")] + [InlineData("One1Two12Three123", "ONE-1-TWO-12-THREE-123")] + [InlineData("One1TWO12Three123", "ONE-1-TWO-12-THREE-123")] + public void ToUpperKebabCaseTest(string input, string expected) => input.ToUpperKebabCase().Should().Be(expected); +} diff --git a/test/Riok.Mapperly.Tests/Mapping/EnumNamingStrategyTest.cs b/test/Riok.Mapperly.Tests/Mapping/EnumNamingStrategyTest.cs index 2e0e87fa74..1ccdc249fe 100644 --- a/test/Riok.Mapperly.Tests/Mapping/EnumNamingStrategyTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/EnumNamingStrategyTest.cs @@ -3,10 +3,10 @@ namespace Riok.Mapperly.Tests.Mapping; public class EnumNamingStrategyTest { [Fact] - public void EnumToStringWithPascalCaseNamingStrategy() + public void EnumToStringWithMemberNameNamingStrategy() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[MapEnum(EnumMappingStrategy.ByName, NamingStrategy = EnumNamingStrategy.PascalCase)] public partial string ToStr(E source);", + "[MapEnum(EnumMappingStrategy.ByName, NamingStrategy = EnumNamingStrategy.MemberName)] public partial string ToStr(E source);", "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" ); TestHelper @@ -30,6 +30,62 @@ public void EnumToStringWithPascalCaseNamingStrategy() ); } + [Fact] + public void EnumToStringWithCamelCaseNamingStrategy() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnum(EnumMappingStrategy.ByName, NamingStrategy = EnumNamingStrategy.CamelCase)] public partial string ToStr(E source);", + "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + global::E.Abc => "abc", + global::E.BcD => "bcD", + global::E.C1D => "c1D", + global::E.DEf => "dEf", + global::E.EFG => "efg", + global::E.FG1 => "fg1", + global::E.Gh1 => "gh1", + global::E.Hi_J => "hiJ", + _ => source.ToString(), + }; + """ + ); + } + + [Fact] + public void EnumToStringWithPascalCaseNamingStrategy() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnum(EnumMappingStrategy.ByName, NamingStrategy = EnumNamingStrategy.PascalCase)] public partial string ToStr(E source);", + "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + global::E.Abc => "Abc", + global::E.BcD => "BcD", + global::E.C1D => "C1D", + global::E.DEf => "DEf", + global::E.EFG => "Efg", + global::E.FG1 => "Fg1", + global::E.Gh1 => "Gh1", + global::E.Hi_J => "HiJ", + _ => source.ToString(), + }; + """ + ); + } + [Fact] public void EnumToStringWithSnakeCaseNamingStrategy() { @@ -58,6 +114,34 @@ public void EnumToStringWithSnakeCaseNamingStrategy() ); } + [Fact] + public void EnumToStringWithUpperSnakeCaseNamingStrategy() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnum(EnumMappingStrategy.ByName, NamingStrategy = EnumNamingStrategy.UpperSnakeCase)] public partial string ToStr(E source);", + "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + global::E.Abc => "ABC", + global::E.BcD => "BC_D", + global::E.C1D => "C_1_D", + global::E.DEf => "D_EF", + global::E.EFG => "EFG", + global::E.FG1 => "FG_1", + global::E.Gh1 => "GH_1", + global::E.Hi_J => "HI_J", + _ => source.ToString(), + }; + """ + ); + } + [Fact] public void EnumToStringWithKebabCaseNamingStrategy() { @@ -87,10 +171,38 @@ public void EnumToStringWithKebabCaseNamingStrategy() } [Fact] - public void EnumFromStringWithPascalCaseNamingStrategy() + public void EnumToStringWithUpperKebabCaseNamingStrategy() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "[MapEnum(EnumMappingStrategy.ByName, NamingStrategy = EnumNamingStrategy.PascalCase)] public partial E ToEnum(string source);", + "[MapEnum(EnumMappingStrategy.ByName, NamingStrategy = EnumNamingStrategy.UpperKebabCase)] public partial string ToStr(E source);", + "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + global::E.Abc => "ABC", + global::E.BcD => "BC-D", + global::E.C1D => "C-1-D", + global::E.DEf => "D-EF", + global::E.EFG => "EFG", + global::E.FG1 => "FG-1", + global::E.Gh1 => "GH-1", + global::E.Hi_J => "HI-J", + _ => source.ToString(), + }; + """ + ); + } + + [Fact] + public void EnumFromStringWithMemberNameNamingStrategy() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnum(EnumMappingStrategy.ByName, NamingStrategy = EnumNamingStrategy.MemberName)] public partial E ToEnum(string source);", "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" ); TestHelper @@ -114,6 +226,62 @@ public void EnumFromStringWithPascalCaseNamingStrategy() ); } + [Fact] + public void EnumFromStringWithCamelCaseNamingStrategy() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnum(EnumMappingStrategy.ByName, NamingStrategy = EnumNamingStrategy.CamelCase)] public partial E ToEnum(string source);", + "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + "abc" => global::E.Abc, + "bcD" => global::E.BcD, + "c1D" => global::E.C1D, + "dEf" => global::E.DEf, + "efg" => global::E.EFG, + "fg1" => global::E.FG1, + "gh1" => global::E.Gh1, + "hiJ" => global::E.Hi_J, + _ => System.Enum.Parse(source, false), + }; + """ + ); + } + + [Fact] + public void EnumFromStringWithPascalCaseNamingStrategy() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnum(EnumMappingStrategy.ByName, NamingStrategy = EnumNamingStrategy.PascalCase)] public partial E ToEnum(string source);", + "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + "Abc" => global::E.Abc, + "BcD" => global::E.BcD, + "C1D" => global::E.C1D, + "DEf" => global::E.DEf, + "Efg" => global::E.EFG, + "Fg1" => global::E.FG1, + "Gh1" => global::E.Gh1, + "HiJ" => global::E.Hi_J, + _ => System.Enum.Parse(source, false), + }; + """ + ); + } + [Fact] public void EnumFromStringWithSnakeCaseNamingStrategy() { @@ -142,6 +310,34 @@ public void EnumFromStringWithSnakeCaseNamingStrategy() ); } + [Fact] + public void EnumFromStringWithUpperSnakeCaseNamingStrategy() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnum(EnumMappingStrategy.ByName, NamingStrategy = EnumNamingStrategy.UpperSnakeCase)] public partial E ToEnum(string source);", + "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + "ABC" => global::E.Abc, + "BC_D" => global::E.BcD, + "C_1_D" => global::E.C1D, + "D_EF" => global::E.DEf, + "EFG" => global::E.EFG, + "FG_1" => global::E.FG1, + "GH_1" => global::E.Gh1, + "HI_J" => global::E.Hi_J, + _ => System.Enum.Parse(source, false), + }; + """ + ); + } + [Fact] public void EnumFromStringWithKebabCaseNamingStrategy() { @@ -169,4 +365,87 @@ public void EnumFromStringWithKebabCaseNamingStrategy() """ ); } + + [Fact] + public void EnumFromStringWithUpperKebabCaseNamingStrategy() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnum(EnumMappingStrategy.ByName, NamingStrategy = EnumNamingStrategy.UpperKebabCase)] public partial E ToEnum(string source);", + "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + "ABC" => global::E.Abc, + "BC-D" => global::E.BcD, + "C-1-D" => global::E.C1D, + "D-EF" => global::E.DEf, + "EFG" => global::E.EFG, + "FG-1" => global::E.FG1, + "GH-1" => global::E.Gh1, + "HI-J" => global::E.Hi_J, + _ => System.Enum.Parse(source, false), + }; + """ + ); + } + + [Fact] + public void EnumToStringWithFallbackValue() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnum(EnumMappingStrategy.ByName, NamingStrategy = EnumNamingStrategy.SnakeCase, FallbackValue = \"default\")] public partial string ToStr(E source);", + "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + global::E.Abc => "abc", + global::E.BcD => "bc_d", + global::E.C1D => "c_1_d", + global::E.DEf => "d_ef", + global::E.EFG => "efg", + global::E.FG1 => "fg_1", + global::E.Gh1 => "gh_1", + global::E.Hi_J => "hi_j", + _ => "default", + }; + """ + ); + } + + [Fact] + public void EnumFromStringWithFallbackValue() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnum(EnumMappingStrategy.ByName, NamingStrategy = EnumNamingStrategy.SnakeCase, FallbackValue = E.DEf)] public partial E ToEnum(string source);", + "public enum E {Abc, BcD, C1D, DEf, EFG, FG1, Gh1, Hi_J}" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ + return source switch + { + "abc" => global::E.Abc, + "bc_d" => global::E.BcD, + "c_1_d" => global::E.C1D, + "efg" => global::E.EFG, + "fg_1" => global::E.FG1, + "gh_1" => global::E.Gh1, + "hi_j" => global::E.Hi_J, + _ => global::E.DEf, + }; + """ + ); + } }