Skip to content

Commit

Permalink
feat: add MapDerivedType for existing target type mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
TimothyMakkison committed Dec 4, 2023
1 parent 7628cf3 commit da6c1ff
Show file tree
Hide file tree
Showing 19 changed files with 548 additions and 9 deletions.
4 changes: 2 additions & 2 deletions docs/docs/configuration/derived-type-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ description: Map derived types and interfaces
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

Mapperly supports interfaces and base types as mapping sources and targets,
but Mapperly needs to know which derived types exist.
Mapperly supports interfaces and base types as mapping sources and targets, for both new instance and [exiting target](./existing-target.md) mapings.
To do this, Mapperly needs to know which derived types exist.
This can be configured with the `MapDerivedTypeAttribute`:

<Tabs>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Configuration;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.Mappings.ExistingTarget;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;

Expand All @@ -19,24 +20,47 @@ public static class DerivedTypeMappingBuilder
: new DerivedTypeSwitchMapping(ctx.Source, ctx.Target, derivedTypeMappings);
}

public static IExistingTargetMapping? TryBuildExistingTargetMapping(MappingBuilderContext ctx)
{
var derivedTypeMappings = TryBuildExistingTargetContainedMappings(ctx);
return derivedTypeMappings == null ? null : new DerivedExistingTargetTypeSwitchMapping(ctx.Source, ctx.Target, derivedTypeMappings);
}

public static IReadOnlyCollection<INewInstanceMapping>? TryBuildContainedMappings(
MappingBuilderContext ctx,
bool duplicatedSourceTypesAllowed = false
)
{
return ctx.Configuration.DerivedTypes.Count == 0
? null
: BuildContainedMappings(ctx, ctx.Configuration.DerivedTypes, duplicatedSourceTypesAllowed);
: BuildContainedMappings(ctx, ctx.Configuration.DerivedTypes, ctx.FindOrBuildMapping, duplicatedSourceTypesAllowed);
}

private static IReadOnlyCollection<IExistingTargetMapping>? TryBuildExistingTargetContainedMappings(
MappingBuilderContext ctx,
bool duplicatedSourceTypesAllowed = false
)
{
return ctx.Configuration.DerivedTypes.Count == 0
? null
: BuildContainedMappings(
ctx,
ctx.Configuration.DerivedTypes,
(source, target, options, _) => ctx.FindOrBuildExistingTargetMapping(source, target, options),
duplicatedSourceTypesAllowed
);
}

