Skip to content

Commit

Permalink
feat: accept destination type as mapping method parameter (#398)
Browse files Browse the repository at this point in the history
  • Loading branch information
latonz authored May 10, 2023
1 parent ebff0af commit b17f666
Show file tree
Hide file tree
Showing 44 changed files with 1,004 additions and 158 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import GeneratedCarMapperSource from '!!raw-loader!../../src/data/generated/samp

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

## The source classes

Expand Down
4 changes: 2 additions & 2 deletions docs/docs/02-configuration/01-mapper.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ To enforce strict mappings
(all source members have to be mapped to a target member
and all target members have to be mapped from a source member,
except for ignored members)
set the following two EditorConfig settings (see also [analyzer diagnostics](./14-analyzer-diagnostics.mdx)):
set the following two EditorConfig settings (see also [analyzer diagnostics](./15-analyzer-diagnostics.mdx)):

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

### Strict enum mappings

To enforce strict enum mappings set 'RMG037' and 'RMG038' to error, see [strict enum mappings](./04-enum.mdx).
To enforce strict enum mappings set `RMG037` and `RMG038` to error, see [strict enum mappings](./04-enum.mdx).
2 changes: 1 addition & 1 deletion docs/docs/02-configuration/04-enum.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public partial class CarMapper
To enforce strict enum mappings
(all source enum values have to be mapped to a target enum value
and all target enum values have to be mapped from a source enum value)
set the following two EditorConfig settings (see also [analyzer diagnostics](./14-analyzer-diagnostics.mdx)):
set the following two EditorConfig settings (see also [analyzer diagnostics](./15-analyzer-diagnostics.mdx)):

```editorconfig title=".editorconfig"
[*.cs]
Expand Down
28 changes: 14 additions & 14 deletions docs/docs/02-configuration/10-derived-type-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@ This can be configured with the `MapDerivedTypeAttribute`:
public static partial class ModelMapper
{
// highlight-start
[MapDerivedType<Audi, AudiDto>] // for c# language level ≥ 11
[MapDerivedType(typeof(Porsche), typeof(PorscheDto))] // for c# language level < 11
[MapDerivedType<Banana, BananaDto>] // for c# language level ≥ 11
[MapDerivedType(typeof(Apple), typeof(AppleDto))] // for c# language level < 11
// highlight-end
public static partial CarDto MapCar(Car source);
public static partial FruitDto MapFruit(Fruit source);
}

abstract class Car {}
class Audi : Car {}
class Porsche : Car {}
abstract class Fruit {}
class Banana : Fruit {}
class Apple : Fruit {}

abstract class CarDto {}
class AudiDto : CarDto {}
class PorscheDto : CarDto {}
abstract class FruitDto {}
class BananaDto : FruitDto {}
class AppleDto : FruitDto {}
```

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

// ... implementations of MapToAudiDto and MapToPorscheDto
// ... implementations of MapToBananaDto and MapToAppleDto
}
```

Expand Down
39 changes: 39 additions & 0 deletions docs/docs/02-configuration/11-runtime-target-type-mapping.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Runtime target type mapping

If the target type of a mapping is not known at compile time,
a mapping method with a `Type` parameter can be used.
Mapperly implements this mapping method
using all mappings the user defined in the mapper.

```csharp
[Mapper]
public static partial class ModelMapper
{
// highlight-start
public static partial object Map(object source, Type targetType);
// highlight-end
private static partial BananaDto MapBanana(Banana source);
private static partial AppleDto MapApple(Apple source);
}

class Banana {}
class Apple {}

