Skip to content

Commit b17f666

Browse files
authored
feat: accept destination type as mapping method parameter (#398)
1 parent ebff0af commit b17f666

File tree

44 files changed

+1004
-158
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1004
-158
lines changed

docs/docs/01-getting-started/03-generated-mapper-example.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import GeneratedCarMapperSource from '!!raw-loader!../../src/data/generated/samp
88

99
This example will show you what kind of code Mapperly generates.
1010
It is based on the [Mapperly sample](https://github.com/riok/mapperly/tree/main/samples/Riok.Mapperly.Sample).
11-
To view the generated code of your own mapper, refer to the [generated source configuration](../02-configuration/15-generated-source.mdx).
11+
To view the generated code of your own mapper, refer to the [generated source configuration](../02-configuration/16-generated-source.mdx).
1212

1313
## The source classes
1414

docs/docs/02-configuration/01-mapper.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ To enforce strict mappings
8484
(all source members have to be mapped to a target member
8585
and all target members have to be mapped from a source member,
8686
except for ignored members)
87-
set the following two EditorConfig settings (see also [analyzer diagnostics](./14-analyzer-diagnostics.mdx)):
87+
set the following two EditorConfig settings (see also [analyzer diagnostics](./15-analyzer-diagnostics.mdx)):
8888

8989
```editorconfig title=".editorconfig"
9090
[*.cs]
@@ -94,4 +94,4 @@ dotnet_diagnostic.RMG020.severity = error # Unmapped source member
9494

9595
### Strict enum mappings
9696

97-
To enforce strict enum mappings set 'RMG037' and 'RMG038' to error, see [strict enum mappings](./04-enum.mdx).
97+
To enforce strict enum mappings set `RMG037` and `RMG038` to error, see [strict enum mappings](./04-enum.mdx).

docs/docs/02-configuration/04-enum.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public partial class CarMapper
5757
To enforce strict enum mappings
5858
(all source enum values have to be mapped to a target enum value
5959
and all target enum values have to be mapped from a source enum value)
60-
set the following two EditorConfig settings (see also [analyzer diagnostics](./14-analyzer-diagnostics.mdx)):
60+
set the following two EditorConfig settings (see also [analyzer diagnostics](./15-analyzer-diagnostics.mdx)):
6161

6262
```editorconfig title=".editorconfig"
6363
[*.cs]

docs/docs/02-configuration/10-derived-type-mapping.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,19 @@ This can be configured with the `MapDerivedTypeAttribute`:
1717
public static partial class ModelMapper
1818
{
1919
// highlight-start
20-
[MapDerivedType<Audi, AudiDto>] // for c# language level ≥ 11
21-
[MapDerivedType(typeof(Porsche), typeof(PorscheDto))] // for c# language level < 11
20+
[MapDerivedType<Banana, BananaDto>] // for c# language level ≥ 11
21+
[MapDerivedType(typeof(Apple), typeof(AppleDto))] // for c# language level < 11
2222
// highlight-end
23-
public static partial CarDto MapCar(Car source);
23+
public static partial FruitDto MapFruit(Fruit source);
2424
}
2525

26-
abstract class Car {}
27-
class Audi : Car {}
28-
class Porsche : Car {}
26+
abstract class Fruit {}
27+
class Banana : Fruit {}
28+
class Apple : Fruit {}
2929

30-
abstract class CarDto {}
31-
class AudiDto : CarDto {}
32-
class PorscheDto : CarDto {}
30+
abstract class FruitDto {}
31+
class BananaDto : FruitDto {}
32+
class AppleDto : FruitDto {}
3333
```
3434

3535
</TabItem>
@@ -39,17 +39,17 @@ class PorscheDto : CarDto {}
3939
[Mapper]
4040
public static partial class ModelMapper
4141
{
42-
public static partial CarDto MapCar(Car source)
42+
public static partial FruitDto MapFruit(Fruit source)
4343
{
4444
return source switch
4545
{
46-
Audi x => MapToAudiDto(x),
47-
Porsche x => MapToPorscheDto(x),
48-
_ => throw new System.ArgumentException($"Cannot map {source.GetType()} to CarDto as there is no known derived type mapping", nameof(source)),
46+
Banana x => MapToBananaDto(x),
47+
Apple x => MapToAppleDto(x),
48+
_ => throw new System.ArgumentException($"Cannot map {source.GetType()} to FruitDto as there is no known derived type mapping", nameof(source)),
4949
};
5050
}
5151

52-
// ... implementations of MapToAudiDto and MapToPorscheDto
52+
// ... implementations of MapToBananaDto and MapToAppleDto
5353
}
5454
```
5555

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Runtime target type mapping
2+
3+
If the target type of a mapping is not known at compile time,
4+
a mapping method with a `Type` parameter can be used.
5+
Mapperly implements this mapping method
6+
using all mappings the user defined in the mapper.
7+
8+
```csharp
9+
[Mapper]
10+
public static partial class ModelMapper
11+
{
12+
// highlight-start
13+
public static partial object Map(object source, Type targetType);
14+
// highlight-end
15+
16+
private static partial BananaDto MapBanana(Banana source);
17+
private static partial AppleDto MapApple(Apple source);
18+
}
19+
20+
class Banana {}
21+
class Apple {}
22+
23+
class BananaDto {}
24+
class AppleDto {}
25+
```
26+
27+
If the source or target type of a runtime target type mapping is not `object`,
28+
only user mappings of which the source/target type is assignable to the source/target type of the mapping method are considered.
29+
30+
Runtime target type mappings support [derived type mappings](./10-derived-type-mapping.md).
31+
The `MapDerivedTypeAttribute` can be directly applied to a runtime target type mapping method.
32+
33+
:::info
34+
Mapperly runtime target type mappings
35+
only support source/target type combinations which are defined
36+
as mappings in the same mapper.
37+
If an unknown source/target type combination is provided at runtime,
38+
an `ArgumentException` is thrown.
39+
:::

src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MappingBodyBuilder.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ public void BuildMappingBodies()
3737
case UserDefinedExistingTargetMethodMapping mapping:
3838
UserMethodMappingBodyBuilder.BuildMappingBody(ctx, mapping);
3939
break;
40+
case UserDefinedNewInstanceRuntimeTargetTypeMapping mapping:
41+
RuntimeTargetTypeMappingBodyBuilder.BuildMappingBody(ctx, mapping);
42+
break;
4043
}
4144
}
4245
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using Riok.Mapperly.Descriptors.MappingBuilders;
2+
using Riok.Mapperly.Descriptors.Mappings;
3+
using Riok.Mapperly.Helpers;
4+
5+
namespace Riok.Mapperly.Descriptors.MappingBodyBuilders;
6+
7+
public static class RuntimeTargetTypeMappingBodyBuilder
8+
{
9+
public static void BuildMappingBody(MappingBuilderContext ctx, UserDefinedNewInstanceRuntimeTargetTypeMapping mapping)
10+
{
11+
// source nulls are filtered out by the type switch arms,
12+
// therefore set source type always to nun-nullable
13+
// as non-nullables are also assignable to nullables.
14+
IEnumerable<ITypeMapping> mappings = ctx.CallableUserMappings.Where(
15+
x =>
16+
x.SourceType.NonNullable().IsAssignableTo(ctx.Compilation, mapping.SourceType)
17+
&& x.TargetType.IsAssignableTo(ctx.Compilation, mapping.TargetType)
18+
);
19+
20+
// include derived type mappings declared on this user defined method
21+
var derivedTypeMappings = DerivedTypeMappingBuilder.TryBuildContainedMappings(ctx, true);
22+
if (derivedTypeMappings != null)
23+
{
24+
mappings = derivedTypeMappings.Concat(mappings);
25+
}
26+
27+
// prefer non-nullable return types
28+
// and prefer types with a higher inheritance level
29+
// over types with a lower inheritance level
30+
// in the type switch
31+
// to use the most specific mapping
32+
mappings = mappings
33+
.OrderByDescending(x => x.SourceType.GetInheritanceLevel())
34+
.ThenByDescending(x => x.TargetType.GetInheritanceLevel())
35+
.ThenBy(x => x.TargetType.IsNullable())
36+
.GroupBy(x => new TypeMappingKey(x, false))
37+
.Select(x => x.First());
38+
mapping.AddMappings(mappings);
39+
}
40+
}

