Skip to content

Commit

Permalink
feat: add option to disable specific conversions (#213)
Browse files Browse the repository at this point in the history
  • Loading branch information
latonz authored Jan 9, 2023
1 parent f3e0126 commit 7430c94
Show file tree
Hide file tree
Showing 25 changed files with 355 additions and 16 deletions.
31 changes: 31 additions & 0 deletions docs/docs/02-configuration/10-conversions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Conversions

Mapperly implements several types of automatic conversions.
A list of conversions supported by Mapperly is available [here](../api/riok.mapperly.abstractions.mappingconversiontype#fields).

## Disable all automatic conversions

To disable all conversions supported by Mapperly set `EnabledConversions` to `None`:
```csharp
// highlight-start
[Mapper(EnabledConversions = MappingConversionType.None)]
// highlight-end
public partial class CarMapper
{
...
}
```

## Disable specific automatic conversions

To disable a specific conversion type, set `EnabledConversions` to `All` excluding the conversion type to disable:
```csharp
// this disables conversions using the ToString() method:
// highlight-start
[Mapper(EnabledConversions = MappingConversionType.All & ~MappingConversionType.ToStringMethod)]
// highlight-end
public partial class CarMapper
{
...
}
```
14 changes: 14 additions & 0 deletions src/Riok.Mapperly.Abstractions/MapperAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,18 @@ public sealed class MapperAttribute : Attribute
/// With <c><see cref="UseDeepCloning"/>=false</c>, the array and each person is cloned.
/// </summary>
public bool UseDeepCloning { get; set; }

/// <summary>
/// Enabled conversions which Mapperly automatically implements.
/// By default all supported type conversions are enabled.
/// <example>
/// Eg. to disable all automatically implemented conversions:<br />
/// <c>EnabledConversions = MappingConversionType.None</c>
/// </example>
/// <example>
/// Eg. to disable <c>ToString()</c> method calls:<br />
/// <c>EnabledConversions = MappingConversionType.All &amp; ~MappingConversionType.ToStringMethod</c>
/// </example>
/// </summary>
public MappingConversionType EnabledConversions { get; set; } = MappingConversionType.All;
}
69 changes: 69 additions & 0 deletions src/Riok.Mapperly.Abstractions/MappingConversionType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
namespace Riok.Mapperly.Abstractions;

/// <summary>
/// A <see cref="MappingConversionType"/> represents a type of conversion
/// how one type can be converted into another.
/// </summary>
[Flags]
public enum MappingConversionType
{
/// <summary>
/// None.
/// </summary>
None = 0,

/// <summary>
/// Use the constructor of the target type,
/// which accepts the source type as a single parameter.
/// </summary>
Constructor = 1 << 0,

/// <summary>
/// An implicit cast from the source type to the target type.
/// </summary>
ImplicitCast = 1 << 1,

/// <summary>
/// An explicit cast from the source type to the target type.
/// </summary>
ExplicitCast = 1 << 2,

/// <summary>
/// If the source type is a <see cref="string"/>,
/// uses a a static visible method named `Parse` on the target type
/// with a return type equal to the target type and a string as single parameter.
/// </summary>
ParseMethod = 1 << 3,

/// <summary>
/// If the target type is a <see cref="string"/>,
/// uses the `ToString` method on the source type.
/// </summary>
ToStringMethod = 1 << 4,

/// <summary>
/// If the target is an <see cref="Enum"/>
/// and the source is a <see cref="string"/>,
/// parses the string to match the name of an enum member.
/// </summary>
StringToEnum = 1 << 5,

/// <summary>
/// If the source is an <see cref="Enum"/>
/// and the target is a <see cref="string"/>,
/// uses the name of the enum member to convert it to a string.
/// </summary>
EnumToString = 1 << 6,

/// <summary>
/// If the source is an <see cref="Enum"/>
/// and the target is another <see cref="Enum"/>,
/// map it according to the <see cref="EnumMappingStrategy"/>.
/// </summary>
EnumToEnum = 1 << 7,

/// <summary>
/// Enables all supported conversions.
/// </summary>
All = ~None,
}
13 changes: 13 additions & 0 deletions src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Riok.Mapperly.Abstractions.MapEnumAttribute.IgnoreCase.set -> void
Riok.Mapperly.Abstractions.MapEnumAttribute.MapEnumAttribute(Riok.Mapperly.Abstractions.EnumMappingStrategy strategy) -> void
Riok.Mapperly.Abstractions.MapEnumAttribute.Strategy.get -> Riok.Mapperly.Abstractions.EnumMappingStrategy
Riok.Mapperly.Abstractions.MapperAttribute
Riok.Mapperly.Abstractions.MapperAttribute.EnabledConversions.get -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MapperAttribute.EnabledConversions.set -> void
Riok.Mapperly.Abstractions.MapperAttribute.EnumMappingIgnoreCase.get -> bool
Riok.Mapperly.Abstractions.MapperAttribute.EnumMappingIgnoreCase.set -> void
Riok.Mapperly.Abstractions.MapperAttribute.EnumMappingStrategy.get -> Riok.Mapperly.Abstractions.EnumMappingStrategy
Expand Down Expand Up @@ -45,3 +47,14 @@ Riok.Mapperly.Abstractions.ObjectFactoryAttribute.ObjectFactoryAttribute() -> vo
Riok.Mapperly.Abstractions.PropertyNameMappingStrategy
Riok.Mapperly.Abstractions.PropertyNameMappingStrategy.CaseInsensitive = 1 -> Riok.Mapperly.Abstractions.PropertyNameMappingStrategy
Riok.Mapperly.Abstractions.PropertyNameMappingStrategy.CaseSensitive = 0 -> Riok.Mapperly.Abstractions.PropertyNameMappingStrategy
Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.All = -1 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.Constructor = 1 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.EnumToEnum = 128 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.EnumToString = 64 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.ExplicitCast = 4 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.ImplicitCast = 2 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.None = 0 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.ParseMethod = 8 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.StringToEnum = 32 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.ToStringMethod = 16 -> Riok.Mapperly.Abstractions.MappingConversionType
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;

Expand All @@ -8,6 +9,9 @@ public static class CtorMappingBuilder
{
public static CtorMapping? TryBuildMapping(MappingBuilderContext ctx)
{
if (!ctx.IsConversionEnabled(MappingConversionType.Constructor))
return null;

if (ctx.Target is not INamedTypeSymbol namedTarget)
return null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public static class EnumMappingBuilder
// one is an enum, other may be an underlying type (eg. int)
if (!sourceIsEnum || !targetIsEnum)
{
return ctx.FindOrBuildMapping(sourceEnumType ?? ctx.Source, targetEnumType ?? ctx.Target) is { } delegateMapping
return ctx.IsConversionEnabled(MappingConversionType.ExplicitCast)
&& ctx.FindOrBuildMapping(sourceEnumType ?? ctx.Source, targetEnumType ?? ctx.Target) is { } delegateMapping
? new CastMapping(ctx.Source, ctx.Target, delegateMapping)
: null;
}
Expand All @@ -29,6 +30,9 @@ public static class EnumMappingBuilder
if (SymbolEqualityComparer.IncludeNullability.Equals(ctx.Source, ctx.Target))
return new DirectAssignmentMapping(ctx.Source);

if (!ctx.IsConversionEnabled(MappingConversionType.EnumToEnum))
return null;

// map enums by strategy
var config = ctx.GetConfigurationOrDefault<MapEnumAttribute>();
return config.Strategy switch
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;

Expand All @@ -8,6 +9,9 @@ public static class EnumToStringMappingBuilder
{
public static TypeMapping? TryBuildMapping(MappingBuilderContext ctx)
{
if (!ctx.IsConversionEnabled(MappingConversionType.EnumToString))
return null;

if (ctx.Target.SpecialType != SpecialType.System_String || !ctx.Source.IsEnum())
return null;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis.CSharp;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;

Expand All @@ -8,6 +9,9 @@ public static class ExplicitCastMappingBuilder
{
public static CastMapping? TryBuildMapping(MappingBuilderContext ctx)
{
if (!ctx.IsConversionEnabled(MappingConversionType.ExplicitCast))
return null;

if (ctx.MapperConfiguration.UseDeepCloning && !ctx.Source.IsImmutable() && !ctx.Target.IsImmutable())
return null;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis.CSharp;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;

Expand All @@ -8,6 +9,9 @@ public static class ImplicitCastMappingBuilder
{
public static CastMapping? TryBuildMapping(MappingBuilderContext ctx)
{
if (!ctx.IsConversionEnabled(MappingConversionType.ImplicitCast))
return null;

if (ctx.MapperConfiguration.UseDeepCloning && !ctx.Source.IsImmutable() && !ctx.Target.IsImmutable())
return null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public static class NewInstanceObjectPropertyMappingBuilder
if (ctx.Target is not INamedTypeSymbol namedTarget || namedTarget.Constructors.All(x => !x.IsAccessible()))
return null;

if (ctx.Source.IsEnum() || ctx.Target.IsEnum())
return null;

return new NewInstanceObjectPropertyMapping(ctx.Source, ctx.Target.NonNullable());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;

Expand All @@ -10,6 +11,9 @@ public static class ParseMappingBuilder

public static StaticMethodMapping? TryBuildMapping(MappingBuilderContext ctx)
{
if (!ctx.IsConversionEnabled(MappingConversionType.ParseMethod))
return null;

if (ctx.Source.SpecialType != SpecialType.System_String)
return null;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.Mappings;

namespace Riok.Mapperly.Descriptors.MappingBuilder;
Expand All @@ -7,6 +8,9 @@ public static class SpecialTypeMappingBuilder
{
public static TypeMapping? TryBuildMapping(MappingBuilderContext ctx)
{
if (!ctx.IsConversionEnabled(MappingConversionType.ExplicitCast))
return null;

return ctx.Target.SpecialType switch
{
SpecialType.System_Object when ctx.MapperConfiguration.UseDeepCloning
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ public static class StringToEnumMappingBuilder
{
public static TypeMapping? TryBuildMapping(MappingBuilderContext ctx)
{
if (!ctx.IsConversionEnabled(MappingConversionType.StringToEnum))
return null;

if (ctx.Source.SpecialType != SpecialType.System_String || !ctx.Target.IsEnum())
return null;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.Mappings;

namespace Riok.Mapperly.Descriptors.MappingBuilder;
Expand All @@ -8,6 +9,9 @@ public static class ToStringMappingBuilder

public static TypeMapping? TryBuildMapping(MappingBuilderContext ctx)
{
if (!ctx.IsConversionEnabled(MappingConversionType.ToStringMethod))
return null;

return ctx.Target.SpecialType == SpecialType.System_String
? new SourceObjectMethodMapping(ctx.Source, ctx.Target, nameof(ToString))
: null;
Expand Down
3 changes: 3 additions & 0 deletions src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ public SimpleMappingBuilderContext(DescriptorBuilder builder)

public MapperAttribute MapperConfiguration => _builder.MapperConfiguration;

public bool IsConversionEnabled(MappingConversionType conversionType)
=> MapperConfiguration.EnabledConversions.HasFlag(conversionType);

public INamedTypeSymbol GetTypeSymbol(Type type)
=> Compilation.GetTypeByMetadataName(type.FullName ?? throw new InvalidOperationException("Could not get name of type " + type))
?? throw new InvalidOperationException("Could not get type " + type.FullName);
Expand Down
27 changes: 27 additions & 0 deletions test/Riok.Mapperly.Tests/Mapping/CastTest.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Diagnostics;

namespace Riok.Mapperly.Tests.Mapping;

public class CastTest
Expand Down Expand Up @@ -344,4 +347,28 @@ public void OperatorImplicitStructWithMutableStructTargetDeepCloning()
.HaveSingleMethodBody(@"var target = new B();
return target;");
}

[Fact]
public void ImplicitCastMappingDisabledShouldDiagnostic()
{
var source = TestSourceBuilder.Mapping(
"byte",
"int",
TestSourceBuilderOptions.WithDisabledMappingConversion(MappingConversionType.ImplicitCast));
TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
.Should()
.HaveDiagnostic(new(DiagnosticDescriptors.CouldNotCreateMapping));
}

[Fact]
public void ExplicitCastMappingDisabledShouldDiagnostic()
{
var source = TestSourceBuilder.Mapping(
"int",
"byte",
TestSourceBuilderOptions.WithDisabledMappingConversion(MappingConversionType.ExplicitCast));
TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
.Should()
.HaveDiagnostic(new(DiagnosticDescriptors.CouldNotCreateMapping));
}
}
16 changes: 16 additions & 0 deletions test/Riok.Mapperly.Tests/Mapping/CtorTest.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Diagnostics;

namespace Riok.Mapperly.Tests.Mapping;

public class CtorTest
Expand Down Expand Up @@ -25,4 +28,17 @@ public void CtorCustomStruct()
.Should()
.HaveSingleMethodBody("return new A(source);");
}

[Fact]
public void CtorMappingDisabledShouldDiagnostic()
{
var source = TestSourceBuilder.Mapping(
"A",
"string",
TestSourceBuilderOptions.WithDisabledMappingConversion(MappingConversionType.ToStringMethod),
"class A { public A(string x) {} }");
TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
.Should()
.HaveDiagnostic(new(DiagnosticDescriptors.CouldNotCreateMapping));
}
}
Loading

0 comments on commit 7430c94

Please sign in to comment.