diff --git a/docs/docs/configuration/derived-type-mapping.md b/docs/docs/configuration/derived-type-mapping.md index f4cbce27c1..4dececd824 100644 --- a/docs/docs/configuration/derived-type-mapping.md +++ b/docs/docs/configuration/derived-type-mapping.md @@ -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`: diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/DerivedTypeMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/DerivedTypeMappingBuilder.cs index 9f9455547d..b4b3c5a46e 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/DerivedTypeMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/DerivedTypeMappingBuilder.cs @@ -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; @@ -19,6 +20,12 @@ 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? TryBuildContainedMappings( MappingBuilderContext ctx, bool duplicatedSourceTypesAllowed = false @@ -26,17 +33,34 @@ public static class DerivedTypeMappingBuilder { return ctx.Configuration.DerivedTypes.Count == 0 ? null - : BuildContainedMappings(ctx, ctx.Configuration.DerivedTypes, duplicatedSourceTypesAllowed); + : BuildContainedMappings(ctx, ctx.Configuration.DerivedTypes, ctx.FindOrBuildMapping, duplicatedSourceTypesAllowed); + } + + private static IReadOnlyCollection? 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 BuildContainedMappings( + private static IReadOnlyCollection BuildContainedMappings( MappingBuilderContext ctx, IReadOnlyCollection configs, + Func findOrBuildMapping, bool duplicatedSourceTypesAllowed ) + where TMapping : ITypeMapping { var derivedTypeMappingSourceTypes = new HashSet(SymbolEqualityComparer.Default); - var derivedTypeMappings = new List(configs.Count); + var derivedTypeMappings = new List(configs.Count); Func isAssignableToSource = ctx.Source is ITypeParameterSymbol sourceTypeParameter ? t => ctx.SymbolAccessor.DoesTypeSatisfyTypeParameterConstraints(sourceTypeParameter, t, ctx.Source.NullableAnnotation) : t => ctx.SymbolAccessor.HasImplicitConversion(t, ctx.Source); @@ -67,7 +91,7 @@ bool duplicatedSourceTypesAllowed continue; } - var mapping = ctx.FindOrBuildMapping( + var mapping = findOrBuildMapping( sourceType, targetType, MappingBuildingOptions.KeepUserSymbol | MappingBuildingOptions.MarkAsReusable | MappingBuildingOptions.ClearDerivedTypes, diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/ExistingTargetMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/ExistingTargetMappingBuilder.cs index b7ec867ac2..a49790bbaa 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/ExistingTargetMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/ExistingTargetMappingBuilder.cs @@ -9,6 +9,7 @@ public class ExistingTargetMappingBuilder(MappingCollection mappings) private static readonly IReadOnlyCollection _builders = new BuildExistingTargetMapping[] { NullableMappingBuilder.TryBuildExistingTargetMapping, + DerivedTypeMappingBuilder.TryBuildExistingTargetMapping, DictionaryMappingBuilder.TryBuildExistingTargetMapping, SpanMappingBuilder.TryBuildExistingTargetMapping, MemoryMappingBuilder.TryBuildExistingTargetMapping, diff --git a/src/Riok.Mapperly/Descriptors/Mappings/DerivedExistingTargetTypeSwitchMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/DerivedExistingTargetTypeSwitchMapping.cs new file mode 100644 index 0000000000..457c62f0e5 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/Mappings/DerivedExistingTargetTypeSwitchMapping.cs @@ -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; + +/// +/// 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. +/// +public class DerivedExistingTargetTypeSwitchMapping( + ITypeSymbol sourceType, + ITypeSymbol targetType, + IReadOnlyCollection existingTargetTypeMappings +) : ExistingTargetMapping(sourceType, targetType) +{ + private const string SourceName = "source"; + private const string TargetName = "target"; + private const string GetTypeMethodName = nameof(GetType); + + public override IEnumerable 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); + } +} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/TypeMappingBuildContext.cs b/src/Riok.Mapperly/Descriptors/Mappings/TypeMappingBuildContext.cs index afadf1584d..ba9ef9934d 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/TypeMappingBuildContext.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/TypeMappingBuildContext.cs @@ -45,8 +45,10 @@ SyntaxFactoryHelper syntaxFactory /// builds the name of the source in this new scope /// and creates a new context with the new source. /// + /// The name for the new scoped source. /// The new context and the scoped name of the source. - public (TypeMappingBuildContext Context, string SourceName) WithNewScopedSource() => WithNewScopedSource(IdentifierName); + public (TypeMappingBuildContext Context, string SourceName) WithNewScopedSource(string sourceName = DefaultSourceName) => + WithNewScopedSource(IdentifierName, sourceName); /// /// Creates a new scoped name builder, @@ -54,11 +56,15 @@ SyntaxFactoryHelper syntaxFactory /// and creates a new context with the new source. /// /// A function to build the source access for the new context. + /// The name for the new scoped source. /// The new context and the scoped name of the source. - public (TypeMappingBuildContext Context, string SourceName) WithNewScopedSource(Func sourceBuilder) + public (TypeMappingBuildContext Context, string SourceName) WithNewScopedSource( + Func 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); } diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Pattern.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Pattern.cs index 4bad9249ca..1322a1880b 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Pattern.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Pattern.cs @@ -1,3 +1,4 @@ +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Helpers; @@ -16,6 +17,12 @@ public static PatternSyntax OrPattern(IEnumerable 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); diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Switch.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Switch.cs index dfcc32c433..a9be6cb753 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Switch.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.Switch.cs @@ -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 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 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)); } diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs index a5623620ac..f3c186b63d 100644 --- a/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs @@ -91,6 +91,15 @@ public static void MapExistingList(List src, List dst) public static partial TTarget MapGeneric(TSource source); +#if NET7_0_OR_GREATER + [MapDerivedType] + [MapDerivedType] +#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); diff --git a/test/Riok.Mapperly.IntegrationTests/Models/ExistingObjectBase.cs b/test/Riok.Mapperly.IntegrationTests/Models/ExistingObjectBase.cs new file mode 100644 index 0000000000..ecf957bd8e --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/Models/ExistingObjectBase.cs @@ -0,0 +1,7 @@ +namespace Riok.Mapperly.IntegrationTests.Models +{ + public class ExistingObjectBase + { + public int Value { get; set; } + } +} diff --git a/test/Riok.Mapperly.IntegrationTests/Models/ExistingObjectTypeA.cs b/test/Riok.Mapperly.IntegrationTests/Models/ExistingObjectTypeA.cs new file mode 100644 index 0000000000..02acc4c585 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/Models/ExistingObjectTypeA.cs @@ -0,0 +1,7 @@ +namespace Riok.Mapperly.IntegrationTests.Models +{ + public class ExistingObjectTypeA : ExistingObjectBase + { + public int ValueA { get; set; } + } +} diff --git a/test/Riok.Mapperly.IntegrationTests/Models/ExistingObjectTypeB.cs b/test/Riok.Mapperly.IntegrationTests/Models/ExistingObjectTypeB.cs new file mode 100644 index 0000000000..5c872b374d --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/Models/ExistingObjectTypeB.cs @@ -0,0 +1,7 @@ +namespace Riok.Mapperly.IntegrationTests.Models +{ + public class ExistingObjectTypeB : ExistingObjectBase + { + public int ValueB { get; set; } + } +} diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs index 6b884bd7f1..0e42f99a1c 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource.verified.cs @@ -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 diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs index bd8552e7bf..8d911fa6aa 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.SnapshotGeneratedSource_NET6_0.verified.cs @@ -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 diff --git a/test/Riok.Mapperly.Tests/Mapping/DerivedExistingTargetTypeTest.cs b/test/Riok.Mapperly.Tests/Mapping/DerivedExistingTargetTypeTest.cs new file mode 100644 index 0000000000..79d4fdf330 --- /dev/null +++ b/test/Riok.Mapperly.Tests/Mapping/DerivedExistingTargetTypeTest.cs @@ -0,0 +1,212 @@ +using Riok.Mapperly.Diagnostics; + +namespace Riok.Mapperly.Tests.Mapping; + +[UsesVerify] +public class DerivedExistingTargetTypeTest +{ + [Fact] + public Task WithAbstractBaseClassShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapDerivedType] + [MapDerivedType] + public partial void Map(A src, B trg); + """, + "abstract class A { public string BaseValue { get; set; } }", + "abstract class B { public string BaseValue { get; set; } }", + "class ASubType1 : A { public string Value1 { get; set; } }", + "class ASubType2 : A { public string Value1 { get; set; } }", + "class BSubType1 : B { public string Value1 { get; set; } }", + "class BSubType2 : B { public string Value1 { get; set; } }" + ); + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task WithAbstractBaseClassAndNonGenericInterfaceShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapDerivedType(typeof(ASubType1), typeof(BSubType1))] + [MapDerivedType(typeof(ASubType2), typeof(BSubType2))] + public partial void Map(A src, B trg); + """, + "abstract class A { public string BaseValue { get; set; } }", + "abstract class B { public string BaseValue { get; set; } }", + "class ASubType1 : A { public string Value1 { get; set; } }", + "class ASubType2 : A { public string Value2 { get; set; } }", + "class BSubType1 : B { public string Value1 { get; set; } }", + "class BSubType2 : B { public string Value2 { get; set; } }" + ); + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task WithInterfaceShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapDerivedType] + [MapDerivedType] + public partial void Map(A src, B trg); + """, + "interface A { string BaseValue { get; set; } }", + "interface B { string BaseValue { get; set; }}", + "class AImpl1 : A { public string BaseValue { get; set; } public string Value1 { get; set; } }", + "class AImpl2 : A { public string BaseValue { get; set; } public string Value2 { get; set; } }", + "class BImpl1 : B { public string BaseValue { get; set; } public string Value1 { get; set; } }", + "class BImpl2 : B { public string BaseValue { get; set; } public string Value2 { get; set; } }" + ); + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task WithInterfaceSourceNullableShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapDerivedType] + [MapDerivedType] + public partial void Map(A? src, B trg); + """, + "interface A { string BaseValue { get; set; } }", + "interface B { string BaseValue { get; set; }}", + "class AImpl1 : A { public string BaseValue { get; set; } public string Value1 { get; set; } }", + "class AImpl2 : A { public string BaseValue { get; set; } public string Value2 { get; set; } }", + "class BImpl1 : B { public string BaseValue { get; set; } public string Value1 { get; set; } }", + "class BImpl2 : B { public string BaseValue { get; set; } public string Value2 { get; set; } }" + ); + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task WithInterfaceSourceAndTargetNullableShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapDerivedType] + [MapDerivedType] + public partial void Map(A? src, B? trg); + """, + "interface A { string BaseValue { get; set; } }", + "interface B { string BaseValue { get; set; }}", + "class AImpl1 : A { public string BaseValue { get; set; } public string Value1 { get; set; } }", + "class AImpl2 : A { public string BaseValue { get; set; } public string Value2 { get; set; } }", + "class BImpl1 : B { public string BaseValue { get; set; } public string Value1 { get; set; } }", + "class BImpl2 : B { public string BaseValue { get; set; } public string Value2 { get; set; } }" + ); + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public void NotAssignableTargetTypeShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapDerivedType] + public partial void Map(A src, B trg); + """, + "interface A {}", + "interface B {}", + "class AImpl1 : A { }", + "class BImpl1 { }" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic( + DiagnosticDescriptors.DerivedTargetTypeIsNotAssignableToReturnType, + "Derived target type BImpl1 is not assignable to return type B" + ) + .HaveAssertedAllDiagnostics(); + } + + [Fact] + public Task WithBaseTypeConfigShouldWork() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapDerivedType] + [MapDerivedType] + [MapProperty(nameof(A.BaseValueA), nameof(B.BaseValueB)] + public partial void Map(A src, B trg); + """, + "abstract class A { public string BaseValueA { get; set; } }", + "abstract class B { public string BaseValueB { get; set; } }", + "class ASubType1 : A { public string Value1 { get; set; } }", + "class ASubType2 : A { public string Value2 { get; set; } }", + "class BSubType1 : B { public string Value1 { get; set; } }", + "class BSubType2 : B { public string Value2 { get; set; } }" + ); + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public void NotAssignableSourceTypeShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapDerivedType] + public partial void Map(A src, B trg); + """, + "interface A {}", + "interface B {}", + "class AImpl1 { }", + "class BImpl1 : B { }" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic( + DiagnosticDescriptors.DerivedSourceTypeIsNotAssignableToParameterType, + "Derived source type AImpl1 is not assignable to parameter type A" + ) + .HaveAssertedAllDiagnostics(); + } + + [Fact] + public void DuplicatedSourceTypeShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapDerivedType] + [MapDerivedType] + public partial void Map(A src, B trg); + """, + "interface A {}", + "interface B {}", + "class AImpl1 : A { }", + "class BImpl1 : B { }", + "class BImpl2 : B { }" + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic( + DiagnosticDescriptors.DerivedSourceTypeDuplicated, + "Derived source type AImpl1 is specified multiple times, a source type may only be specified once" + ) + .HaveAssertedAllDiagnostics(); + } + + [Fact] + public void NotMappableShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBody( + """ + [MapDerivedType] + public partial void Map(object src, object trg); + """ + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic( + DiagnosticDescriptors.CouldNotCreateMapping, + "Could not create mapping from System.Version to int. Consider implementing the mapping manually." + ) + .HaveAssertedAllDiagnostics(); + } +} diff --git a/test/Riok.Mapperly.Tests/_snapshots/DerivedExistingTargetTypeTest.WithAbstractBaseClassAndNonGenericInterfaceShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/DerivedExistingTargetTypeTest.WithAbstractBaseClassAndNonGenericInterfaceShouldWork#Mapper.g.verified.cs new file mode 100644 index 0000000000..c5a1463bf0 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/DerivedExistingTargetTypeTest.WithAbstractBaseClassAndNonGenericInterfaceShouldWork#Mapper.g.verified.cs @@ -0,0 +1,22 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + public partial void Map(global::A src, global::B trg) + { + switch (src, trg) + { + case (global::ASubType1 source, global::BSubType1 target): + target.Value1 = source.Value1; + target.BaseValue = source.BaseValue; + break; + case (global::ASubType2 source, global::BSubType2 target): + target.Value2 = source.Value2; + target.BaseValue = source.BaseValue; + break; + default: + throw new System.ArgumentException($"Cannot map {src.GetType()} to {trg.GetType()} as there is no known derived type mapping", nameof(src)); + } + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/DerivedExistingTargetTypeTest.WithAbstractBaseClassShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/DerivedExistingTargetTypeTest.WithAbstractBaseClassShouldWork#Mapper.g.verified.cs new file mode 100644 index 0000000000..ad567c88d3 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/DerivedExistingTargetTypeTest.WithAbstractBaseClassShouldWork#Mapper.g.verified.cs @@ -0,0 +1,22 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + public partial void Map(global::A src, global::B trg) + { + switch (src, trg) + { + case (global::ASubType1 source, global::BSubType1 target): + target.Value1 = source.Value1; + target.BaseValue = source.BaseValue; + break; + case (global::ASubType2 source, global::BSubType2 target): + target.Value1 = source.Value1; + target.BaseValue = source.BaseValue; + break; + default: + throw new System.ArgumentException($"Cannot map {src.GetType()} to {trg.GetType()} as there is no known derived type mapping", nameof(src)); + } + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/DerivedExistingTargetTypeTest.WithBaseTypeConfigShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/DerivedExistingTargetTypeTest.WithBaseTypeConfigShouldWork#Mapper.g.verified.cs new file mode 100644 index 0000000000..02bec65def --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/DerivedExistingTargetTypeTest.WithBaseTypeConfigShouldWork#Mapper.g.verified.cs @@ -0,0 +1,22 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + public partial void Map(global::A src, global::B trg) + { + switch (src, trg) + { + case (global::ASubType1 source, global::BSubType1 target): + target.Value1 = source.Value1; + target.BaseValueB = source.BaseValueA; + break; + case (global::ASubType2 source, global::BSubType2 target): + target.Value2 = source.Value2; + target.BaseValueB = source.BaseValueA; + break; + default: + throw new System.ArgumentException($"Cannot map {src.GetType()} to {trg.GetType()} as there is no known derived type mapping", nameof(src)); + } + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/DerivedExistingTargetTypeTest.WithInterfaceShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/DerivedExistingTargetTypeTest.WithInterfaceShouldWork#Mapper.g.verified.cs new file mode 100644 index 0000000000..8b23fa146f --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/DerivedExistingTargetTypeTest.WithInterfaceShouldWork#Mapper.g.verified.cs @@ -0,0 +1,22 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + public partial void Map(global::A src, global::B trg) + { + switch (src, trg) + { + case (global::AImpl1 source, global::BImpl1 target): + target.BaseValue = source.BaseValue; + target.Value1 = source.Value1; + break; + case (global::AImpl2 source, global::BImpl2 target): + target.BaseValue = source.BaseValue; + target.Value2 = source.Value2; + break; + default: + throw new System.ArgumentException($"Cannot map {src.GetType()} to {trg.GetType()} as there is no known derived type mapping", nameof(src)); + } + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/DerivedExistingTargetTypeTest.WithInterfaceSourceAndTargetNullableShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/DerivedExistingTargetTypeTest.WithInterfaceSourceAndTargetNullableShouldWork#Mapper.g.verified.cs new file mode 100644 index 0000000000..c1f830d413 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/DerivedExistingTargetTypeTest.WithInterfaceSourceAndTargetNullableShouldWork#Mapper.g.verified.cs @@ -0,0 +1,24 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + public partial void Map(global::A? src, global::B? trg) + { + if (src == null || trg == null) + return; + switch (src, trg) + { + case (global::AImpl1 source, global::BImpl1 target): + target.BaseValue = source.BaseValue; + target.Value1 = source.Value1; + break; + case (global::AImpl2 source, global::BImpl2 target): + target.BaseValue = source.BaseValue; + target.Value2 = source.Value2; + break; + default: + throw new System.ArgumentException($"Cannot map {src.GetType()} to {trg.GetType()} as there is no known derived type mapping", nameof(src)); + } + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/DerivedExistingTargetTypeTest.WithInterfaceSourceNullableShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/DerivedExistingTargetTypeTest.WithInterfaceSourceNullableShouldWork#Mapper.g.verified.cs new file mode 100644 index 0000000000..42f847da81 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/DerivedExistingTargetTypeTest.WithInterfaceSourceNullableShouldWork#Mapper.g.verified.cs @@ -0,0 +1,24 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + public partial void Map(global::A? src, global::B trg) + { + if (src == null) + return; + switch (src, trg) + { + case (global::AImpl1 source, global::BImpl1 target): + target.BaseValue = source.BaseValue; + target.Value1 = source.Value1; + break; + case (global::AImpl2 source, global::BImpl2 target): + target.BaseValue = source.BaseValue; + target.Value2 = source.Value2; + break; + default: + throw new System.ArgumentException($"Cannot map {src.GetType()} to {trg.GetType()} as there is no known derived type mapping", nameof(src)); + } + } +} \ No newline at end of file