Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add MapDerivedType for existing target type mapping #918

Merged
merged 2 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
TimothyMakkison marked this conversation as resolved.
Show resolved Hide resolved
TimothyMakkison marked this conversation as resolved.
Show resolved Hide resolved
)
{
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 @@ -560,6 +560,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
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