From e3b954b4f7f746da9cdeb880b25b5f048097c9d6 Mon Sep 17 00:00:00 2001 From: Timothy Makkison Date: Mon, 28 Aug 2023 22:40:19 +0100 Subject: [PATCH] feat: add strict mapping --- .../MapperAttribute.cs | 6 + .../MapperRequiredMappingAttribute.cs | 22 ++ .../PublicAPI.Shipped.txt | 10 + .../RequiredMappingStrategy.cs | 28 ++ .../Configuration/MapperConfiguration.cs | 6 + .../MapperConfigurationMerger.cs | 5 + .../MapperConfigurationReader.cs | 14 +- .../PropertiesMappingConfiguration.cs | 3 +- .../MembersMappingBuilderContext.cs | 3 + ...wInstanceObjectMemberMappingBodyBuilder.cs | 41 ++- .../ObjectMemberMappingBodyBuilder.cs | 5 +- .../Mapper/StaticTestMapper.cs | 1 + .../Models/TestObject.cs | 2 + ...t.RunMappingShouldWork_NET6_0.verified.txt | 3 + ...t.RunMappingShouldWork_NET6_0.verified.txt | 1 + ...nsionMappingShouldWork_NET6_0.verified.txt | 1 + ...t.RunMappingShouldWork_NET6_0.verified.txt | 1 + .../Helpers/MapperConfigurationBuilderTest.cs | 4 + .../Mapping/RequiredMappingTest.cs | 242 ++++++++++++++++++ test/Riok.Mapperly.Tests/TestSourceBuilder.cs | 1 + .../TestSourceBuilderOptions.cs | 6 +- 21 files changed, 388 insertions(+), 17 deletions(-) create mode 100644 src/Riok.Mapperly.Abstractions/MapperRequiredMappingAttribute.cs create mode 100644 src/Riok.Mapperly.Abstractions/RequiredMappingStrategy.cs create mode 100644 test/Riok.Mapperly.Tests/Mapping/RequiredMappingTest.cs diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs index a8c7de5750..f6832b3e5d 100644 --- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs +++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs @@ -86,4 +86,10 @@ public class MapperAttribute : Attribute /// Defaults to . /// public IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy { get; set; } = IgnoreObsoleteMembersStrategy.None; + + /// + /// Defines the strategy used when emitting warnings for unmapped members. + /// By default this is , emitting warnings for unmapped source and target members. + /// + public RequiredMappingStrategy RequiredMappingStrategy { get; set; } = RequiredMappingStrategy.Both; } diff --git a/src/Riok.Mapperly.Abstractions/MapperRequiredMappingAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperRequiredMappingAttribute.cs new file mode 100644 index 0000000000..45fed9ecc6 --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/MapperRequiredMappingAttribute.cs @@ -0,0 +1,22 @@ +namespace Riok.Mapperly.Abstractions; + +/// +/// Defines the strategy used when emitting warnings for unmapped members. +/// +[AttributeUsage(AttributeTargets.Method)] +public sealed class MapperRequiredMappingAttribute : Attribute +{ + /// + /// Defines the strategy used when emitting warnings for unmapped members. + /// + /// The strategy used when emitting warnings for unmapped members. + public MapperRequiredMappingAttribute(RequiredMappingStrategy requiredMappingStrategy) + { + RequiredMappingStrategy = requiredMappingStrategy; + } + + /// + /// The strategy used when emitting warnings for unmapped members. + /// + public RequiredMappingStrategy RequiredMappingStrategy { get; } +} diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt index 80b1edf4ec..80be9349f8 100644 --- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt +++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt @@ -109,3 +109,13 @@ Riok.Mapperly.Abstractions.UseStaticMapperAttribute Riok.Mapperly.Abstractions.UseStaticMapperAttribute.UseStaticMapperAttribute(System.Type! mapperType) -> void Riok.Mapperly.Abstractions.UseStaticMapperAttribute Riok.Mapperly.Abstractions.UseStaticMapperAttribute.UseStaticMapperAttribute() -> void +Riok.Mapperly.Abstractions.MapperAttribute.RequiredMappingStrategy.get -> Riok.Mapperly.Abstractions.RequiredMappingStrategy +Riok.Mapperly.Abstractions.MapperAttribute.RequiredMappingStrategy.set -> void +Riok.Mapperly.Abstractions.MapperRequiredMappingAttribute +Riok.Mapperly.Abstractions.MapperRequiredMappingAttribute.MapperRequiredMappingAttribute(Riok.Mapperly.Abstractions.RequiredMappingStrategy requiredMappingStrategy) -> void +Riok.Mapperly.Abstractions.MapperRequiredMappingAttribute.RequiredMappingStrategy.get -> Riok.Mapperly.Abstractions.RequiredMappingStrategy +Riok.Mapperly.Abstractions.RequiredMappingStrategy +Riok.Mapperly.Abstractions.RequiredMappingStrategy.Both = -1 -> Riok.Mapperly.Abstractions.RequiredMappingStrategy +Riok.Mapperly.Abstractions.RequiredMappingStrategy.None = 0 -> Riok.Mapperly.Abstractions.RequiredMappingStrategy +Riok.Mapperly.Abstractions.RequiredMappingStrategy.Source = 1 -> Riok.Mapperly.Abstractions.RequiredMappingStrategy +Riok.Mapperly.Abstractions.RequiredMappingStrategy.Target = 2 -> Riok.Mapperly.Abstractions.RequiredMappingStrategy diff --git a/src/Riok.Mapperly.Abstractions/RequiredMappingStrategy.cs b/src/Riok.Mapperly.Abstractions/RequiredMappingStrategy.cs new file mode 100644 index 0000000000..b7593fba1b --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/RequiredMappingStrategy.cs @@ -0,0 +1,28 @@ +namespace Riok.Mapperly.Abstractions; + +/// +/// Defines the strategy used when emitting warnings for unmapped members. +/// +[Flags] +public enum RequiredMappingStrategy +{ + /// + /// Warnings are not emitted for unmapped source or target members. + /// + None = 0, + + /// + /// Warnings are emitted for both unmapped source and target members. + /// + Both = ~None, + + /// + /// Warnings are emitted for unmapped source members but not for target members. + /// + Source = 1 << 0, + + /// + /// Warnings are emitted for unmapped target members but not for source members. + /// + Target = 1 << 1 +} diff --git a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs index b234aa9e13..270049f41a 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs @@ -92,4 +92,10 @@ public record MapperConfiguration /// Defaults to . /// public IgnoreObsoleteMembersStrategy? IgnoreObsoleteMembersStrategy { get; init; } + + /// + /// Defines the strategy used when emitting warnings for unmapped members. + /// By default this is , emitting warnings for unmapped source and target members. + /// + public RequiredMappingStrategy? RequiredMappingStrategy { get; init; } } diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs index 5e386056f9..f83f5843b0 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs @@ -46,6 +46,11 @@ public static MapperAttribute Merge(MapperConfiguration mapperConfiguration, Map ?? defaultMapperConfiguration.IgnoreObsoleteMembersStrategy ?? mapper.IgnoreObsoleteMembersStrategy; + mapper.RequiredMappingStrategy = + mapperConfiguration.RequiredMappingStrategy + ?? defaultMapperConfiguration.RequiredMappingStrategy + ?? mapper.RequiredMappingStrategy; + return mapper; } } diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs index 795eb0ae9b..31eb60f357 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs @@ -32,7 +32,8 @@ MapperConfiguration defaultMapperConfiguration Array.Empty(), Array.Empty(), Array.Empty(), - Mapper.IgnoreObsoleteMembersStrategy + Mapper.IgnoreObsoleteMembersStrategy, + Mapper.RequiredMappingStrategy ), Array.Empty() ); @@ -75,8 +76,17 @@ private PropertiesMappingConfiguration BuildPropertiesConfig(IMethodSymbol metho var ignoreObsolete = _dataAccessor.Access(method).FirstOrDefault() is not { } methodIgnore ? _defaultConfiguration.Properties.IgnoreObsoleteMembersStrategy : methodIgnore.IgnoreObsoleteStrategy; + var requiredMapping = _dataAccessor.Access(method).FirstOrDefault() is not { } methodWarnUnmapped + ? _defaultConfiguration.Properties.RequiredMappingStrategy + : methodWarnUnmapped.RequiredMappingStrategy; - return new PropertiesMappingConfiguration(ignoredSourceProperties, ignoredTargetProperties, explicitMappings, ignoreObsolete); + return new PropertiesMappingConfiguration( + ignoredSourceProperties, + ignoredTargetProperties, + explicitMappings, + ignoreObsolete, + requiredMapping + ); } private EnumMappingConfiguration BuildEnumConfig(MappingConfigurationReference configRef) diff --git a/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs b/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs index f991cc536a..9d65612eb8 100644 --- a/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs @@ -6,5 +6,6 @@ public record PropertiesMappingConfiguration( IReadOnlyCollection IgnoredSources, IReadOnlyCollection IgnoredTargets, IReadOnlyCollection ExplicitMappings, - IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy + IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy, + RequiredMappingStrategy RequiredMappingStrategy ); diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs index d91d570023..fe11e2cc21 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersMappingBuilderContext.cs @@ -166,6 +166,9 @@ private void AddUnmatchedTargetMembersDiagnostics() private void AddUnmatchedSourceMembersDiagnostics() { + if (!BuilderContext.Configuration.Properties.RequiredMappingStrategy.HasFlag(RequiredMappingStrategy.Source)) + return; + foreach (var sourceMemberName in _unmappedSourceMemberNames) { BuilderContext.ReportDiagnostic( diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs index 0ca922df48..1154db4e38 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewInstanceObjectMemberMappingBodyBuilder.cs @@ -59,12 +59,25 @@ out var sourceMemberPath ) ) { - ctx.BuilderContext.ReportDiagnostic( - targetMember.IsRequired ? DiagnosticDescriptors.RequiredMemberNotMapped : DiagnosticDescriptors.SourceMemberNotFound, - targetMember.Name, - ctx.Mapping.TargetType, - ctx.Mapping.SourceType - ); + if (targetMember.IsRequired) + { + ctx.BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.RequiredMemberNotMapped, + targetMember.Name, + ctx.Mapping.TargetType, + ctx.Mapping.SourceType + ); + } + else if (ctx.BuilderContext.Configuration.Properties.RequiredMappingStrategy.HasFlag(RequiredMappingStrategy.Target)) + { + ctx.BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.SourceMemberNotFound, + targetMember.Name, + ctx.Mapping.TargetType, + ctx.Mapping.SourceType + ); + } + continue; } @@ -104,12 +117,16 @@ IReadOnlyCollection memberConfigs !ctx.BuilderContext.SymbolAccessor.TryFindMemberPath(ctx.Mapping.SourceType, memberConfig.Source.Path, out var sourceMemberPath) ) { - ctx.BuilderContext.ReportDiagnostic( - DiagnosticDescriptors.SourceMemberNotFound, - targetMember.Name, - ctx.Mapping.TargetType, - ctx.Mapping.SourceType - ); + if (ctx.BuilderContext.Configuration.Properties.RequiredMappingStrategy.HasFlag(RequiredMappingStrategy.Target)) + { + ctx.BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.SourceMemberNotFound, + targetMember.Name, + ctx.Mapping.TargetType, + ctx.Mapping.SourceType + ); + } + return; } diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs index d30e8b1a4d..164276c191 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/ObjectMemberMappingBodyBuilder.cs @@ -54,7 +54,10 @@ out var sourceMemberPath continue; } - if (targetMember.CanSet) + if ( + targetMember.CanSet + && ctx.BuilderContext.Configuration.Properties.RequiredMappingStrategy.HasFlag(RequiredMappingStrategy.Target) + ) { ctx.BuilderContext.ReportDiagnostic( DiagnosticDescriptors.SourceMemberNotFound, diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs index 1f1c71ec7a..a5623620ac 100644 --- a/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/StaticTestMapper.cs @@ -30,6 +30,7 @@ public static partial class StaticTestMapper [MapperIgnoreSource(nameof(TestObject.IgnoredIntValue))] [MapperIgnoreTarget(nameof(TestObjectDto.IgnoredStringValue))] [MapperIgnoreObsoleteMembers] + [MapperRequiredMapping(RequiredMappingStrategy.Target)] public static partial TestObjectDto MapToDtoExt(this TestObject src); public static TestObjectDto MapToDto(TestObject src) diff --git a/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs b/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs index f2a273ba73..21e1d94064 100644 --- a/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs +++ b/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs @@ -26,6 +26,8 @@ public TestObject(int ctorValue, int unknownValue = 10, int ctorValue2 = 100) public int RequiredValue { get; init; } #endif + public int UnmappedValue => 10; + public string StringValue { get; set; } = string.Empty; public string RenamedStringValue { get; set; } = string.Empty; diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.RunMappingShouldWork_NET6_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.RunMappingShouldWork_NET6_0.verified.txt index 97299a4121..a6fbf2c826 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.RunMappingShouldWork_NET6_0.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningMapperTest.RunMappingShouldWork_NET6_0.verified.txt @@ -4,6 +4,7 @@ IntValue: 10, IntInitOnlyValue: 3, RequiredValue: 4, + UnmappedValue: 10, StringValue: fooBar, RenamedStringValue: fooBar2, Flattening: { @@ -27,6 +28,7 @@ CtorValue: 5, CtorValue2: 100, RequiredValue: 4, + UnmappedValue: 10, StringValue: , RenamedStringValue: , Flattening: {}, @@ -45,6 +47,7 @@ CtorValue2: 100, IntValue: 99, RequiredValue: 98, + UnmappedValue: 10, StringValue: , RenamedStringValue: , Flattening: {}, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.RunMappingShouldWork_NET6_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.RunMappingShouldWork_NET6_0.verified.txt index 128e6b3084..23d9668a6f 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.RunMappingShouldWork_NET6_0.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/MapperTest.RunMappingShouldWork_NET6_0.verified.txt @@ -54,6 +54,7 @@ CtorValue2: 100, IntValue: 99, RequiredValue: 98, + UnmappedValue: 10, StringValue: , RenamedStringValue: , Flattening: {}, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunExtensionMappingShouldWork_NET6_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunExtensionMappingShouldWork_NET6_0.verified.txt index 6b3cde8e51..257a3273fd 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunExtensionMappingShouldWork_NET6_0.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunExtensionMappingShouldWork_NET6_0.verified.txt @@ -49,6 +49,7 @@ CtorValue2: 100, IntValue: 99, RequiredValue: 98, + UnmappedValue: 10, StringValue: , RenamedStringValue: , Flattening: {}, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunMappingShouldWork_NET6_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunMappingShouldWork_NET6_0.verified.txt index 4812c0f332..9f4d61564e 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunMappingShouldWork_NET6_0.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/StaticMapperTest.RunMappingShouldWork_NET6_0.verified.txt @@ -54,6 +54,7 @@ CtorValue2: 100, IntValue: 99, RequiredValue: 98, + UnmappedValue: 10, StringValue: , RenamedStringValue: , Flattening: {}, diff --git a/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs b/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs index 1e1e06617f..4b153f7096 100644 --- a/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs +++ b/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs @@ -21,6 +21,7 @@ public void ShouldMergeMapperConfigurations() EnabledConversions = MappingConversionType.Dictionary, UseReferenceHandling = false, IgnoreObsoleteMembersStrategy = IgnoreObsoleteMembersStrategy.Target, + RequiredMappingStrategy = RequiredMappingStrategy.Target, }; var mapper = MapperConfigurationMerger.Merge(mapperConfiguration, defaultMapperConfiguration); @@ -34,6 +35,7 @@ public void ShouldMergeMapperConfigurations() mapper.EnabledConversions.Should().Be(MappingConversionType.Constructor); mapper.UseReferenceHandling.Should().BeTrue(); mapper.IgnoreObsoleteMembersStrategy.Should().Be(IgnoreObsoleteMembersStrategy.Source); + mapper.RequiredMappingStrategy.Should().Be(RequiredMappingStrategy.Source); } [Fact] @@ -51,6 +53,7 @@ public void ShouldMergeMapperConfigurationsWithEmptyDefaultMapperConfiguration() mapper.EnabledConversions.Should().Be(MappingConversionType.Constructor); mapper.UseReferenceHandling.Should().BeTrue(); mapper.IgnoreObsoleteMembersStrategy.Should().Be(IgnoreObsoleteMembersStrategy.Source); + mapper.RequiredMappingStrategy.Should().Be(RequiredMappingStrategy.Source); } [Fact] @@ -84,6 +87,7 @@ private MapperConfiguration NewMapperConfiguration() EnabledConversions = MappingConversionType.Constructor, UseReferenceHandling = true, IgnoreObsoleteMembersStrategy = IgnoreObsoleteMembersStrategy.Source, + RequiredMappingStrategy = RequiredMappingStrategy.Source, }; } } diff --git a/test/Riok.Mapperly.Tests/Mapping/RequiredMappingTest.cs b/test/Riok.Mapperly.Tests/Mapping/RequiredMappingTest.cs new file mode 100644 index 0000000000..476f010c9a --- /dev/null +++ b/test/Riok.Mapperly.Tests/Mapping/RequiredMappingTest.cs @@ -0,0 +1,242 @@ +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Diagnostics; + +namespace Riok.Mapperly.Tests.Mapping; + +[UsesVerify] +public class RequiredMappingTest +{ + [Fact] + public void ClassAttributeRequiredMappingSource() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + TestSourceBuilderOptions.WithRequiredMappingStrategy(RequiredMappingStrategy.Source), + "class A { public int Value { get; set; } public int UnmappedSource { get; set; } }", + "class B { public int Value { get; set; } public int UnmappedTarget { get; set; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped) + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + return target; + """ + ); + } + + [Fact] + public void ClassAttributeRequiredMappingTarget() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + TestSourceBuilderOptions.WithRequiredMappingStrategy(RequiredMappingStrategy.Target), + "class A { public int Value { get; set; } public int UnmappedSource { get; set; } }", + "class B { public int Value { get; set; } public int UnmappedTarget { get; set; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound) + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + return target; + """ + ); + } + + [Fact] + public void ClassAttributeRequiredMappingBoth() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + TestSourceBuilderOptions.WithRequiredMappingStrategy(RequiredMappingStrategy.Both), + "class A { public int Value { get; set; } public int UnmappedSource { get; set; } }", + "class B { public int Value { get; set; } public int UnmappedTarget { get; set; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound) + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped) + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + return target; + """ + ); + } + + [Fact] + public void ClassAttributeRequiredMappingNone() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + TestSourceBuilderOptions.WithRequiredMappingStrategy(RequiredMappingStrategy.None), + "class A { public int Value { get; set; } public int UnmappedSource { get; set; } }", + "class B { public int Value { get; set; } public int UnmappedTarget { get; set; } }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + return target; + """ + ); + } + + [Fact] + public void MethodAttributeRequiredMappingSource() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapperRequiredMappingAttribute(RequiredMappingStrategy.Source)] partial B Map(A source);", + "class A { public int Value { get; set; } public int UnmappedSource { get; set; } }", + "class B { public int Value { get; set; } public int UnmappedTarget { get; set; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped) + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + return target; + """ + ); + } + + [Fact] + public void MethodAttributeRequiredMappingTarget() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapperRequiredMappingAttribute(RequiredMappingStrategy.Target)] partial B Map(A source);", + "class A { public int Value { get; set; } public int UnmappedSource { get; set; } }", + "class B { public int Value { get; set; } public int UnmappedTarget { get; set; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound) + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + return target; + """ + ); + } + + [Fact] + public void MethodAttributeRequiredMappingBoth() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapperRequiredMappingAttribute(RequiredMappingStrategy.Both)] partial B Map(A source);", + "class A { public int Value { get; set; } public int UnmappedSource { get; set; } }", + "class B { public int Value { get; set; } public int UnmappedTarget { get; set; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound) + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped) + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + return target; + """ + ); + } + + [Fact] + public void MethodAttributeRequiredMappingNone() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapperRequiredMappingAttribute(RequiredMappingStrategy.None)] partial B Map(A source);", + "class A { public int Value { get; set; } public int UnmappedSource { get; set; } }", + "class B { public int Value { get; set; } public int UnmappedTarget { get; set; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + return target; + """ + ); + } + + [Fact] + public void MethodAttributeOverridesClass() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapperRequiredMappingAttribute(RequiredMappingStrategy.Target)] partial B Map(A source);", + TestSourceBuilderOptions.WithRequiredMappingStrategy(RequiredMappingStrategy.Source), + "class A { public int Value { get; set; } public int UnmappedSource { get; set; } }", + "class B { public int Value { get; set; } public int UnmappedTarget { get; set; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowInfoDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound) + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody( + """ + var target = new global::B(); + target.Value = source.Value; + return target; + """ + ); + } + + [Fact] + public void AllowUnmappedTargetTupleShouldDiagnostic() + { + var source = TestSourceBuilder.Mapping( + "A", + "(int MyValue, int Value)", + TestSourceBuilderOptions.WithRequiredMappingStrategy(RequiredMappingStrategy.Target), + "class A { public int Value { get; set; } }" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowAllDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.NoConstructorFound) + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound) + .HaveAssertedAllDiagnostics(); + } +} diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs index 0d39237741..41f2ad5545 100644 --- a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs +++ b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs @@ -90,6 +90,7 @@ private static string BuildAttribute(TestSourceBuilderOptions options) Attribute(options.EnumMappingStrategy), Attribute(options.EnumMappingIgnoreCase), Attribute(options.IgnoreObsoleteMembersStrategy), + Attribute(options.RequiredMappingStrategy), }.WhereNotNull(); return $"[Mapper({string.Join(", ", attrs)})]"; diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs index 69c28d2f06..8824846829 100644 --- a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs +++ b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs @@ -14,7 +14,8 @@ public record TestSourceBuilderOptions( MappingConversionType? EnabledConversions = null, EnumMappingStrategy? EnumMappingStrategy = null, bool? EnumMappingIgnoreCase = null, - IgnoreObsoleteMembersStrategy? IgnoreObsoleteMembersStrategy = null + IgnoreObsoleteMembersStrategy? IgnoreObsoleteMembersStrategy = null, + RequiredMappingStrategy? RequiredMappingStrategy = null ) { public const string DefaultMapperClassName = "Mapper"; @@ -26,6 +27,9 @@ public record TestSourceBuilderOptions( public static TestSourceBuilderOptions WithIgnoreObsolete(IgnoreObsoleteMembersStrategy ignoreObsoleteStrategy) => new(IgnoreObsoleteMembersStrategy: ignoreObsoleteStrategy); + public static TestSourceBuilderOptions WithRequiredMappingStrategy(RequiredMappingStrategy requiredMappingStrategy) => + new(RequiredMappingStrategy: requiredMappingStrategy); + public static TestSourceBuilderOptions WithDisabledMappingConversion(params MappingConversionType[] conversionTypes) { var enabled = MappingConversionType.All;