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 Nov 29, 2023
1 parent 574cef7 commit b536683
Show file tree
Hide file tree
Showing 19 changed files with 584 additions and 39 deletions.
70 changes: 38 additions & 32 deletions docs/docs/configuration/derived-type-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,38 +24,44 @@ This can be configured with the `MapDerivedTypeAttribute`:
[MapDerivedType(typeof(Apple), typeof(AppleDto))] // for c# language level < 11
// highlight-end
public static partial FruitDto MapFruit(Fruit source);
}

abstract class Fruit {}
class Banana : Fruit {}
class Apple : Fruit {}

abstract class FruitDto {}
class BananaDto : FruitDto {}
class AppleDto : FruitDto {}
```

</TabItem>
<TabItem value="generated" label="Generated code" default>

```csharp
[Mapper]
public static partial class ModelMapper
{
public static partial FruitDto MapFruit(Fruit source)
{
return source switch
{
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 MapToBananaDto and MapToAppleDto
}
```


// highlight-start
[MapDerivedType<Banana, BananaDto>] // for c# language level ≥ 11
[MapDerivedType(typeof(Apple), typeof(AppleDto))] // for c# language level < 11
// highlight-end
public static partial void MapToFruit(Fruit source, FruitDto target);
}

abstract class Fruit {}
class Banana : Fruit {}
class Apple : Fruit {}

abstract class FruitDto {}
class BananaDto : FruitDto {}
class AppleDto : FruitDto {}
```

</TabItem>
<TabItem value="generated" label="Generated code" default>

```csharp
[Mapper]
public static partial class ModelMapper
{
public static partial FruitDto MapFruit(Fruit source)
{
return source switch
{
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 MapToBananaDto and MapToAppleDto
}
```

</TabItem>
</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 b536683

Please sign in to comment.