Skip to content

Commit

Permalink
feat: support for enum naming strategy mappings
Browse files Browse the repository at this point in the history
  • Loading branch information
BeeTwin committed Sep 5, 2024
1 parent 959e194 commit f3d70e4
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 93 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using System.Globalization;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.Mappings;
Expand All @@ -23,53 +21,9 @@ public static class EnumToStringMappingBuilder

private static EnumToStringMapping BuildEnumToStringMapping(MappingBuilderContext ctx)
{
var customNameMappings = BuildCustomNameStrategyMappings(ctx);
var customNameMappings = ctx.Source.BuildCustomNameStrategyMappings(ctx.Configuration.Enum.NamingStrategy);
// 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);
}

private static Dictionary<IFieldSymbol, string> BuildCustomNameStrategyMappings(MappingBuilderContext ctx)
{
var values = ctx.Source.GetMembers().OfType<IFieldSymbol>();
var customNameMappings = new Dictionary<IFieldSymbol, string>(SymbolEqualityComparer.Default);
foreach (var value in values)
{
var valueString = ConvertEnumValueNameToString(value.Name, ctx.Configuration.Enum.NamingStrategy);
customNameMappings.Add(value, valueString);
}

return customNameMappings;
}

private static string ConvertEnumValueNameToString(string enumValueName, EnumNamingStrategy namingStrategy)
{
var enumValueNameWords = SplitEnumValueNameIntoWords(enumValueName);
return namingStrategy switch
{
EnumNamingStrategy.PascalCase => JoinWords("", enumValueNameWords),
EnumNamingStrategy.SnakeCase => JoinWords("_", enumValueNameWords, toLower: true),
EnumNamingStrategy.KebabCase => JoinWords("-", enumValueNameWords, toLower: true),
_ => 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 JoinWords(string separator, string[] words, bool toLower = false) =>
string.Join(separator, toLower ? words.Select(w => w.ToLower(CultureInfo.InvariantCulture)) : words);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public static class StringToEnumMappingBuilder
if (ctx.Source.SpecialType != SpecialType.System_String || !ctx.Target.IsEnum())
return null;

return BuildEnumToStringMapping(ctx);
}

private static INewInstanceMapping BuildEnumToStringMapping(MappingBuilderContext ctx)
{
var genericEnumParseMethodSupported = ctx
.Types.Get<Enum>()
.GetMembers(nameof(Enum.Parse))
Expand All @@ -33,6 +38,8 @@ public static class StringToEnumMappingBuilder
);
}

var customNameMappings = ctx.Target.BuildCustomNameStrategyMappings(ctx.Configuration.Enum.NamingStrategy);

// 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.
Expand All @@ -44,7 +51,14 @@ public static class StringToEnumMappingBuilder
members = members.Where(x => fallbackMapping.FallbackMember.ConstantValue?.Equals(x.ConstantValue) != true);
}

return new EnumFromStringSwitchMapping(ctx.Source, ctx.Target, members, ctx.Configuration.Enum.IgnoreCase, fallbackMapping);
return new EnumFromStringSwitchMapping(
ctx.Source,
ctx.Target,
members,
ctx.Configuration.Enum.IgnoreCase,
fallbackMapping,
customNameMappings
);
}

