diff --git a/docs/docs/02-configuration/10-conversions.md b/docs/docs/02-configuration/10-conversions.md new file mode 100644 index 0000000000..152ac5b83c --- /dev/null +++ b/docs/docs/02-configuration/10-conversions.md @@ -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 +{ + ... +} +``` diff --git a/docs/docs/02-configuration/10-analyzer-diagnostics.mdx b/docs/docs/02-configuration/11-analyzer-diagnostics.mdx similarity index 100% rename from docs/docs/02-configuration/10-analyzer-diagnostics.mdx rename to docs/docs/02-configuration/11-analyzer-diagnostics.mdx diff --git a/docs/docs/02-configuration/11-generated-source.mdx b/docs/docs/02-configuration/12-generated-source.mdx similarity index 100% rename from docs/docs/02-configuration/11-generated-source.mdx rename to docs/docs/02-configuration/12-generated-source.mdx diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs index 0a679a6636..1dd8f9fbe5 100644 --- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs +++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs @@ -46,4 +46,18 @@ public sealed class MapperAttribute : Attribute /// With =false, the array and each person is cloned. /// public bool UseDeepCloning { get; set; } + + /// + /// Enabled conversions which Mapperly automatically implements. + /// By default all supported type conversions are enabled. + /// + /// Eg. to disable all automatically implemented conversions:
+ /// EnabledConversions = MappingConversionType.None + ///
+ /// + /// Eg. to disable ToString() method calls:
+ /// EnabledConversions = MappingConversionType.All & ~MappingConversionType.ToStringMethod + ///
+ ///
+ public MappingConversionType EnabledConversions { get; set; } = MappingConversionType.All; } diff --git a/src/Riok.Mapperly.Abstractions/MappingConversionType.cs b/src/Riok.Mapperly.Abstractions/MappingConversionType.cs new file mode 100644 index 0000000000..17b4da9b02 --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/MappingConversionType.cs @@ -0,0 +1,69 @@ +namespace Riok.Mapperly.Abstractions; + +/// +/// A represents a type of conversion +/// how one type can be converted into another. +/// +[Flags] +public enum MappingConversionType +{ + /// + /// None. + /// + None = 0, + + /// + /// Use the constructor of the target type, + /// which accepts the source type as a single parameter. + /// + Constructor = 1 << 0, + + /// + /// An implicit cast from the source type to the target type. + /// + ImplicitCast = 1 << 1, + + /// + /// An explicit cast from the source type to the target type. + /// + ExplicitCast = 1 << 2, + + /// + /// If the source type is a , + /// 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. + /// + ParseMethod = 1 << 3, + + /// + /// If the target type is a , + /// uses the `ToString` method on the source type. + /// + ToStringMethod = 1 << 4, + + /// + /// If the target is an + /// and the source is a , + /// parses the string to match the name of an enum member. + /// + StringToEnum = 1 << 5, + + /// + /// If the source is an + /// and the target is a , + /// uses the name of the enum member to convert it to a string. + /// + EnumToString = 1 << 6, + + /// + /// If the source is an + /// and the target is another , + /// map it according to the . + /// + EnumToEnum = 1 << 7, + + /// + /// Enables all supported conversions. + /// + All = ~None, +} diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt index a44fd2935d..67cebe031c 100644 --- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt +++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt @@ -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 @@ -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 diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/CtorMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/CtorMappingBuilder.cs index 98b7944341..46c57e64f0 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilder/CtorMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/CtorMappingBuilder.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using Riok.Mapperly.Abstractions; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Helpers; @@ -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; diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumMappingBuilder.cs index 43f492753a..b7da424694 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumMappingBuilder.cs @@ -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; } @@ -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(); return config.Strategy switch diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumToStringMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumToStringMappingBuilder.cs index 125ca04ae8..4c4b95847b 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumToStringMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/EnumToStringMappingBuilder.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using Riok.Mapperly.Abstractions; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Helpers; @@ -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; diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/ExplicitCastMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/ExplicitCastMappingBuilder.cs index cfba194c83..d95df3f0d9 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilder/ExplicitCastMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/ExplicitCastMappingBuilder.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis.CSharp; +using Riok.Mapperly.Abstractions; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Helpers; @@ -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; diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/ImplicitCastMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/ImplicitCastMappingBuilder.cs index c80547ac09..e4d3246331 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilder/ImplicitCastMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/ImplicitCastMappingBuilder.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis.CSharp; +using Riok.Mapperly.Abstractions; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Helpers; @@ -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; diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/NewInstanceObjectPropertyMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/NewInstanceObjectPropertyMappingBuilder.cs index 9692769a0b..2f64e5cb1f 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilder/NewInstanceObjectPropertyMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/NewInstanceObjectPropertyMappingBuilder.cs @@ -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()); } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/ParseMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/ParseMappingBuilder.cs index a64cfe38f5..7b18069851 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilder/ParseMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/ParseMappingBuilder.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using Riok.Mapperly.Abstractions; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Helpers; @@ -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; diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/SpecialTypeMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/SpecialTypeMappingBuilder.cs index 89faa38c9c..aad35e5455 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilder/SpecialTypeMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/SpecialTypeMappingBuilder.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using Riok.Mapperly.Abstractions; using Riok.Mapperly.Descriptors.Mappings; namespace Riok.Mapperly.Descriptors.MappingBuilder; @@ -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 diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/StringToEnumMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/StringToEnumMappingBuilder.cs index 9c0769fb43..b1a1147b8f 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilder/StringToEnumMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/StringToEnumMappingBuilder.cs @@ -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; diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilder/ToStringMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilder/ToStringMappingBuilder.cs index ef6f421dcd..7683c1ea7c 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilder/ToStringMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilder/ToStringMappingBuilder.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using Riok.Mapperly.Abstractions; using Riok.Mapperly.Descriptors.Mappings; namespace Riok.Mapperly.Descriptors.MappingBuilder; @@ -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; diff --git a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs index 44bc11ad82..34f7044e17 100644 --- a/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs @@ -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); diff --git a/test/Riok.Mapperly.Tests/Mapping/CastTest.cs b/test/Riok.Mapperly.Tests/Mapping/CastTest.cs index 79f6e35088..e29e411630 100644 --- a/test/Riok.Mapperly.Tests/Mapping/CastTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/CastTest.cs @@ -1,3 +1,6 @@ +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Diagnostics; + namespace Riok.Mapperly.Tests.Mapping; public class CastTest @@ -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)); + } } diff --git a/test/Riok.Mapperly.Tests/Mapping/CtorTest.cs b/test/Riok.Mapperly.Tests/Mapping/CtorTest.cs index 39be0b26e0..abdb9e2f3d 100644 --- a/test/Riok.Mapperly.Tests/Mapping/CtorTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/CtorTest.cs @@ -1,3 +1,6 @@ +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Diagnostics; + namespace Riok.Mapperly.Tests.Mapping; public class CtorTest @@ -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)); + } } diff --git a/test/Riok.Mapperly.Tests/Mapping/EnumTest.cs b/test/Riok.Mapperly.Tests/Mapping/EnumTest.cs index b3ae60b0fd..09541034ee 100644 --- a/test/Riok.Mapperly.Tests/Mapping/EnumTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/EnumTest.cs @@ -1,3 +1,6 @@ +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Diagnostics; + namespace Riok.Mapperly.Tests.Mapping; [UsesVerify] @@ -244,4 +247,82 @@ public void StringToEnumShouldSwitch() _ => (E1)System.Enum.Parse(typeof(E1), source, false), };"); } + + [Fact] + public void EnumToEnumMappingAndExplicitCastMappingDisabledShouldDiagnostic() + { + var source = TestSourceBuilder.Mapping( + "E2", + "E1", + TestSourceBuilderOptions.WithDisabledMappingConversion(MappingConversionType.EnumToEnum, MappingConversionType.ExplicitCast), + "enum E1 {A, B, C}", + "enum E2 {A, B, C}"); + TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(new(DiagnosticDescriptors.CouldNotCreateMapping)); + } + + [Fact] + public void EnumToEnumMappingDisabledShouldUseExplicitCastMapping() + { + var source = TestSourceBuilder.Mapping( + "E2", + "E1", + TestSourceBuilderOptions.WithDisabledMappingConversion(MappingConversionType.EnumToEnum), + "enum E1 {A, B, C}", + "enum E2 {A, B, C}"); + TestHelper.GenerateMapper(source) + .Should() + .HaveSingleMethodBody("return (E1)source;"); + } + + [Fact] + public void StringToEnumMappingAndParseMappingDisabledShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnum(EnumMappingStrategy.ByName)] partial E1 ToE1(string source);", + TestSourceBuilderOptions.WithDisabledMappingConversion(MappingConversionType.StringToEnum, MappingConversionType.ParseMethod), + "enum E1 {A, B, C}"); + TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(new(DiagnosticDescriptors.CouldNotCreateMapping)); + } + + [Fact] + public void StringToEnumMappingDisabledShouldUseParseMethodMapping() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + "[MapEnum(EnumMappingStrategy.ByName)] partial E1 ToE1(string source);", + TestSourceBuilderOptions.WithDisabledMappingConversion(MappingConversionType.StringToEnum), + "enum E1 {A, B, C}"); + TestHelper.GenerateMapper(source) + .Should() + .HaveSingleMethodBody("return (E1)int.Parse(source);"); + } + + [Fact] + public void EnumToStringMappingAndToStringMethodMappingDisabledShouldDiagnostic() + { + var source = TestSourceBuilder.Mapping( + "E1", + "string", + TestSourceBuilderOptions.WithDisabledMappingConversion(MappingConversionType.EnumToString, MappingConversionType.ToStringMethod), + "enum E1 {A, B, C}"); + TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(new(DiagnosticDescriptors.CouldNotCreateMapping)); + } + + [Fact] + public void EnumToStringMappingDisabledShouldUseToStringMapping() + { + var source = TestSourceBuilder.Mapping( + "E1", + "string", + TestSourceBuilderOptions.WithDisabledMappingConversion(MappingConversionType.EnumToString), + "enum E1 {A, B, C}"); + TestHelper.GenerateMapper(source) + .Should() + .HaveSingleMethodBody("return (string)source.ToString();"); + } } diff --git a/test/Riok.Mapperly.Tests/Mapping/ParseTest.cs b/test/Riok.Mapperly.Tests/Mapping/ParseTest.cs index 7d74a45add..56467ceb04 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ParseTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ParseTest.cs @@ -1,3 +1,6 @@ +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Diagnostics; + namespace Riok.Mapperly.Tests.Mapping; public class ParseTest @@ -52,4 +55,17 @@ public void ParseableCustomClass() .Should() .HaveSingleMethodBody("return A.Parse(source);"); } + + [Fact] + public void ParseMappingDisabledShouldDiagnostic() + { + var source = TestSourceBuilder.Mapping( + "string", + "DateTime", + TestSourceBuilderOptions.WithDisabledMappingConversion(MappingConversionType.ParseMethod), + "class A { public A(string x) {} }"); + TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(new(DiagnosticDescriptors.CouldNotCreateMapping)); + } } diff --git a/test/Riok.Mapperly.Tests/Mapping/ToStringTest.cs b/test/Riok.Mapperly.Tests/Mapping/ToStringTest.cs index 0b4b708be0..40f80b2c63 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ToStringTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ToStringTest.cs @@ -1,3 +1,6 @@ +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Diagnostics; + namespace Riok.Mapperly.Tests.Mapping; public class ToStringTest @@ -47,4 +50,17 @@ public void ObjectToString() .Should() .HaveSingleMethodBody("return source.ToString();"); } + + [Fact] + public void ToStringMappingDisabledShouldDiagnostic() + { + var source = TestSourceBuilder.Mapping( + "A", + "string", + TestSourceBuilderOptions.WithDisabledMappingConversion(MappingConversionType.ToStringMethod), + "class A {}"); + TestHelper.GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(new(DiagnosticDescriptors.CouldNotCreateMapping)); + } } diff --git a/test/Riok.Mapperly.Tests/TestHelperOptions.cs b/test/Riok.Mapperly.Tests/TestHelperOptions.cs index a335269975..f047a2591f 100644 --- a/test/Riok.Mapperly.Tests/TestHelperOptions.cs +++ b/test/Riok.Mapperly.Tests/TestHelperOptions.cs @@ -8,21 +8,21 @@ public record TestHelperOptions( LanguageVersion LanguageVersion = LanguageVersion.Default, IReadOnlySet? AllowedDiagnostics = null) { - public static readonly TestHelperOptions Default = new(); + public static readonly TestHelperOptions AllowDiagnostics = new(); - public static readonly TestHelperOptions DisabledNullable = Default with { NullableOption = NullableContextOptions.Disable }; + public static readonly TestHelperOptions DisabledNullable = AllowDiagnostics with { NullableOption = NullableContextOptions.Disable }; - public static readonly TestHelperOptions NoDiagnostics = Default with + public static readonly TestHelperOptions NoDiagnostics = AllowDiagnostics with { AllowedDiagnostics = new HashSet(), }; - public static readonly TestHelperOptions AllowAllDiagnostics = Default with + public static readonly TestHelperOptions AllowAllDiagnostics = AllowDiagnostics with { AllowedDiagnostics = Enum.GetValues().ToHashSet(), }; - public static readonly TestHelperOptions AllowInfoDiagnostics = Default with + public static readonly TestHelperOptions AllowInfoDiagnostics = AllowDiagnostics with { AllowedDiagnostics = new HashSet { diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs index 68885ff43a..9ac04a13de 100644 --- a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs +++ b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs @@ -1,5 +1,4 @@ using System.Runtime.CompilerServices; -using Riok.Mapperly.Abstractions; namespace Riok.Mapperly.Tests; @@ -39,27 +38,31 @@ public partial class Mapper private static string BuildAttribute(TestSourceBuilderOptions options) { - var attrs = new List + var attrs = new[] { Attribute(options.UseDeepCloning), Attribute(options.ThrowOnMappingNullMismatch), - Attribute(options.ThrowOnPropertyMappingNullMismatch) + Attribute(options.ThrowOnPropertyMappingNullMismatch), + Attribute(options.EnabledConversions), + Attribute(options.PropertyNameMappingStrategy), }; - if (options.PropertyNameMappingStrategy != PropertyNameMappingStrategy.CaseSensitive) - { - attrs.Add($"{nameof(MapperAttribute.PropertyNameMappingStrategy)} = {nameof(PropertyNameMappingStrategy)}.{options.PropertyNameMappingStrategy}"); - } - return $"[Mapper({string.Join(", ", attrs)})]"; } + private static string Attribute(T value, [CallerArgumentExpression("value")] string? expression = null) + where T : Enum + => Attribute(Convert.ChangeType(value, Enum.GetUnderlyingType(typeof(T))).ToString() ?? throw new ArgumentNullException(), expression); + private static string Attribute(bool value, [CallerArgumentExpression("value")] string? expression = null) + => Attribute(value ? "true" : "false", expression); + + private static string Attribute(string value, [CallerArgumentExpression("value")] string? expression = null) { if (expression == null) throw new ArgumentNullException(nameof(expression)); - return $"{expression.Split(".").Last()} = {(value ? "true" : "false")}"; + return $"{expression.Split(".").Last()} = {value}"; } public static string MapperWithBodyAndTypes(string body, params string[] types) diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs index ec400daebe..e7158d24f4 100644 --- a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs +++ b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs @@ -7,8 +7,20 @@ public record TestSourceBuilderOptions( bool UseDeepCloning = false, bool ThrowOnMappingNullMismatch = true, bool ThrowOnPropertyMappingNullMismatch = false, - PropertyNameMappingStrategy PropertyNameMappingStrategy = PropertyNameMappingStrategy.CaseSensitive) + PropertyNameMappingStrategy PropertyNameMappingStrategy = PropertyNameMappingStrategy.CaseSensitive, + MappingConversionType EnabledConversions = MappingConversionType.All) { public static readonly TestSourceBuilderOptions Default = new(); public static readonly TestSourceBuilderOptions WithDeepCloning = new(UseDeepCloning: true); + public static TestSourceBuilderOptions WithDisabledMappingConversion(params MappingConversionType[] conversionTypes) + { + var enabled = MappingConversionType.All; + + foreach (var disabledConversionType in conversionTypes) + { + enabled &= ~disabledConversionType; + } + + return Default with { EnabledConversions = enabled }; + } }