private static IReadOnlyCollection<INewInstanceMapping> BuildContainedMappings(
private static IReadOnlyCollection<TMapping> BuildContainedMappings<TMapping>(
MappingBuilderContext ctx,
IReadOnlyCollection<DerivedTypeMappingConfiguration> configs,
Func<ITypeSymbol, ITypeSymbol, MappingBuildingOptions, Location?, TMapping?> findOrBuildMapping,
bool duplicatedSourceTypesAllowed
)
where TMapping : ITypeMapping
{
var derivedTypeMappingSourceTypes = new HashSet<ITypeSymbol>(SymbolEqualityComparer.Default);
var derivedTypeMappings = new List<INewInstanceMapping>(configs.Count);
var derivedTypeMappings = new List<TMapping>(configs.Count);
Func<ITypeSymbol, bool> isAssignableToSource = ctx.Source is ITypeParameterSymbol sourceTypeParameter
? t => ctx.SymbolAccessor.DoesTypeSatisfyTypeParameterConstraints(sourceTypeParameter, t, ctx.Source.NullableAnnotation)
: t => ctx.SymbolAccessor.HasImplicitConversion(t, ctx.Source);
Expand Down Expand Up @@ -67,7 +91,7 @@ bool duplicatedSourceTypesAllowed
continue;
}

var mapping = ctx.FindOrBuildMapping(
var mapping = findOrBuildMapping(
sourceType,
targetType,
MappingBuildingOptions.KeepUserSymbol | MappingBuildingOptions.MarkAsReusable | MappingBuildingOptions.ClearDerivedTypes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class ExistingTargetMappingBuilder(MappingCollection mappings)
private static readonly IReadOnlyCollection<BuildExistingTargetMapping> _builders = new BuildExistingTargetMapping[]
{
NullableMappingBuilder.TryBuildExistingTargetMapping,
DerivedTypeMappingBuilder.TryBuildExistingTargetMapping,
DictionaryMappingBuilder.TryBuildExistingTargetMapping,
SpanMappingBuilder.TryBuildExistingTargetMapping,
MemoryMappingBuilder.TryBuildExistingTargetMapping,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Descriptors.Mappings.ExistingTarget;
using Riok.Mapperly.Emit.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;

namespace Riok.Mapperly.Descriptors.Mappings;

/// <summary>
/// A derived type mapping maps one base type or interface to another
/// by implementing a switch statement over known types and performs the provided mapping for each type.
/// </summary>
public class DerivedExistingTargetTypeSwitchMapping(
ITypeSymbol sourceType,
ITypeSymbol targetType,
IReadOnlyCollection<IExistingTargetMapping> existingTargetTypeMappings
) : ExistingTargetMapping(sourceType, targetType)
{
private const string SourceName = "source";
private const string TargetName = "target";
private const string GetTypeMethodName = nameof(GetType);

public override IEnumerable<StatementSyntax> Build(TypeMappingBuildContext ctx, ExpressionSyntax target)
{
var sourceExpression = TupleExpression(CommaSeparatedList(Argument(ctx.Source), Argument(target)));
var caseSections = existingTargetTypeMappings.Select(x => BuildSwitchSection(ctx, x));
var defaultSection = BuildDefaultSwitchSection(ctx, target);

yield return ctx.SyntaxFactory
.SwitchStatement(sourceExpression, caseSections, defaultSection)
.AddLeadingLineFeed(ctx.SyntaxFactory.Indentation);
}

private SwitchSectionSyntax BuildSwitchSection(TypeMappingBuildContext ctx, IExistingTargetMapping mapping)
{
var (sectionCtx, sourceVariableName) = ctx.WithNewScopedSource(SourceName);
var targetVariableName = sectionCtx.NameBuilder.New(TargetName);
sectionCtx = sectionCtx.AddIndentation();

// (A source, B target)
var positionalTypeMatch = PositionalPatternClause(
CommaSeparatedList(
Subpattern(DeclarationPattern(mapping.SourceType, sourceVariableName)),
Subpattern(DeclarationPattern(mapping.TargetType, targetVariableName))
)
);
var pattern = RecursivePattern().WithPositionalPatternClause(positionalTypeMatch);

// case (A source, B target):
var caseLabel = CasePatternSwitchLabel(pattern).AddLeadingLineFeed(sectionCtx.SyntaxFactory.Indentation);

// break;
var statementContext = sectionCtx.AddIndentation();
var breakStatement = BreakStatement().AddLeadingLineFeed(statementContext.SyntaxFactory.Indentation);
var target = IdentifierName(targetVariableName);
var statements = mapping.Build(statementContext, target).Append(breakStatement);

return SwitchSection(caseLabel, statements);
}

private SwitchSectionSyntax BuildDefaultSwitchSection(TypeMappingBuildContext ctx, ExpressionSyntax target)
{
// default:
var sectionCtx = ctx.SyntaxFactory.AddIndentation();
var defaultCaseLabel = DefaultSwitchLabel().AddLeadingLineFeed(sectionCtx.Indentation);

// throw new ArgumentException(msg, nameof(ctx.Source)),
var sourceType = Invocation(MemberAccess(ctx.Source, GetTypeMethodName));
var targetType = Invocation(MemberAccess(target, GetTypeMethodName));
var statementContext = sectionCtx.AddIndentation();
var throwExpression = ThrowArgumentExpression(
InterpolatedString($"Cannot map {sourceType} to {targetType} as there is no known derived type mapping"),
ctx.Source
)
.AddLeadingLineFeed(statementContext.Indentation);

var statements = new StatementSyntax[] { ExpressionStatement(throwExpression) };

return SwitchSection(defaultCaseLabel, statements);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,26 @@ SyntaxFactoryHelper syntaxFactory
/// builds the name of the source in this new scope
/// and creates a new context with the new source.
/// </summary>
/// <param name="sourceName">The name for the new scoped source.</param>
/// <returns>The new context and the scoped name of the source.</returns>
public (TypeMappingBuildContext Context, string SourceName) WithNewScopedSource() => WithNewScopedSource(IdentifierName);
public (TypeMappingBuildContext Context, string SourceName) WithNewScopedSource(string sourceName = DefaultSourceName) =>
WithNewScopedSource(IdentifierName, sourceName);

/// <summary>
/// Creates a new scoped name builder,
/// builds the name of the source in this new scope
/// and creates a new context with the new source.
/// </summary>
/// <param name="sourceBuilder">A function to build the source access for the new context.</param>
/// <param name="sourceName">The name for the new scoped source.</param>
/// <returns>The new context and the scoped name of the source.</returns>
public (TypeMappingBuildContext Context, string SourceName) WithNewScopedSource(Func<string, ExpressionSyntax> sourceBuilder)
public (TypeMappingBuildContext Context, string SourceName) WithNewScopedSource(
Func<string, ExpressionSyntax> sourceBuilder,
string sourceName = DefaultSourceName
)
{
var scopedNameBuilder = NameBuilder.NewScope();
var scopedSourceName = scopedNameBuilder.New(DefaultSourceName);
var scopedSourceName = scopedNameBuilder.New(sourceName);
var ctx = new TypeMappingBuildContext(sourceBuilder(scopedSourceName), ReferenceHandler, scopedNameBuilder, SyntaxFactory);
return (ctx, scopedSourceName);
}
Expand Down
7 changes: 7 additions & 0 deletions src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Pattern.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Helpers;
Expand All @@ -16,6 +17,12 @@ public static PatternSyntax OrPattern(IEnumerable<ExpressionSyntax?> values) =>
public static IsPatternExpressionSyntax IsPattern(ExpressionSyntax expression, PatternSyntax pattern) =>
IsPatternExpression(expression, SpacedToken(SyntaxKind.IsKeyword), pattern);

public static DeclarationPatternSyntax DeclarationPattern(ITypeSymbol type, string designation) =>
SyntaxFactory.DeclarationPattern(
FullyQualifiedIdentifier(type).AddTrailingSpace(),
SingleVariableDesignation(Identifier(designation))
);

private static BinaryPatternSyntax BinaryPattern(SyntaxKind kind, PatternSyntax left, PatternSyntax right)
{
var binaryPattern = SyntaxFactory.BinaryPattern(kind, left, right);
Expand Down
24 changes: 24 additions & 0 deletions src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Switch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,28 @@ public static SwitchExpressionArmSyntax SwitchArm(PatternSyntax pattern, Express
}

public static WhenClauseSyntax SwitchWhen(ExpressionSyntax condition) => WhenClause(SpacedToken(SyntaxKind.WhenKeyword), condition);

public SwitchStatementSyntax SwitchStatement(
ExpressionSyntax governingExpression,
IEnumerable<SwitchSectionSyntax> sections,
SwitchSectionSyntax defaultSection
)
{
return SyntaxFactory.SwitchStatement(
default,
TrailingSpacedToken(SyntaxKind.SwitchKeyword),
Token(SyntaxKind.None),
governingExpression,
Token(SyntaxKind.None),
LeadingLineFeedToken(SyntaxKind.OpenBraceToken),
List(sections.Append(defaultSection)),
LeadingLineFeedToken(SyntaxKind.CloseBraceToken)
);
}

public static SwitchSectionSyntax SwitchSection(SwitchLabelSyntax labelSyntax, IEnumerable<StatementSyntax> statements) =>
SyntaxFactory.SwitchSection().WithLabels(SingletonList(labelSyntax)).WithStatements(List(statements));

public static CasePatternSwitchLabelSyntax CasePatternSwitchLabel(PatternSyntax pattern) =>
SyntaxFactory.CasePatternSwitchLabel(TrailingSpacedToken(SyntaxKind.CaseKeyword), pattern, null, Token(SyntaxKind.ColonToken));
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ public static void MapExistingList(List<string> src, List<int> dst)

public static partial TTarget MapGeneric<TSource, TTarget>(TSource source);

#if NET7_0_OR_GREATER
[MapDerivedType<ExistingObjectTypeA, ExistingObjectTypeA>]
[MapDerivedType<ExistingObjectTypeB, ExistingObjectTypeB>]
#else
[MapDerivedType(typeof(ExistingObjectTypeA), typeof(ExistingObjectTypeA))]
[MapDerivedType(typeof(ExistingObjectTypeB), typeof(ExistingObjectTypeB))]
#endif
public static partial void MapToDerivedExisting(ExistingObjectBase source, ExistingObjectBase target);

[MapEnum(EnumMappingStrategy.ByName)]
public static partial TestEnumDtoByName MapToEnumDtoByName(TestEnum v);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Riok.Mapperly.IntegrationTests.Models
{
public class ExistingObjectBase
{
public int Value { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Riok.Mapperly.IntegrationTests.Models
{
public class ExistingObjectTypeA : ExistingObjectBase
{
public int ValueA { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Riok.Mapperly.IntegrationTests.Models
{
public class ExistingObjectTypeB : ExistingObjectBase
{
public int ValueB { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,23 @@ object x when typeof(TTarget).IsAssignableFrom(typeof(object)) => (TTarget)(obje
};
}

public static partial void MapToDerivedExisting(global::Riok.Mapperly.IntegrationTests.Models.ExistingObjectBase source, global::Riok.Mapperly.IntegrationTests.Models.ExistingObjectBase target)
{
switch (source, target)
{
case (global::Riok.Mapperly.IntegrationTests.Models.ExistingObjectTypeA source1, global::Riok.Mapperly.IntegrationTests.Models.ExistingObjectTypeA target1):
target1.ValueA = DirectInt(source1.ValueA);
target1.Value = DirectInt(source1.Value);
break;
case (global::Riok.Mapperly.IntegrationTests.Models.ExistingObjectTypeB source1, global::Riok.Mapperly.IntegrationTests.Models.ExistingObjectTypeB target1):
target1.ValueB = DirectInt(source1.ValueB);
target1.Value = DirectInt(source1.Value);
break;
default:
throw new System.ArgumentException($"Cannot map {source.GetType()} to {target.GetType()} as there is no known derived type mapping", nameof(source));
}
}

public static partial global::Riok.Mapperly.IntegrationTests.Dto.TestEnumDtoByName MapToEnumDtoByName(global::Riok.Mapperly.IntegrationTests.Models.TestEnum v)
{
return v switch
Expand Down
Loading

0 comments on commit da6c1ff

Please sign in to comment.