Skip to content

Commit

Permalink
fix: generate generic mapping only for matching source / target types…
Browse files Browse the repository at this point in the history
… if non-generic (#1000)
  • Loading branch information
trejjam committed Dec 14, 2023
1 parent 0aa8b0b commit 9a2916f
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 5 deletions.
9 changes: 5 additions & 4 deletions docs/docs/configuration/generic-mapping.md
Expand Up @@ -7,22 +7,23 @@ description: Create a generic mapping method

Mapperly supports generic user defined mapping methods.
Mapperly implements this mapping method
using all mappings the user defined in the mapper.
using all mappings the user defined in the mapper that satisfy the source and target type constraints.

```csharp
[Mapper]
public static partial class ModelMapper
{
// highlight-start
public static partial TTarget Map<TTarget>(object source);
public static partial TTarget MapFruit<TTarget>(Fruit source);
// highlight-end
private static partial BananaDto MapBanana(Banana source);
private static partial AppleDto MapApple(Apple source);
}

class Banana {}
class Apple {}
class Fruit {}
class Banana : Fruit {}
class Apple : Fruit {}

class BananaDto {}
class AppleDto {}
Expand Down
@@ -1,3 +1,4 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Descriptors.MappingBuilders;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.Mappings.UserMappings;
Expand All @@ -16,14 +17,24 @@ public static void BuildMappingBody(MappingBuilderContext ctx, UserDefinedNewIns
var mappings = GetUserMappingCandidates(ctx)
.Where(
x =>
mapping
DoesTypesSatisfySubstitutionPrinciples(mapping, ctx.SymbolAccessor, x.SourceType.NonNullable(), x.TargetType)
&& mapping
.TypeParameters
.DoesTypesSatisfyTypeParameterConstraints(ctx.SymbolAccessor, x.SourceType.NonNullable(), x.TargetType)
);

BuildMappingBody(ctx, mapping, mappings);
}

private static bool DoesTypesSatisfySubstitutionPrinciples(
IMapping mapping,
SymbolAccessor symbolAccessor,
ITypeSymbol sourceType,
ITypeSymbol targetType
) =>
(mapping.SourceType.TypeKind == TypeKind.TypeParameter || symbolAccessor.HasImplicitConversion(sourceType, mapping.SourceType))
&& (mapping.TargetType.TypeKind == TypeKind.TypeParameter || symbolAccessor.HasImplicitConversion(targetType, mapping.TargetType));

public static void BuildMappingBody(MappingBuilderContext ctx, UserDefinedNewInstanceRuntimeTargetTypeMapping mapping)
{
// source nulls are filtered out by the type switch arms,
Expand Down
70 changes: 70 additions & 0 deletions test/Riok.Mapperly.Tests/Mapping/GenericTest.cs
Expand Up @@ -303,6 +303,41 @@ public void WithGenericSourceAndTargetTypeNullableReferenceTypeConstraint()
);
}

[Fact]
public void WithGenericSourceSpecificTarget()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"""
partial BaseDto Map<TSource>(TSource source);

partial C MapToC(A source);
partial D MapToD(B source);
partial MyEnum MapToMyEnum(DtoEnum source);
""",
"record A(string BaseValue);",
"record B(string BaseValue);",
"abstract record BaseDto(string BaseValue);",
"record C(string BaseValue) : BaseDto(BaseValue);",
"record D(string BaseValue) : BaseDto(BaseValue);",
"enum DtoEnum;",
"enum MyEnum;"
);
TestHelper
.GenerateMapper(source)
.Should()
.HaveMapMethodBody(
"""
return source switch
{
global::A x => MapToC(x),
global::B x => MapToD(x),
null => throw new System.ArgumentNullException(nameof(source)),
_ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(global::BaseDto)} as there is no known type mapping", nameof(source)),
};
"""
);
}

[Fact]
public void WithGenericTarget()
{
Expand Down Expand Up @@ -365,6 +400,41 @@ public void WithGenericTargetTypeConstraints()
);
}

[Fact]
public void WithGenericTargetSpecificSource()
{
var source = TestSourceBuilder.MapperWithBodyAndTypes(
"""
partial TTarget Map<TTarget>(BaseDto source);

partial C MapToC(A source);
partial D MapToD(B source);
partial MyEnum MapToMyEnum(DtoEnum source);
""",
"abstract record BaseDto(string BaseValue);",
"record A(string BaseValue) : BaseDto(BaseValue);",
"record B(string BaseValue) : BaseDto(BaseValue);",
"record C(string BaseValue);",
"record D(string BaseValue);",
"enum DtoEnum;",
"enum MyEnum;"
);
TestHelper
.GenerateMapper(source)
.Should()
.HaveMapMethodBody(
"""
return source switch
{
global::A x when typeof(TTarget).IsAssignableFrom(typeof(global::C)) => (TTarget)(object)MapToC(x),
global::B x when typeof(TTarget).IsAssignableFrom(typeof(global::D)) => (TTarget)(object)MapToD(x),
null => throw new System.ArgumentNullException(nameof(source)),
_ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)),
};
"""
);
}

[Fact]
public void WithGenericSourceAndTargetTypeConstraints()
{
Expand Down

0 comments on commit 9a2916f

Please sign in to comment.