private static EnumFallbackValueMapping BuildFallbackParseMapping(MappingBuilderContext ctx, bool genericEnumParseMethodSupported)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ public class EnumFromStringSwitchMapping(
ITypeSymbol targetType,
IEnumerable<IFieldSymbol> enumMembers,
bool ignoreCase,
EnumFallbackValueMapping fallbackMapping
EnumFallbackValueMapping fallbackMapping,
Dictionary<IFieldSymbol, string> customNameMappings
) : NewInstanceMethodMapping(sourceType, targetType)
{
private const string IgnoreCaseSwitchDesignatedVariableName = "s";
Expand Down Expand Up @@ -50,11 +51,16 @@ private SwitchExpressionArmSyntax BuildArmIgnoreCase(string ignoreCaseSwitchDesi
// source.Value1
var typeMemberAccess = MemberAccess(field.ContainingType.NonNullable().FullyQualifiedIdentifierName(), field.Name);

// nameof(source.Value1)
// or
// "value_1" (if custom naming mapping exists)
ExpressionSyntax expression = customNameMappings.TryGetValue(field, out var str) ? StringLiteral(str) : NameOf(typeMemberAccess);

// when s.Equals(nameof(source.Value1), StringComparison.OrdinalIgnoreCase)
var whenClause = SwitchWhen(
InvocationWithoutIndention(
MemberAccess(ignoreCaseSwitchDesignatedVariableName, StringEqualsMethodName),
NameOf(typeMemberAccess),
expression,
IdentifierName(StringComparisonFullName)
)
);
Expand All @@ -66,8 +72,13 @@ private SwitchExpressionArmSyntax BuildArmIgnoreCase(string ignoreCaseSwitchDesi
private SwitchExpressionArmSyntax BuildArm(IFieldSymbol field)
{
// nameof(source.Value1) => source.Value1;
// or
// "value_1" => source.Value1 (if custom naming mapping exists)
var typeMemberAccess = MemberAccess(FullyQualifiedIdentifier(field.ContainingType), field.Name);
var pattern = ConstantPattern(NameOf(typeMemberAccess));

ExpressionSyntax expression = customNameMappings.TryGetValue(field, out var str) ? StringLiteral(str) : NameOf(typeMemberAccess);

var pattern = ConstantPattern(expression);
return SwitchArm(pattern, typeMemberAccess);
}
}
59 changes: 59 additions & 0 deletions src/Riok.Mapperly/Helpers/EnumNamingStrategyHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.Globalization;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;

namespace Riok.Mapperly.Helpers;

public static class EnumNamingStrategyHelper
{
public static Dictionary<IFieldSymbol, string> BuildCustomNameStrategyMappings(
this ITypeSymbol enumSymbol,
EnumNamingStrategy namingStrategy
)
{
var customNameMappings = new Dictionary<IFieldSymbol, string>(SymbolEqualityComparer.Default);

var values = enumSymbol.GetMembers().OfType<IFieldSymbol>();
foreach (var value in values)
{
var valueString = ConvertEnumValueNameToString(value.Name, namingStrategy);
customNameMappings.Add(value, valueString);
}

return customNameMappings;
}

private static string ConvertEnumValueNameToString(string enumValueName, EnumNamingStrategy namingStrategy)
{
return namingStrategy switch
{
EnumNamingStrategy.PascalCase => enumValueName,
EnumNamingStrategy.SnakeCase => JoinWordsFrom("_", enumValueName),
EnumNamingStrategy.KebabCase => JoinWordsFrom("-", enumValueName),
_ => 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -588,9 +588,9 @@ private string MapToString(global::Riok.Mapperly.IntegrationTests.Models.TestEnu
{
return source switch
{
global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10 => nameof(global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10),
global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20 => nameof(global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20),
global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30 => nameof(global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30),
global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10 => "Value10",
global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20 => "Value20",
global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30 => "Value30",
_ => source.ToString(),
};
}
Expand All @@ -600,9 +600,9 @@ private string MapToString(global::Riok.Mapperly.IntegrationTests.Models.TestEnu
{
return source switch
{
nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue1) => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue1,
nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue2) => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue2,
nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue3) => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue3,
"DtoValue1" => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue1,
"DtoValue2" => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue2,
"DtoValue3" => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue3,
_ => System.Enum.Parse<global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue>(source, false),
};
}
Expand Down Expand Up @@ -658,9 +658,9 @@ private string[] MapToStringArray(global::System.ReadOnlySpan<int> source)
{
return source switch
{
nameof(global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10) => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10,
nameof(global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20) => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20,
nameof(global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30) => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30,
"Value10" => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10,
"Value20" => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20,
"Value30" => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30,
_ => System.Enum.Parse<global::Riok.Mapperly.IntegrationTests.Models.TestEnum>(source, false),
};
}
Expand All @@ -670,9 +670,9 @@ private string MapToString1(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumD
{
return source switch
{
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue1 => nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue1),
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue2 => nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue2),
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue3 => nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue3),
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue1 => "DtoValue1",
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue2 => "DtoValue2",
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue3 => "DtoValue3",
_ => source.ToString(),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,9 @@ private static string MapToString(global::Riok.Mapperly.IntegrationTests.Models.
{
return source switch
{
global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10 => nameof(global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10),
global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20 => nameof(global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20),
global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30 => nameof(global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30),
global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10 => "Value10",
global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20 => "Value20",
global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30 => "Value30",
_ => source.ToString(),
};
}
Expand All @@ -226,9 +226,9 @@ private static string MapToString(global::Riok.Mapperly.IntegrationTests.Models.
{
return source switch
{
nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName.Value10) => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName.Value10,
nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName.Value20) => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName.Value20,
nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName.Value30) => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName.Value30,
"Value10" => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName.Value10,
"Value20" => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName.Value20,
"Value30" => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName.Value30,
_ => System.Enum.Parse<global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName>(source, false),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -938,9 +938,9 @@ private static string MapToString(global::Riok.Mapperly.IntegrationTests.Models.
{
return source switch
{
global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10 => nameof(global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10),
global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20 => nameof(global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20),
global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30 => nameof(global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30),
global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10 => "Value10",
global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20 => "Value20",
global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30 => "Value30",
_ => source.ToString(),
};
}
Expand All @@ -950,9 +950,9 @@ private static string MapToString(global::Riok.Mapperly.IntegrationTests.Models.
{
return source switch
{
nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue1) => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue1,
nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue2) => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue2,
nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue3) => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue3,
"DtoValue1" => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue1,
"DtoValue2" => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue2,
"DtoValue3" => global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue3,
_ => System.Enum.Parse<global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue>(source, false),
};
}
Expand Down Expand Up @@ -1008,9 +1008,9 @@ private static string[] MapToStringArray(global::System.ReadOnlySpan<int> source
{
return source switch
{
nameof(global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10) => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10,
nameof(global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20) => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20,
nameof(global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30) => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30,
"Value10" => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value10,
"Value20" => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value20,
"Value30" => global::Riok.Mapperly.IntegrationTests.Models.TestEnum.Value30,
_ => System.Enum.Parse<global::Riok.Mapperly.IntegrationTests.Models.TestEnum>(source, false),
};
}
Expand All @@ -1020,9 +1020,9 @@ private static string MapToString1(global::Riok.Mapperly.IntegrationTests.Dto.Te
{
return source switch
{
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue1 => nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue1),
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue2 => nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue2),
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue3 => nameof(global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue3),
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue1 => "DtoValue1",
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue2 => "DtoValue2",
global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByValue.DtoValue3 => "DtoValue3",
_ => source.ToString(),
};
}
Expand Down
Loading

0 comments on commit f3d70e4

Please sign in to comment.