class BananaDto {}
class AppleDto {}
```

If the source or target type of a runtime target type mapping is not `object`,
only user mappings of which the source/target type is assignable to the source/target type of the mapping method are considered.

Runtime target type mappings support [derived type mappings](./10-derived-type-mapping.md).
The `MapDerivedTypeAttribute` can be directly applied to a runtime target type mapping method.

:::info
Mapperly runtime target type mappings
only support source/target type combinations which are defined
as mappings in the same mapper.
If an unknown source/target type combination is provided at runtime,
an `ArgumentException` is thrown.
:::
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ public void BuildMappingBodies()
case UserDefinedExistingTargetMethodMapping mapping:
UserMethodMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
case UserDefinedNewInstanceRuntimeTargetTypeMapping mapping:
RuntimeTargetTypeMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Riok.Mapperly.Descriptors.MappingBuilders;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Descriptors.MappingBodyBuilders;

public static class RuntimeTargetTypeMappingBodyBuilder
{
public static void BuildMappingBody(MappingBuilderContext ctx, UserDefinedNewInstanceRuntimeTargetTypeMapping mapping)
{
// source nulls are filtered out by the type switch arms,
// therefore set source type always to nun-nullable
// as non-nullables are also assignable to nullables.
IEnumerable<ITypeMapping> mappings = ctx.CallableUserMappings.Where(
x =>
x.SourceType.NonNullable().IsAssignableTo(ctx.Compilation, mapping.SourceType)
&& x.TargetType.IsAssignableTo(ctx.Compilation, mapping.TargetType)
);

// include derived type mappings declared on this user defined method
var derivedTypeMappings = DerivedTypeMappingBuilder.TryBuildContainedMappings(ctx, true);
if (derivedTypeMappings != null)
{
mappings = derivedTypeMappings.Concat(mappings);
}

// prefer non-nullable return types
// and prefer types with a higher inheritance level
// over types with a lower inheritance level
// in the type switch
// to use the most specific mapping
mappings = mappings
.OrderByDescending(x => x.SourceType.GetInheritanceLevel())
.ThenByDescending(x => x.TargetType.GetInheritanceLevel())
.ThenBy(x => x.TargetType.IsNullable())
.GroupBy(x => new TypeMappingKey(x, false))
.Select(x => x.First());
mapping.AddMappings(mappings);
}
}
3 changes: 3 additions & 0 deletions src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ protected MappingBuilderContext(MappingBuilderContext ctx, IMethodSymbol? userSy

public ObjectFactoryCollection ObjectFactories { get; }

/// <inheritdoc cref="MappingBuilderContext.CallableUserMappings"/>
public IReadOnlyCollection<IUserMapping> CallableUserMappings => MappingBuilder.CallableUserMappings;

public T GetConfigurationOrDefault<T>()
where T : Attribute => Configuration.GetOrDefault<T>(_userSymbol);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,40 @@ public static class DerivedTypeMappingBuilder
{
public static ITypeMapping? TryBuildMapping(MappingBuilderContext ctx)
{
var configs = ctx.ListConfiguration<MapDerivedTypeAttribute, MapDerivedType>()
.Concat(ctx.ListConfiguration<MapDerivedTypeAttribute<object, object>, MapDerivedType>())
.ToList();
if (configs.Count == 0)
var derivedTypeMappings = TryBuildContainedMappings(ctx);
if (derivedTypeMappings == null)
return null;

var derivedTypeMappings = BuildDerivedTypeMappings(ctx, configs);
return ctx.IsExpression
? new DerivedTypeIfExpressionMapping(ctx.Source, ctx.Target, derivedTypeMappings)
: new DerivedTypeSwitchMapping(ctx.Source, ctx.Target, derivedTypeMappings);
}

private static IReadOnlyCollection<ITypeMapping> BuildDerivedTypeMappings(
public static IReadOnlyCollection<ITypeMapping>? TryBuildContainedMappings(
MappingBuilderContext ctx,
bool duplicatedSourceTypesAllowed = false
)
{
var configs = ctx.ListConfiguration<MapDerivedTypeAttribute, MapDerivedType>()
.Concat(ctx.ListConfiguration<MapDerivedTypeAttribute<object, object>, MapDerivedType>())
.ToList();
return configs.Count == 0 ? null : BuildContainedMappings(ctx, configs, duplicatedSourceTypesAllowed);
}

private static IReadOnlyCollection<ITypeMapping> BuildContainedMappings(
MappingBuilderContext ctx,
IReadOnlyCollection<MapDerivedType> configs
IReadOnlyCollection<MapDerivedType> configs,
bool duplicatedSourceTypesAllowed
)
{
var derivedTypeMappingSourceTypes = new HashSet<ITypeSymbol>(SymbolEqualityComparer.Default);
var derivedTypeMappings = new List<ITypeMapping>(configs.Count);

foreach (var config in configs)
{
// set reference types non-nullable as they can never be null when type-switching.
var sourceType = config.SourceType.WithNullableAnnotation(NullableAnnotation.NotAnnotated);
if (!derivedTypeMappingSourceTypes.Add(sourceType))
// set types non-nullable as they can never be null when type-switching.
var sourceType = config.SourceType.NonNullable();
if (!duplicatedSourceTypesAllowed && !derivedTypeMappingSourceTypes.Add(sourceType))
{
ctx.ReportDiagnostic(DiagnosticDescriptors.DerivedSourceTypeDuplicated, sourceType);
continue;
Expand All @@ -47,7 +56,7 @@ IReadOnlyCollection<MapDerivedType> configs
continue;
}

var targetType = config.TargetType.WithNullableAnnotation(NullableAnnotation.NotAnnotated);
var targetType = config.TargetType.NonNullable();
if (!targetType.IsAssignableTo(ctx.Compilation, ctx.Target))
{
ctx.ReportDiagnostic(DiagnosticDescriptors.DerivedTargetTypeIsNotAssignableToReturnType, targetType, ctx.Target);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public MappingBuilder(MappingCollection mappings)
_mappings = mappings;
}

/// <inheritdoc cref="MappingCollection.CallableUserMappings"/>
public IReadOnlyCollection<IUserMapping> CallableUserMappings => _mappings.CallableUserMappings;

/// <inheritdoc cref="MappingBuilderContext.FindMapping"/>
public ITypeMapping? Find(ITypeSymbol sourceType, ITypeSymbol targetType) => _mappings.Find(sourceType, targetType);

Expand Down
68 changes: 26 additions & 42 deletions src/Riok.Mapperly/Descriptors/MappingCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,37 @@ namespace Riok.Mapperly.Descriptors;

public class MappingCollection
{
// this includes mappings to build and already built mappings
/// <summary>
/// The first callable mapping of each type pair.
/// Contains mappings to build and already built mappings
/// </summary>
private readonly Dictionary<TypeMappingKey, ITypeMapping> _mappings = new();

// a list of all method mappings (extra mappings and mappings)
/// <summary>
/// A list of all method mappings (extra mappings and mappings)
/// </summary>
private readonly List<MethodMapping> _methodMappings = new();

// queue of mappings which don't have the body built yet
/// <summary>
/// A list of all callable user mappings with <see cref="ITypeMapping.CallableByOtherMappings"/> <c>true</c>.
/// </summary>
private readonly List<IUserMapping> _callableUserMappings = new();

/// <summary>
/// Queue of mappings which don't have the body built yet
/// </summary>
private readonly Queue<(IMapping, MappingBuilderContext)> _mappingsToBuildBody = new();

// a list of existing target mappings
/// <summary>
/// All existing target mappings
/// </summary>
private readonly Dictionary<TypeMappingKey, IExistingTargetMapping> _existingTargetMappings = new();

public IReadOnlyCollection<MethodMapping> MethodMappings => _methodMappings;

/// <inheritdoc cref="_callableUserMappings"/>
public IReadOnlyCollection<IUserMapping> CallableUserMappings => _callableUserMappings;

public ITypeMapping? Find(ITypeSymbol sourceType, ITypeSymbol targetType)
{
_mappings.TryGetValue(new TypeMappingKey(sourceType, targetType), out var mapping);
Expand All @@ -37,6 +54,11 @@ public class MappingCollection

public void Add(ITypeMapping mapping)
{
if (mapping is IUserMapping { CallableByOtherMappings: true } userMapping)
{
_callableUserMappings.Add(userMapping);
}

if (mapping is MethodMapping methodMapping)
{
_methodMappings.Add(methodMapping);
Expand All @@ -52,42 +74,4 @@ public void AddExistingTargetMapping(IExistingTargetMapping mapping) =>
_existingTargetMappings.Add(new TypeMappingKey(mapping), mapping);

public IEnumerable<(IMapping, MappingBuilderContext)> DequeueMappingsToBuildBody() => _mappingsToBuildBody.DequeueAll();

private readonly struct TypeMappingKey
{
private static readonly IEqualityComparer<ISymbol?> _comparer = SymbolEqualityComparer.IncludeNullability;

private readonly ITypeSymbol _source;
private readonly ITypeSymbol _target;

public TypeMappingKey(ITypeMapping mapping)
: this(mapping.SourceType, mapping.TargetType) { }

public TypeMappingKey(IExistingTargetMapping mapping)
: this(mapping.SourceType, mapping.TargetType) { }

public TypeMappingKey(ITypeSymbol source, ITypeSymbol target)
{
_source = source;
_target = target;
}

private bool Equals(TypeMappingKey other) => _comparer.Equals(_source, other._source) && _comparer.Equals(_target, other._target);

public override bool Equals(object? obj) => obj is TypeMappingKey other && Equals(other);

public override int GetHashCode()
{
unchecked
{
var hashCode = _comparer.GetHashCode(_source);
hashCode = (hashCode * 397) ^ _comparer.GetHashCode(_target);
return hashCode;
}
}

public static bool operator ==(TypeMappingKey left, TypeMappingKey right) => left.Equals(right);

public static bool operator !=(TypeMappingKey left, TypeMappingKey right) => !left.Equals(right);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Riok.Mapperly.Descriptors.Mappings;
/// </summary>
public class DerivedTypeSwitchMapping : TypeMapping
{
private const string GetTypeMethodName = "GetType";
private const string GetTypeMethodName = nameof(GetType);

private readonly IReadOnlyCollection<ITypeMapping> _typeMappings;

Expand All @@ -33,7 +33,7 @@ public override ExpressionSyntax Build(TypeMappingBuildContext ctx)
)
);

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

private SwitchExpressionArmSyntax BuildSwitchArm(string typeArmVariableName, ITypeSymbol type, ExpressionSyntax mapping)
{
// A x => MapToA(x),
// A x => MapToADto(x),
var declaration = DeclarationPattern(FullyQualifiedIdentifier(type), SingleVariableDesignation(Identifier(typeArmVariableName)));
return SwitchExpressionArm(declaration, mapping);
}
Expand Down
Loading

0 comments on commit b17f666

Please sign in to comment.