Skip to content

Commit

Permalink
feat: accept destination type as mapping method parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
latonz committed May 9, 2023
1 parent 3c41c84 commit a003c60
Show file tree
Hide file tree
Showing 43 changed files with 982 additions and 144 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
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 AudiDto MapAudi(Audi source);
private static partial PorscheDto MapPorsche(Porsche source);
}

class Audi {}
class Porsche {}

class AudiDto {}
class PorscheDto {}
```

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 Add(ITypeMapping 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
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,12 @@ public class UserDefinedExistingTargetMethodMapping : MethodMapping, IUserMappin
bool enableReferenceHandling,
INamedTypeSymbol referenceHandlerType
)
: base(sourceParameter, targetParameter.Type)
: base(method, sourceParameter, referenceHandlerParameter, targetParameter.Type)
{
_enableReferenceHandling = enableReferenceHandling;
_referenceHandlerType = referenceHandlerType;
IsPartial = true;
IsExtensionMethod = method.IsExtensionMethod;
Accessibility = method.DeclaredAccessibility;
Method = method;
MethodName = method.Name;
TargetParameter = targetParameter;
ReferenceHandlerParameter = referenceHandlerParameter;
}

public IMethodSymbol Method { get; }
Expand All @@ -47,8 +42,6 @@ INamedTypeSymbol referenceHandlerType

public override bool CallableByOtherMappings => false;

protected override ITypeSymbol? ReturnType => null; // return type is always void.

public override ExpressionSyntax Build(TypeMappingBuildContext ctx) =>
throw new InvalidOperationException($"{nameof(UserDefinedExistingTargetMethodMapping)} does not support {nameof(Build)}");

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.Diagnostics;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Symbols;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;

namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings;
Expand All @@ -25,10 +24,8 @@ public MemberAssignmentMapping(MemberPath targetPath, IMemberMapping mapping)

public MemberPath TargetPath { get; }

public IEnumerable<StatementSyntax> Build(TypeMappingBuildContext ctx, ExpressionSyntax targetAccess)
{
return new[] { ExpressionStatement(BuildExpression(ctx, targetAccess)), };
}
public IEnumerable<StatementSyntax> Build(TypeMappingBuildContext ctx, ExpressionSyntax targetAccess) =>
SingleStatement(BuildExpression(ctx, targetAccess));

public ExpressionSyntax BuildExpression(TypeMappingBuildContext ctx, ExpressionSyntax? targetAccess)
{
Expand Down

0 comments on commit a003c60

Please sign in to comment.