Skip to content

Commit

Permalink
feat: add strict mapping (#700)
Browse files Browse the repository at this point in the history
  • Loading branch information
TimothyMakkison authored Oct 11, 2023
1 parent 6322377 commit 9d6c21a
Show file tree
Hide file tree
Showing 21 changed files with 388 additions and 17 deletions.
6 changes: 6 additions & 0 deletions src/Riok.Mapperly.Abstractions/MapperAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,10 @@ public class MapperAttribute : Attribute
/// Defaults to <see cref="IgnoreObsoleteMembersStrategy.None"/>.
/// </summary>
public IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy { get; set; } = IgnoreObsoleteMembersStrategy.None;

/// <summary>
/// Defines the strategy used when emitting warnings for unmapped members.
/// By default this is <see cref="RequiredMappingStrategy.Both"/>, emitting warnings for unmapped source and target members.
/// </summary>
public RequiredMappingStrategy RequiredMappingStrategy { get; set; } = RequiredMappingStrategy.Both;
}
22 changes: 22 additions & 0 deletions src/Riok.Mapperly.Abstractions/MapperRequiredMappingAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Riok.Mapperly.Abstractions;

/// <summary>
/// Defines the strategy used when emitting warnings for unmapped members.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class MapperRequiredMappingAttribute : Attribute
{
/// <summary>
/// Defines the strategy used when emitting warnings for unmapped members.
/// </summary>
/// <param name="requiredMappingStrategy">The strategy used when emitting warnings for unmapped members.</param>
public MapperRequiredMappingAttribute(RequiredMappingStrategy requiredMappingStrategy)
{
RequiredMappingStrategy = requiredMappingStrategy;
}

/// <summary>
/// The strategy used when emitting warnings for unmapped members.
/// </summary>
public RequiredMappingStrategy RequiredMappingStrategy { get; }
}
10 changes: 10 additions & 0 deletions src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,13 @@ Riok.Mapperly.Abstractions.UseStaticMapperAttribute
Riok.Mapperly.Abstractions.UseStaticMapperAttribute.UseStaticMapperAttribute(System.Type! mapperType) -> void
Riok.Mapperly.Abstractions.UseStaticMapperAttribute<T>
Riok.Mapperly.Abstractions.UseStaticMapperAttribute<T>.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
28 changes: 28 additions & 0 deletions src/Riok.Mapperly.Abstractions/RequiredMappingStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace Riok.Mapperly.Abstractions;

/// <summary>
/// Defines the strategy used when emitting warnings for unmapped members.
/// </summary>
[Flags]
public enum RequiredMappingStrategy
{
/// <summary>
/// Warnings are not emitted for unmapped source or target members.
/// </summary>
None = 0,

/// <summary>
/// Warnings are emitted for both unmapped source and target members.
/// </summary>
Both = ~None,

/// <summary>
/// Warnings are emitted for unmapped source members but not for target members.
/// </summary>
Source = 1 << 0,

/// <summary>
/// Warnings are emitted for unmapped target members but not for source members.
/// </summary>
Target = 1 << 1
}
6 changes: 6 additions & 0 deletions src/Riok.Mapperly/Configuration/MapperConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,10 @@ public record MapperConfiguration
/// Defaults to <see cref="IgnoreObsoleteMembersStrategy.None"/>.
/// </summary>
public IgnoreObsoleteMembersStrategy? IgnoreObsoleteMembersStrategy { get; init; }

/// <summary>
/// Defines the strategy used when emitting warnings for unmapped members.
/// By default this is <see cref="RequiredMappingStrategy.Both"/>, emitting warnings for unmapped source and target members.
/// </summary>
public RequiredMappingStrategy? RequiredMappingStrategy { get; init; }
}
5 changes: 5 additions & 0 deletions src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
14 changes: 12 additions & 2 deletions src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ MapperConfiguration defaultMapperConfiguration
Array.Empty<string>(),
Array.Empty<string>(),
Array.Empty<PropertyMappingConfiguration>(),
Mapper.IgnoreObsoleteMembersStrategy
Mapper.IgnoreObsoleteMembersStrategy,
Mapper.RequiredMappingStrategy
),
Array.Empty<DerivedTypeMappingConfiguration>()
);
Expand Down Expand Up @@ -75,8 +76,17 @@ private PropertiesMappingConfiguration BuildPropertiesConfig(IMethodSymbol metho
var ignoreObsolete = _dataAccessor.Access<MapperIgnoreObsoleteMembersAttribute>(method).FirstOrDefault() is not { } methodIgnore
? _defaultConfiguration.Properties.IgnoreObsoleteMembersStrategy
: methodIgnore.IgnoreObsoleteStrategy;
var requiredMapping = _dataAccessor.Access<MapperRequiredMappingAttribute>(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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ public record PropertiesMappingConfiguration(
IReadOnlyCollection<string> IgnoredSources,
IReadOnlyCollection<string> IgnoredTargets,
IReadOnlyCollection<PropertyMappingConfiguration> ExplicitMappings,
IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy
IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy,
RequiredMappingStrategy RequiredMappingStrategy
);
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -104,12 +117,16 @@ IReadOnlyCollection<PropertyMappingConfiguration> 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
IntValue: 10,
IntInitOnlyValue: 3,
RequiredValue: 4,
UnmappedValue: 10,
StringValue: fooBar,
RenamedStringValue: fooBar2,
Flattening: {
Expand All @@ -27,6 +28,7 @@
CtorValue: 5,
CtorValue2: 100,
RequiredValue: 4,
UnmappedValue: 10,
StringValue: ,
RenamedStringValue: ,
Flattening: {},
Expand All @@ -45,6 +47,7 @@
CtorValue2: 100,
IntValue: 99,
RequiredValue: 98,
UnmappedValue: 10,
StringValue: ,
RenamedStringValue: ,
Flattening: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
CtorValue2: 100,
IntValue: 99,
RequiredValue: 98,
UnmappedValue: 10,
StringValue: ,
RenamedStringValue: ,
Flattening: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
CtorValue2: 100,
IntValue: 99,
RequiredValue: 98,
UnmappedValue: 10,
StringValue: ,
RenamedStringValue: ,
Flattening: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
CtorValue2: 100,
IntValue: 99,
RequiredValue: 98,
UnmappedValue: 10,
StringValue: ,
RenamedStringValue: ,
Flattening: {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public void ShouldMergeMapperConfigurations()
EnabledConversions = MappingConversionType.Dictionary,
UseReferenceHandling = false,
IgnoreObsoleteMembersStrategy = IgnoreObsoleteMembersStrategy.Target,
RequiredMappingStrategy = RequiredMappingStrategy.Target,
};

var mapper = MapperConfigurationMerger.Merge(mapperConfiguration, defaultMapperConfiguration);
Expand All @@ -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]
Expand All @@ -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]
Expand Down Expand Up @@ -84,6 +87,7 @@ private MapperConfiguration NewMapperConfiguration()
EnabledConversions = MappingConversionType.Constructor,
UseReferenceHandling = true,
IgnoreObsoleteMembersStrategy = IgnoreObsoleteMembersStrategy.Source,
RequiredMappingStrategy = RequiredMappingStrategy.Source,
};
}
}
Loading

0 comments on commit 9d6c21a

Please sign in to comment.