Thanks to visit codestin.com
Credit goes to github.com

Skip to content
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
29 changes: 8 additions & 21 deletions docs/docs/configuration/mapper.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -209,29 +209,16 @@ or by setting the `IgnoreObsoleteMembersStrategy` option of the `MapperAttribute
### Property name mapping strategy

By default, property and field names are matched using a case sensitive strategy.
If all properties/fields differ only in casing, for example `ModelName` on the source
and `modelName` on the target,
the `MapperAttribute` can be used with the `PropertyNameMappingStrategy` option.
If properties/fields differ in naming conventions, the `MapperAttribute` can be used with the `PropertyNameMappingStrategy` option.

```csharp
// highlight-start
[Mapper(PropertyNameMappingStrategy = PropertyNameMappingStrategy.CaseInsensitive)]
// highlight-end
public partial class CarMapper
{
public partial CarDto ToDto(Car car);
}

public class Car
{
public string ModelName { get; set; }
}
Available strategies:

public class CarDto
{
public string modelName { get; set; }
}
```
| Name | Description |
| --------------- | ------------------------------------------------------------------------ |
| CaseSensitive | Matches properties by their exact name (default) |
| CaseInsensitive | Matches properties ignoring case differences |
| SnakeCase | Matches properties by converting to `snake_case` before comparison |
| UpperSnakeCase | Matches properties by converting to `UPPER_SNAKE_CASE` before comparison |

### `null` values

Expand Down
12 changes: 12 additions & 0 deletions src/Riok.Mapperly.Abstractions/PropertyNameMappingStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,16 @@ public enum PropertyNameMappingStrategy
/// Matches a property by its name in case insensitive manner.
/// </summary>
CaseInsensitive,

/// <summary>
/// Matches a property by converting both source and target property names to snake_case before comparison.
/// For example, "FirstName" would match "first_name".
/// </summary>
SnakeCase,

/// <summary>
/// Matches a property by converting both source and target property names to SNAKE_CASE before comparison.
/// For example, "FirstName" would match "FIRST_NAME".
/// </summary>
UpperSnakeCase,
}
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,10 @@ private bool TryFindSourcePath(
bool? ignoreCase = null
)
{
ignoreCase ??= BuilderContext.Configuration.Mapper.PropertyNameMappingStrategy == PropertyNameMappingStrategy.CaseInsensitive;
var pathCandidates = MemberPathCandidateBuilder.BuildMemberPathCandidates(targetMemberName);
var strategy = BuilderContext.Configuration.Mapper.PropertyNameMappingStrategy;
ignoreCase ??= strategy == PropertyNameMappingStrategy.CaseInsensitive;

var pathCandidates = MemberPathCandidateBuilder.BuildMemberPathCandidates(targetMemberName, strategy);

// First, try to find the property on (a sub-path of) the source type itself. (If this is undesired, an Ignore property can be used.)
if (TryFindSourcePath(pathCandidates, ignoreCase.Value, out sourceMemberPath))
Expand Down
63 changes: 45 additions & 18 deletions src/Riok.Mapperly/Descriptors/MemberPathCandidateBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Configuration.PropertyReferences;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Descriptors;

Expand All @@ -7,36 +9,61 @@ public static class MemberPathCandidateBuilder
/// <summary>
/// Maximum number of indices which are considered to compute the member path candidates.
/// </summary>
private const int MaxPascalCaseIndices = 8;
private const int MaxPermutationIndices = 8;

/// <summary>
/// Splits a name into pascal case chunks and joins them together in all possible combinations.
/// Splits a name into chunks and joins them together in all possible combinations.
/// <example><c>"MyValueId"</c> leads to <c>[["MyValueId"], ["My", "ValueId"], ["MyValue", "Id"], ["My", "Value", "Id"]</c></example>
/// </summary>
/// <param name="name">The name to build candidates from.</param>
/// <param name="strategy">The naming strategy to use.</param>
/// <returns>The joined member path groups.</returns>
public static IEnumerable<StringMemberPath> BuildMemberPathCandidates(string name)
public static IEnumerable<StringMemberPath> BuildMemberPathCandidates(string name, PropertyNameMappingStrategy strategy)
{
if (name.Length == 0)
yield break;
if (string.IsNullOrEmpty(name))
return [];

// yield full string
// as a fast path (often member match by their exact name)
yield return new StringMemberPath([name]);
return BuildCandidates(name, strategy).Prepend(new StringMemberPath([name])).DistinctBy(x => x.FullName);
}

private static IEnumerable<StringMemberPath> BuildCandidates(string name, PropertyNameMappingStrategy strategy)
{
return strategy switch
{
PropertyNameMappingStrategy.SnakeCase => BuildSnakeCaseCandidates(name, name.ToSnakeCase()),
PropertyNameMappingStrategy.UpperSnakeCase => BuildSnakeCaseCandidates(name, name.ToUpperSnakeCase()),
_ => BuildPermutations(name, char.IsUpper, skipSeparator: false),
};
}

var indices = GetPascalCaseSplitIndices(name).Take(MaxPascalCaseIndices).ToArray();
if (indices.Length == 0)
yield break;
private static IEnumerable<StringMemberPath> BuildSnakeCaseCandidates(string originalName, string snakeCaseName)
{
var snakeCasePermutations = BuildPermutations(snakeCaseName, static c => c == '_', skipSeparator: true);

// PascalCase Fallback
var pascalCaseSource = originalName.Contains('_', StringComparison.Ordinal) ? originalName.ToPascalCase() : originalName;
var pascalCasePermutations = BuildCandidates(pascalCaseSource, PropertyNameMappingStrategy.CaseSensitive);
return snakeCasePermutations.Concat(pascalCasePermutations);
}

private static IEnumerable<StringMemberPath> BuildPermutations(string name, Func<char, bool> isSeparator, bool skipSeparator)
{
var indices = GetSplitIndices(name, isSeparator).Take(MaxPermutationIndices).ToArray();

// try all permutations, skipping the first because the full string is already yielded
// try all permutations
var permutationsCount = 1 << indices.Length;
for (var i = 1; i < permutationsCount; i++)
for (var i = 0; i < permutationsCount; i++)
{
yield return new StringMemberPath(BuildPermutationParts(name, indices, i));
yield return new StringMemberPath(BuildPermutationParts(name, indices, i, skipSeparator));
}
}

private static IEnumerable<string> BuildPermutationParts(string source, int[] splitIndices, int enabledSplitPositions)
private static IEnumerable<string> BuildPermutationParts(
string source,
int[] splitIndices,
int enabledSplitPositions,
bool skipSplitIndex
)
{
var lastSplitIndex = 0;
var currentSplitPosition = 1;
Expand All @@ -45,7 +72,7 @@ private static IEnumerable<string> BuildPermutationParts(string source, int[] sp
if ((enabledSplitPositions & currentSplitPosition) == currentSplitPosition)
{
yield return source.Substring(lastSplitIndex, splitIndex - lastSplitIndex);
lastSplitIndex = splitIndex;
lastSplitIndex = splitIndex + (skipSplitIndex ? 1 : 0);
}

currentSplitPosition <<= 1;
Expand All @@ -55,11 +82,11 @@ private static IEnumerable<string> BuildPermutationParts(string source, int[] sp
yield return source.Substring(lastSplitIndex);
}

private static IEnumerable<int> GetPascalCaseSplitIndices(string str)
private static IEnumerable<int> GetSplitIndices(string str, Func<char, bool> isSeparator)
{
for (var i = 1; i < str.Length; i++)
{
if (char.IsUpper(str[i]))
if (isSeparator(str[i]))
yield return i;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ public enum PropertyNameMappingStrategy
{
CaseSensitive = 0,
CaseInsensitive = 1,
SnakeCase = 2,
UpperSnakeCase = 3,
}
[System.Flags]
public enum RequiredMappingStrategy
Expand Down
144 changes: 132 additions & 12 deletions test/Riok.Mapperly.Tests/Descriptors/MemberPathCandidateBuilderTest.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,70 @@
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors;

namespace Riok.Mapperly.Tests.Descriptors;

public class MemberPathCandidateBuilderTest
{
[Theory]
[InlineData("", new string[] { })]
[InlineData("a", new[] { "a" })]
[InlineData("A", new[] { "A" })]
[InlineData("aA", new[] { "aA", "a.A" })]
[InlineData("AB", new[] { "AB", "A.B" })]
[InlineData("Value", new[] { "Value" })]
[InlineData("MyValue", new[] { "MyValue", "My.Value" })]
[InlineData("MyValueId", new[] { "MyValueId", "My.ValueId", "MyValue.Id", "My.Value.Id" })]
[InlineData("", new string[] { }, new string[] { }, new string[] { })]
[InlineData("a", new[] { "a" }, new[] { "a" }, new[] { "a", "A" })]
[InlineData("aa", new[] { "aa" }, new[] { "aa" }, new[] { "aa", "AA" })]
[InlineData("A", new[] { "A" }, new[] { "A", "a" }, new[] { "A" })]
[InlineData("aA", new[] { "aA", "a.A" }, new[] { "aA", "a_a", "a.a", "a.A" }, new[] { "aA", "A_A", "A.A", "a.A" })]
[InlineData(
"aAA",
new[] { "aAA", "a.AA", "aA.A", "a.A.A" },
new[] { "aAA", "a_aa", "a.aa", "a.AA", "aA.A", "a.A.A" },
new[] { "aAA", "A_AA", "A.AA", "a.AA", "aA.A", "a.A.A" }
)]
[InlineData("AB", new[] { "AB", "A.B" }, new[] { "AB", "ab", "A.B" }, new[] { "AB", "A.B" })]
[InlineData("Value", new[] { "Value" }, new[] { "Value", "value" }, new[] { "Value", "VALUE" })]
[InlineData(
"MyValue",
new[] { "MyValue", "My.Value" },
new[] { "MyValue", "my_value", "my.value", "My.Value" },
new[] { "MyValue", "MY_VALUE", "MY.VALUE", "My.Value" }
)]
[InlineData(
"MyValueId",
new[] { "MyValueId", "My.ValueId", "MyValue.Id", "My.Value.Id" },
new[] { "MyValueId", "my_value_id", "my.value_id", "my_value.id", "my.value.id", "My.ValueId", "MyValue.Id", "My.Value.Id" },
new[] { "MyValueId", "MY_VALUE_ID", "MY.VALUE_ID", "MY_VALUE.ID", "MY.VALUE.ID", "My.ValueId", "MyValue.Id", "My.Value.Id" }
)]
[InlineData(
"my_value_id",
new[] { "my_value_id" },
new[] { "my_value_id", "my.value_id", "my_value.id", "my.value.id", "MyValueId", "My.ValueId", "MyValue.Id", "My.Value.Id" },
new[]
{
"my_value_id",
"MY_VALUE_ID",
"MY.VALUE_ID",
"MY_VALUE.ID",
"MY.VALUE.ID",
"MyValueId",
"My.ValueId",
"MyValue.Id",
"My.Value.Id",
}
)]
[InlineData(
"MY_VALUE_ID",
null,
new[]
{
"MY_VALUE_ID",
"my_value_id",
"my.value_id",
"my_value.id",
"my.value.id",
"MyValueId",
"My.ValueId",
"MyValue.Id",
"My.Value.Id",
},
new[] { "MY_VALUE_ID", "MY.VALUE_ID", "MY_VALUE.ID", "MY.VALUE.ID", "MyValueId", "My.ValueId", "MyValue.Id", "My.Value.Id" }
)]
[InlineData(
"MyValueIdNum",
new[]
Expand All @@ -25,16 +77,84 @@ public class MemberPathCandidateBuilderTest
"My.ValueId.Num",
"MyValue.Id.Num",
"My.Value.Id.Num",
},
new[]
{
"MyValueIdNum",
"my_value_id_num",
"my.value_id_num",
"my_value.id_num",
"my.value.id_num",
"my_value_id.num",
"my.value_id.num",
"my_value.id.num",
"my.value.id.num",
"My.ValueIdNum",
"MyValue.IdNum",
"My.Value.IdNum",
"MyValueId.Num",
"My.ValueId.Num",
"MyValue.Id.Num",
"My.Value.Id.Num",
},
new[]
{
"MyValueIdNum",
"MY_VALUE_ID_NUM",
"MY.VALUE_ID_NUM",
"MY_VALUE.ID_NUM",
"MY.VALUE.ID_NUM",
"MY_VALUE_ID.NUM",
"MY.VALUE_ID.NUM",
"MY_VALUE.ID.NUM",
"MY.VALUE.ID.NUM",
"My.ValueIdNum",
"MyValue.IdNum",
"My.Value.IdNum",
"MyValueId.Num",
"My.ValueId.Num",
"MyValue.Id.Num",
"My.Value.Id.Num",
}
)]
public void BuildMemberPathCandidatesShouldWork(string name, string[] chunks)
public void BuildMemberPathCandidatesShouldWork(
string name,
string[]? caseSensitiveChunks,
string[]? snakeCaseChunks,
string[]? upperSnakeCaseChunks
)
{
MemberPathCandidateBuilder.BuildMemberPathCandidates(name).Select(x => x.FullName).ShouldBe(chunks);
if (caseSensitiveChunks != null)
{
MemberPathCandidateBuilder
.BuildMemberPathCandidates(name, PropertyNameMappingStrategy.CaseSensitive)
.Select(x => x.FullName)
.ShouldBe(caseSensitiveChunks);
}

if (snakeCaseChunks != null)
{
MemberPathCandidateBuilder
.BuildMemberPathCandidates(name, PropertyNameMappingStrategy.SnakeCase)
.Select(x => x.FullName)
.ShouldBe(snakeCaseChunks);
}

if (upperSnakeCaseChunks != null)
{
MemberPathCandidateBuilder
.BuildMemberPathCandidates(name, PropertyNameMappingStrategy.UpperSnakeCase)
.Select(x => x.FullName)
.ShouldBe(upperSnakeCaseChunks);
}
}

[Fact]
public void BuildMemberPathCandidatesWithPascalCaseShouldLimitPermutations()
public void BuildMemberPathCandidatesShouldLimitPermutations()
{
MemberPathCandidateBuilder.BuildMemberPathCandidates("NOT_A_PASCAL_CASE_STRING").Count().ShouldBe(256);
MemberPathCandidateBuilder
.BuildMemberPathCandidates("NOT_A_PASCAL_CASE_STRING", PropertyNameMappingStrategy.CaseSensitive)
.Count()
.ShouldBe(256);
}
}
Loading