src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ protected MappingBuilderContext(MappingBuilderContext ctx, IMethodSymbol? userSy
4242

4343
public ObjectFactoryCollection ObjectFactories { get; }
4444

45+
/// <inheritdoc cref="MappingBuilderContext.CallableUserMappings"/>
46+
public IReadOnlyCollection<IUserMapping> CallableUserMappings => MappingBuilder.CallableUserMappings;
47+
4548
public T GetConfigurationOrDefault<T>()
4649
where T : Attribute => Configuration.GetOrDefault<T>(_userSymbol);
4750

src/Riok.Mapperly/Descriptors/MappingBuilders/DerivedTypeMappingBuilder.cs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,40 @@ public static class DerivedTypeMappingBuilder
1111
{
1212
public static ITypeMapping? TryBuildMapping(MappingBuilderContext ctx)
1313
{
14-
var configs = ctx.ListConfiguration<MapDerivedTypeAttribute, MapDerivedType>()
15-
.Concat(ctx.ListConfiguration<MapDerivedTypeAttribute<object, object>, MapDerivedType>())
16-
.ToList();
17-
if (configs.Count == 0)
14+
var derivedTypeMappings = TryBuildContainedMappings(ctx);
15+
if (derivedTypeMappings == null)
1816
return null;
1917

20-
var derivedTypeMappings = BuildDerivedTypeMappings(ctx, configs);
2118
return ctx.IsExpression
2219
? new DerivedTypeIfExpressionMapping(ctx.Source, ctx.Target, derivedTypeMappings)
2320
: new DerivedTypeSwitchMapping(ctx.Source, ctx.Target, derivedTypeMappings);
2421
}
2522

26-
private static IReadOnlyCollection<ITypeMapping> BuildDerivedTypeMappings(
23+
public static IReadOnlyCollection<ITypeMapping>? TryBuildContainedMappings(
24+
MappingBuilderContext ctx,
25+
bool duplicatedSourceTypesAllowed = false
26+
)
27+
{
28+
var configs = ctx.ListConfiguration<MapDerivedTypeAttribute, MapDerivedType>()
29+
.Concat(ctx.ListConfiguration<MapDerivedTypeAttribute<object, object>, MapDerivedType>())
30+
.ToList();
31+
return configs.Count == 0 ? null : BuildContainedMappings(ctx, configs, duplicatedSourceTypesAllowed);
32+
}
33+
34+
private static IReadOnlyCollection<ITypeMapping> BuildContainedMappings(
2735
MappingBuilderContext ctx,
28-
IReadOnlyCollection<MapDerivedType> configs
36+
IReadOnlyCollection<MapDerivedType> configs,
37+
bool duplicatedSourceTypesAllowed
2938
)
3039
{
3140
var derivedTypeMappingSourceTypes = new HashSet<ITypeSymbol>(SymbolEqualityComparer.Default);
3241
var derivedTypeMappings = new List<ITypeMapping>(configs.Count);
3342

3443
foreach (var config in configs)
3544
{
36-
// set reference types non-nullable as they can never be null when type-switching.
37-
var sourceType = config.SourceType.WithNullableAnnotation(NullableAnnotation.NotAnnotated);
38-
if (!derivedTypeMappingSourceTypes.Add(sourceType))
45+
// set types non-nullable as they can never be null when type-switching.
46+
var sourceType = config.SourceType.NonNullable();
47+
if (!duplicatedSourceTypesAllowed && !derivedTypeMappingSourceTypes.Add(sourceType))
3948
{
4049
ctx.ReportDiagnostic(DiagnosticDescriptors.DerivedSourceTypeDuplicated, sourceType);
4150
continue;
@@ -47,7 +56,7 @@ IReadOnlyCollection<MapDerivedType> configs
4756
continue;
4857
}
4958

50-
var targetType = config.TargetType.WithNullableAnnotation(NullableAnnotation.NotAnnotated);
59+
var targetType = config.TargetType.NonNullable();
5160
if (!targetType.IsAssignableTo(ctx.Compilation, ctx.Target))
5261
{
5362
ctx.ReportDiagnostic(DiagnosticDescriptors.DerivedTargetTypeIsNotAssignableToReturnType, targetType, ctx.Target);

src/Riok.Mapperly/Descriptors/MappingBuilders/MappingBuilder.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ public MappingBuilder(MappingCollection mappings)
3636
_mappings = mappings;
3737
}
3838

39+
/// <inheritdoc cref="MappingCollection.CallableUserMappings"/>
40+
public IReadOnlyCollection<IUserMapping> CallableUserMappings => _mappings.CallableUserMappings;
41+
3942
/// <inheritdoc cref="MappingBuilderContext.FindMapping"/>
4043
public ITypeMapping? Find(ITypeSymbol sourceType, ITypeSymbol targetType) => _mappings.Find(sourceType, targetType);
4144

src/Riok.Mapperly/Descriptors/MappingCollection.cs

Lines changed: 26 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,37 @@ namespace Riok.Mapperly.Descriptors;
77

88
public class MappingCollection
99
{
10-
// this includes mappings to build and already built mappings
10+
/// <summary>
11+
/// The first callable mapping of each type pair.
12+
/// Contains mappings to build and already built mappings
13+
/// </summary>
1114
private readonly Dictionary<TypeMappingKey, ITypeMapping> _mappings = new();
1215

13-
// a list of all method mappings (extra mappings and mappings)
16+
/// <summary>
17+
/// A list of all method mappings (extra mappings and mappings)
18+
/// </summary>
1419
private readonly List<MethodMapping> _methodMappings = new();
1520

16-
// queue of mappings which don't have the body built yet
21+
/// <summary>
22+
/// A list of all callable user mappings with <see cref="ITypeMapping.CallableByOtherMappings"/> <c>true</c>.
23+
/// </summary>
24+
private readonly List<IUserMapping> _callableUserMappings = new();
25+
26+
/// <summary>
27+
/// Queue of mappings which don't have the body built yet
28+
/// </summary>
1729
private readonly Queue<(IMapping, MappingBuilderContext)> _mappingsToBuildBody = new();
1830

19-
// a list of existing target mappings
31+
/// <summary>
32+
/// All existing target mappings
33+
/// </summary>
2034
private readonly Dictionary<TypeMappingKey, IExistingTargetMapping> _existingTargetMappings = new();
2135

2236
public IReadOnlyCollection<MethodMapping> MethodMappings => _methodMappings;
2337

38+
/// <inheritdoc cref="_callableUserMappings"/>
39+
public IReadOnlyCollection<IUserMapping> CallableUserMappings => _callableUserMappings;
40+
2441
public ITypeMapping? Find(ITypeSymbol sourceType, ITypeSymbol targetType)
2542
{
2643
_mappings.TryGetValue(new TypeMappingKey(sourceType, targetType), out var mapping);
@@ -37,6 +54,11 @@ public class MappingCollection
3754

3855
public void Add(ITypeMapping mapping)
3956
{
57+
if (mapping is IUserMapping { CallableByOtherMappings: true } userMapping)
58+
{
59+
_callableUserMappings.Add(userMapping);
60+
}
61+
4062
if (mapping is MethodMapping methodMapping)
4163
{
4264
_methodMappings.Add(methodMapping);
@@ -52,42 +74,4 @@ public void AddExistingTargetMapping(IExistingTargetMapping mapping) =>
5274
_existingTargetMappings.Add(new TypeMappingKey(mapping), mapping);
5375

5476
public IEnumerable<(IMapping, MappingBuilderContext)> DequeueMappingsToBuildBody() => _mappingsToBuildBody.DequeueAll();
55-
56-
private readonly struct TypeMappingKey
57-
{
58-
private static readonly IEqualityComparer<ISymbol?> _comparer = SymbolEqualityComparer.IncludeNullability;
59-
60-
private readonly ITypeSymbol _source;
61-
private readonly ITypeSymbol _target;
62-
63-
public TypeMappingKey(ITypeMapping mapping)
64-
: this(mapping.SourceType, mapping.TargetType) { }
65-
66-
public TypeMappingKey(IExistingTargetMapping mapping)
67-
: this(mapping.SourceType, mapping.TargetType) { }
68-
69-
public TypeMappingKey(ITypeSymbol source, ITypeSymbol target)
70-
{
71-
_source = source;
72-
_target = target;
73-
}
74-
75-
private bool Equals(TypeMappingKey other) => _comparer.Equals(_source, other._source) && _comparer.Equals(_target, other._target);
76-
77-
public override bool Equals(object? obj) => obj is TypeMappingKey other && Equals(other);
78-
79-
public override int GetHashCode()
80-
{
81-
unchecked
82-
{
83-
var hashCode = _comparer.GetHashCode(_source);
84-
hashCode = (hashCode * 397) ^ _comparer.GetHashCode(_target);
85-
return hashCode;
86-
}
87-
}
88-
89-
public static bool operator ==(TypeMappingKey left, TypeMappingKey right) => left.Equals(right);
90-
91-
public static bool operator !=(TypeMappingKey left, TypeMappingKey right) => !left.Equals(right);
92-
}
9377
}

src/Riok.Mapperly/Descriptors/Mappings/DerivedTypeSwitchMapping.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Riok.Mapperly.Descriptors.Mappings;
1111
/// </summary>
1212
public class DerivedTypeSwitchMapping : TypeMapping
1313
{
14-
private const string GetTypeMethodName = "GetType";
14+
private const string GetTypeMethodName = nameof(GetType);
1515

1616
private readonly IReadOnlyCollection<ITypeMapping> _typeMappings;
1717

@@ -33,7 +33,7 @@ public override ExpressionSyntax Build(TypeMappingBuildContext ctx)
3333
)
3434
);
3535

36-
// source switch { A x => MapToA(x), B x => MapToB(x) }
36+
// source switch { A x => MapToADto(x), B x => MapToBDto(x) }
3737
var (typeArmContext, typeArmVariableName) = ctx.WithNewSource();
3838
var arms = _typeMappings
3939
.Select(x => BuildSwitchArm(typeArmVariableName, x.SourceType, x.Build(typeArmContext)))
@@ -43,7 +43,7 @@ public override ExpressionSyntax Build(TypeMappingBuildContext ctx)
4343

4444
private SwitchExpressionArmSyntax BuildSwitchArm(string typeArmVariableName, ITypeSymbol type, ExpressionSyntax mapping)
4545
{
46-
// A x => MapToA(x),
46+
// A x => MapToADto(x),
4747
var declaration = DeclarationPattern(FullyQualifiedIdentifier(type), SingleVariableDesignation(Identifier(typeArmVariableName)));
4848
return SwitchExpressionArm(declaration, mapping);
4949
}

0 commit comments

Comments
 (0)