diff --git a/docs/docs/configuration/conversions.md b/docs/docs/configuration/conversions.md index d3ef71228b..44dfe121c0 100644 --- a/docs/docs/configuration/conversions.md +++ b/docs/docs/configuration/conversions.md @@ -14,6 +14,7 @@ Mapperly implements several types of automatic conversions (in order of priority | Dictionary | Maps a source dictionary to an enumerable target | Source type is an `IDictionary<,>` or an `IReadOnlyDictionary<,>` | | Enumerable | Maps an enumerable source to an enumerable target | Source type is an `IEnumerable<>` | | Span | Maps a `Span<>`, `ReadOnlySpan<>` to or from `Span<>`, `ReadOnlySpan<>` or enumerable | Source or target type is a `Span<>`, `ReadOnlySpan<>` | +| Tuple | Create a new instance of a `ValueTuple` or tuple expression i.e. `(10, 12)` | Target type is a `ValueTuple<>` or tuple expression | | Memory | Maps a `Memory<>`, `ReadOnlyMemory<>` to or from `Memory<>`, `ReadOnlyMemory<>`, `Span<>`, `ReadOnlySpan<>` or enumerable | Source or target type is a `Memory<>` or `ReadOnlyMemory<>` | | Implicit cast | Implicit cast operator | An implicit cast operator is defined to cast from the source type to the target type | | Parse method | Uses a static `Parse` method on the target type | Source type is a `string` and target has a static method with the following signature: `TTarget Parse(string)`. | diff --git a/src/Riok.Mapperly.Abstractions/MappingConversionType.cs b/src/Riok.Mapperly.Abstractions/MappingConversionType.cs index 32b161ed68..a50319e3c7 100644 --- a/src/Riok.Mapperly.Abstractions/MappingConversionType.cs +++ b/src/Riok.Mapperly.Abstractions/MappingConversionType.cs @@ -107,6 +107,13 @@ public enum MappingConversionType /// Memory = 1 << 14, + /// + /// If the target is a or tuple expression (A: 10, B: 12). + /// Supports positional and named mapping. + /// Only uses in . + /// + Tuple = 1 << 15, + /// /// Enables all supported conversions. /// diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt index 0bf47f01ee..4c37c1c801 100644 --- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt +++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt @@ -78,6 +78,7 @@ Riok.Mapperly.Abstractions.MappingConversionType.ParseMethod = 8 -> Riok.Mapperl Riok.Mapperly.Abstractions.MappingConversionType.Span = 8192 -> Riok.Mapperly.Abstractions.MappingConversionType Riok.Mapperly.Abstractions.MappingConversionType.StringToEnum = 32 -> Riok.Mapperly.Abstractions.MappingConversionType Riok.Mapperly.Abstractions.MappingConversionType.ToStringMethod = 16 -> Riok.Mapperly.Abstractions.MappingConversionType +Riok.Mapperly.Abstractions.MappingConversionType.Tuple = 32768 -> Riok.Mapperly.Abstractions.MappingConversionType Riok.Mapperly.Abstractions.MappingConversionType.Queryable = 1024 -> Riok.Mapperly.Abstractions.MappingConversionType Riok.Mapperly.Abstractions.MapperAttribute.UseReferenceHandling.get -> bool Riok.Mapperly.Abstractions.MapperAttribute.UseReferenceHandling.set -> void diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/INewValueTupleBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/INewValueTupleBuilderContext.cs new file mode 100644 index 0000000000..e92dca4548 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/INewValueTupleBuilderContext.cs @@ -0,0 +1,15 @@ +using Riok.Mapperly.Descriptors.Mappings; +using Riok.Mapperly.Descriptors.Mappings.MemberMappings; + +namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; + +/// +/// A for mappings which create the target object +/// via a tuple expression (eg. (A: source.A, B: MapToB(source.B))). +/// +/// The mapping type. +public interface INewValueTupleBuilderContext : IMembersBuilderContext + where T : IMapping +{ + void AddTupleConstructorParameterMapping(ValueTupleConstructorParameterMapping mapping); +} diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleConstructorBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleConstructorBuilderContext.cs new file mode 100644 index 0000000000..1d93bddf1b --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleConstructorBuilderContext.cs @@ -0,0 +1,22 @@ +using Riok.Mapperly.Descriptors.Mappings; +using Riok.Mapperly.Descriptors.Mappings.MemberMappings; + +namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; + +/// +/// An implementation of . +/// +/// The type of the mapping. +public class NewValueTupleConstructorBuilderContext : MembersMappingBuilderContext, INewValueTupleBuilderContext + where T : INewValueTupleMapping +{ + public NewValueTupleConstructorBuilderContext(MappingBuilderContext builderContext, T mapping) + : base(builderContext, mapping) { } + + public void AddTupleConstructorParameterMapping(ValueTupleConstructorParameterMapping mapping) + { + MemberConfigsByRootTargetName.Remove(mapping.Parameter.Name); + SetSourceMemberMapped(mapping.DelegateMapping.SourcePath); + Mapping.AddConstructorParameterMapping(mapping); + } +} diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleExpressionBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleExpressionBuilderContext.cs new file mode 100644 index 0000000000..75d6e06555 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/NewValueTupleExpressionBuilderContext.cs @@ -0,0 +1,26 @@ +using Riok.Mapperly.Descriptors.Mappings; +using Riok.Mapperly.Descriptors.Mappings.MemberMappings; + +namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; + +/// +/// An implementation of . +/// +/// The type of the mapping. +public class NewValueTupleExpressionBuilderContext : MembersContainerBuilderContext, INewValueTupleBuilderContext + where T : INewValueTupleMapping, IMemberAssignmentTypeMapping +{ + public NewValueTupleExpressionBuilderContext(MappingBuilderContext builderContext, T mapping) + : base(builderContext, mapping) { } + + public void AddTupleConstructorParameterMapping(ValueTupleConstructorParameterMapping mapping) + { + if (MemberConfigsByRootTargetName.TryGetValue(mapping.Parameter.Name, out var value)) + { + value.RemoveAll(x => x.Target.Path.Count == 1); + } + + SetSourceMemberMapped(mapping.DelegateMapping.SourcePath); + Mapping.AddConstructorParameterMapping(mapping); + } +} diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MappingBodyBuilder.cs index 8fa3f00523..30820e3b15 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MappingBodyBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/MappingBodyBuilder.cs @@ -28,6 +28,12 @@ public void BuildMappingBodies() case NewInstanceObjectMemberMapping mapping: NewInstanceObjectMemberMappingBodyBuilder.BuildMappingBody(ctx, mapping); break; + case NewValueTupleExpressionMapping mapping: + NewValueTupleMappingBodyBuilder.BuildMappingBody(ctx, mapping); + break; + case NewValueTupleConstructorMapping mapping: + NewValueTupleMappingBodyBuilder.BuildMappingBody(ctx, mapping); + break; case IMemberAssignmentTypeMapping mapping: ObjectMemberMappingBodyBuilder.BuildMappingBody(ctx, mapping); break; diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs new file mode 100644 index 0000000000..b5a1cb7da0 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs @@ -0,0 +1,225 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; +using Riok.Mapperly.Descriptors.Mappings; +using Riok.Mapperly.Descriptors.Mappings.MemberMappings; +using Riok.Mapperly.Diagnostics; +using Riok.Mapperly.Helpers; +using Riok.Mapperly.Symbols; + +namespace Riok.Mapperly.Descriptors.MappingBodyBuilders; + +public static class NewValueTupleMappingBodyBuilder +{ + public static void BuildMappingBody(MappingBuilderContext ctx, NewValueTupleConstructorMapping expressionMapping) + { + var mappingCtx = new NewValueTupleConstructorBuilderContext(ctx, expressionMapping); + BuildTupleConstructorMapping(mappingCtx); + mappingCtx.AddDiagnostics(); + } + + public static void BuildMappingBody(MappingBuilderContext ctx, NewValueTupleExpressionMapping expressionMapping) + { + var mappingCtx = new NewValueTupleExpressionBuilderContext(ctx, expressionMapping); + BuildTupleConstructorMapping(mappingCtx); + ObjectMemberMappingBodyBuilder.BuildMappingBody(mappingCtx); + mappingCtx.AddDiagnostics(); + } + + private static void BuildTupleConstructorMapping(INewValueTupleBuilderContext ctx) + { + if (ctx.Mapping.TargetType is not INamedTypeSymbol namedTargetType) + { + ctx.BuilderContext.ReportDiagnostic(DiagnosticDescriptors.NoConstructorFound, ctx.BuilderContext.Target); + return; + } + + if (!TryBuildTupleConstructorMapping(ctx, namedTargetType, out var constructorParameterMappings, out var mappedTargetMemberNames)) + { + ctx.BuilderContext.ReportDiagnostic(DiagnosticDescriptors.NoConstructorFound, ctx.BuilderContext.Target); + return; + } + + var removableMappedTargetMemberNames = mappedTargetMemberNames.Where(x => + { + ctx.MemberConfigsByRootTargetName.TryGetValue(x, out var value); + return !value?.Any() ?? true; + }); + + ctx.TargetMembers.RemoveRange(removableMappedTargetMemberNames); + foreach (var constructorParameterMapping in constructorParameterMappings) + { + ctx.AddTupleConstructorParameterMapping(constructorParameterMapping); + } + } + + private static bool TryBuildTupleConstructorMapping( + INewValueTupleBuilderContext ctx, + INamedTypeSymbol namedTargetType, + out HashSet constructorParameterMappings, + out HashSet mappedTargetMemberNames + ) + { + mappedTargetMemberNames = new HashSet(); + constructorParameterMappings = new HashSet(); + + foreach (var targetMember in namedTargetType.TupleElements) + { + if (!ctx.TargetMembers.ContainsKey(targetMember.Name)) + { + return false; + } + if (!TryFindConstructorParameterSourcePath(ctx, targetMember, out var sourcePath)) + { + ctx.BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.SourceMemberNotFound, + targetMember.Name, + ctx.Mapping.TargetType, + ctx.Mapping.SourceType + ); + + return false; + } + + // nullability is handled inside the member expressionMapping + var paramType = targetMember.Type.WithNullableAnnotation(targetMember.NullableAnnotation); + var delegateMapping = + ctx.BuilderContext.FindMapping(sourcePath.MemberType, paramType) + ?? ctx.BuilderContext.FindOrBuildMapping(sourcePath.Member.Type.NonNullable(), paramType.NonNullable()); + + if (delegateMapping == null) + { + ctx.BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.CouldNotMapMember, + ctx.Mapping.SourceType, + sourcePath.FullName, + sourcePath.Member.Type, + ctx.Mapping.TargetType, + targetMember.Name, + targetMember.Type + ); + return false; + } + + if (delegateMapping.Equals(ctx.Mapping)) + { + ctx.BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.ReferenceLoopInCtorMapping, + ctx.Mapping.SourceType, + sourcePath.FullName, + ctx.Mapping.TargetType, + targetMember.Name + ); + return false; + } + + var memberMapping = new NullMemberMapping( + delegateMapping, + sourcePath, + paramType, + ctx.BuilderContext.GetNullFallbackValue(paramType), + !ctx.BuilderContext.IsExpression + ); + + var ctorMapping = new ValueTupleConstructorParameterMapping(targetMember, memberMapping); + constructorParameterMappings.Add(ctorMapping); + mappedTargetMemberNames.Add(targetMember.Name); + } + + return true; + } + + private static bool TryFindConstructorParameterSourcePath( + INewValueTupleBuilderContext ctx, + IFieldSymbol field, + [NotNullWhen(true)] out MemberPath? sourcePath + ) + { + sourcePath = null; + + if (!ctx.MemberConfigsByRootTargetName.TryGetValue(field.Name, out var memberConfigs)) + { + return TryBuildConstructorParameterSourcePath(ctx, field, out sourcePath); + } + + if (memberConfigs.Count > 1) + { + ctx.BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.MultipleConfigurationsForConstructorParameter, + field.Type, + field.Name + ); + } + + var memberConfig = memberConfigs.First(); + if (memberConfig.Target.Path.Count > 1) + { + return TryBuildConstructorParameterSourcePath(ctx, field, out sourcePath); + } + + if (!ctx.BuilderContext.SymbolAccessor.TryFindMemberPath(ctx.Mapping.SourceType, memberConfig.Source.Path, out sourcePath)) + { + ctx.BuilderContext.ReportDiagnostic( + DiagnosticDescriptors.SourceMemberNotFound, + memberConfig.Source, + ctx.Mapping.TargetType, + ctx.Mapping.SourceType + ); + return false; + } + + return true; + } + + private static bool TryBuildConstructorParameterSourcePath( + INewValueTupleBuilderContext ctx, + IFieldSymbol field, + out MemberPath? sourcePath + ) + { + var memberNameComparer = + ctx.BuilderContext.MapperConfiguration.PropertyNameMappingStrategy == PropertyNameMappingStrategy.CaseSensitive + ? StringComparer.Ordinal + : StringComparer.OrdinalIgnoreCase; + + if ( + ctx.BuilderContext.SymbolAccessor.TryFindMemberPath( + ctx.Mapping.SourceType, + MemberPathCandidateBuilder.BuildMemberPathCandidates(field.Name), + ctx.IgnoredSourceMemberNames, + memberNameComparer, + out sourcePath + ) + ) + return true; + + // if standard matching fails, use the underlying field + // if source is a tuple compare the underlying field ie, Item1, Item2 + // if source is not a tuple compare top level members + if (ctx.Mapping.SourceType.IsTupleType) + { + if (ctx.Mapping.SourceType is not INamedTypeSymbol namedType) + return false; + + var mappableMember = namedType.TupleElements + .Where( + x => + x.CorrespondingTupleField != default + && !ctx.IgnoredSourceMemberNames.Contains(x.Name) + && string.Equals(field.CorrespondingTupleField!.Name, x.CorrespondingTupleField!.Name) + ) + .Select(MappableMember.Create) + .WhereNotNull() + .FirstOrDefault(); + + if (mappableMember != default) + { + sourcePath = new MemberPath(new[] { mappableMember }); + return true; + } + } + + return false; + } +} diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/ExplicitCastMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/ExplicitCastMappingBuilder.cs index 756479c60d..63ff14dd5a 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/ExplicitCastMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/ExplicitCastMappingBuilder.cs @@ -16,6 +16,12 @@ public static class ExplicitCastMappingBuilder if (ctx.MapperConfiguration.UseDeepCloning && !ctx.Source.IsImmutable() && !ctx.Target.IsImmutable()) return null; + // ClassifyConversion does not check if tuple field member names are the same + // if tuple check isn't done then (A: int, B: int) -> (B: int, A: int) would be mapped + // return ((int, int))source; instead of return (B: source.A, A: source.B); + if (ctx.Target.IsTupleType) + return null; + if (SymbolEqualityComparer.Default.Equals(ctx.Source, ctx.Compilation.ObjectType)) return null; diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/ImplicitCastMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/ImplicitCastMappingBuilder.cs index 4e9e93a672..2945843908 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/ImplicitCastMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/ImplicitCastMappingBuilder.cs @@ -15,6 +15,12 @@ public static class ImplicitCastMappingBuilder if (ctx.MapperConfiguration.UseDeepCloning && !ctx.Source.IsImmutable() && !ctx.Target.IsImmutable()) return null; + // ClassifyConversion does not check if tuple field member names are the same + // if tuple check isn't done then (A: int, B: int) -> (B: int, A: int) would be mapped + // return source; instead of return (B: source.A, A: source.B); + if (ctx.Target.IsTupleType) + return null; + var conversion = ctx.Compilation.ClassifyConversion(ctx.Source, ctx.Target); return conversion.IsImplicit ? new CastMapping(ctx.Source, ctx.Target) : null; } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/NewInstanceObjectPropertyMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/NewInstanceObjectPropertyMappingBuilder.cs index c17ef13287..97a3952494 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/NewInstanceObjectPropertyMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/NewInstanceObjectPropertyMappingBuilder.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using Riok.Mapperly.Abstractions; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.Mappings.ExistingTarget; using Riok.Mapperly.Helpers; @@ -26,6 +27,21 @@ public static class NewInstanceObjectPropertyMappingBuilder if (ctx.Source.IsEnum() || ctx.Target.IsEnum()) return null; + if (ctx.Target.IsTupleType) + { + if (!ctx.IsConversionEnabled(MappingConversionType.Tuple)) + return null; + + // inline expressions don't support tuple expressions so ValueTuple is used instead + if (ctx.IsExpression) + { + return new NewValueTupleConstructorMapping(ctx.Source, ctx.Target); + } + + var expectedArgumentCount = (ctx.Target as INamedTypeSymbol)!.TupleElements.Length; + return new NewValueTupleExpressionMapping(ctx.Source, ctx.Target, expectedArgumentCount); + } + // inline expressions don't support method property mappings // and can only map to properties via object initializers. return ctx.IsExpression diff --git a/src/Riok.Mapperly/Descriptors/Mappings/INewValueTupleMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/INewValueTupleMapping.cs new file mode 100644 index 0000000000..95e25e465c --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/Mappings/INewValueTupleMapping.cs @@ -0,0 +1,11 @@ +using Riok.Mapperly.Descriptors.Mappings.MemberMappings; + +namespace Riok.Mapperly.Descriptors.Mappings; + +/// +/// A tuple mapping creating the target instance via a tuple expression (eg. (A: 10, B: 20)). +/// +public interface INewValueTupleMapping : IMapping +{ + void AddConstructorParameterMapping(ValueTupleConstructorParameterMapping mapping); +} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberAssignmentMappingContainer.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberAssignmentMappingContainer.cs index ce4274699a..e4f8cd84d6 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberAssignmentMappingContainer.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/IMemberAssignmentMappingContainer.cs @@ -7,6 +7,8 @@ namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; /// public interface IMemberAssignmentMappingContainer { + bool HasMemberMappings(); + bool HasMemberMapping(IMemberAssignmentMapping mapping); void AddMemberMapping(IMemberAssignmentMapping mapping); diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberAssignmentMappingContainer.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberAssignmentMappingContainer.cs index 87744146f1..f3a0b44a2f 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberAssignmentMappingContainer.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberAssignmentMappingContainer.cs @@ -42,6 +42,8 @@ public void AddMemberMapping(IMemberAssignmentMapping mapping) } } + public bool HasMemberMappings() => _delegateMappings.Any() || _childContainers.Any(x => x.HasMemberMappings()); + public bool HasMemberMapping(IMemberAssignmentMapping mapping) => _delegateMappings.Contains(mapping) || _parent?.HasMemberMapping(mapping) == true; } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ValueTupleConstructorParameterMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ValueTupleConstructorParameterMapping.cs new file mode 100644 index 0000000000..a0321b8aa7 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/ValueTupleConstructorParameterMapping.cs @@ -0,0 +1,67 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Riok.Mapperly.Descriptors.Mappings.MemberMappings; + +public class ValueTupleConstructorParameterMapping +{ + public ValueTupleConstructorParameterMapping(IFieldSymbol parameter, NullMemberMapping delegateMapping) + { + DelegateMapping = delegateMapping; + Parameter = parameter; + } + + public IFieldSymbol Parameter { get; } + + public NullMemberMapping DelegateMapping { get; } + + public ArgumentSyntax BuildArgument(TypeMappingBuildContext ctx, bool insideExpression) + { + var argumentExpression = DelegateMapping.Build(ctx); + var argument = Argument(argumentExpression); + + // tuples inside expression cannot use the expression form (A: .., ..) instead new ValueTuple<>(..) must be used instead + // custom field names cannot be used so we return a plain Argument + if (insideExpression) + return argument; + + // add field name if available + return SymbolEqualityComparer.Default.Equals(Parameter.CorrespondingTupleField, Parameter) + ? argument + : argument.WithNameColon(NameColon(IdentifierName(Parameter.Name))); + } + + protected bool Equals(ValueTupleConstructorParameterMapping other) => + Parameter.Equals(other.Parameter, SymbolEqualityComparer.Default) && DelegateMapping.Equals(other.DelegateMapping); + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + + if (ReferenceEquals(this, obj)) + return true; + + if (obj.GetType() != GetType()) + return false; + + return Equals((ValueTupleConstructorParameterMapping)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = SymbolEqualityComparer.Default.GetHashCode(Parameter); + hashCode = (hashCode * 397) ^ DelegateMapping.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(ValueTupleConstructorParameterMapping? left, ValueTupleConstructorParameterMapping? right) => + Equals(left, right); + + public static bool operator !=(ValueTupleConstructorParameterMapping? left, ValueTupleConstructorParameterMapping? right) => + !Equals(left, right); +} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs index a5852717cc..0e771ed980 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs @@ -63,7 +63,7 @@ ITypeSymbol targetType public override ExpressionSyntax Build(TypeMappingBuildContext ctx) => Invocation(MethodName, SourceParameter.WithArgument(ctx.Source), ReferenceHandlerParameter?.WithArgument(ctx.ReferenceHandler)); - public virtual MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx) + public virtual MethodDeclarationSyntax? BuildMethod(SourceEmitterContext ctx) { var returnType = FullyQualifiedIdentifier(_returnType); diff --git a/src/Riok.Mapperly/Descriptors/Mappings/NewValueTupleConstructorMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/NewValueTupleConstructorMapping.cs new file mode 100644 index 0000000000..4c71bd8cf8 --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/Mappings/NewValueTupleConstructorMapping.cs @@ -0,0 +1,33 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors.Mappings.MemberMappings; +using static Riok.Mapperly.Emit.SyntaxFactoryHelper; + +namespace Riok.Mapperly.Descriptors.Mappings; + +/// +/// An object mapping creating the target instance via new ValueTuple<int, string>(source.A, source.B), +/// mapping properties via ctor, but not by assigning. +/// +/// +public class NewValueTupleConstructorMapping : TypeMapping, INewValueTupleMapping +{ + private const string ValueTupleName = "global::System.ValueTuple"; + private readonly HashSet _constructorPropertyMappings = new(); + + public NewValueTupleConstructorMapping(ITypeSymbol sourceType, ITypeSymbol targetType) + : base(sourceType, targetType) { } + + public void AddConstructorParameterMapping(ValueTupleConstructorParameterMapping mapping) => _constructorPropertyMappings.Add(mapping); + + public override ExpressionSyntax Build(TypeMappingBuildContext ctx) + { + // new ValueTuple(ctorArgs) + var ctorArgs = _constructorPropertyMappings.Select(x => x.BuildArgument(ctx, insideExpression: true)).ToArray(); + var genericName = SyntaxFactory.GenericName(ValueTupleName); + var typeArguments = TypeArgumentList((TargetType as INamedTypeSymbol)!.TypeArguments.Select(NonNullableIdentifier)); + var typedValue = genericName.WithTypeArgumentList(typeArguments); + return SyntaxFactory.ObjectCreationExpression(typedValue).WithArgumentList(ArgumentList(ctorArgs)); + } +} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/NewValueTupleExpressionMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/NewValueTupleExpressionMapping.cs new file mode 100644 index 0000000000..0847a1b4dc --- /dev/null +++ b/src/Riok.Mapperly/Descriptors/Mappings/NewValueTupleExpressionMapping.cs @@ -0,0 +1,81 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Descriptors.Mappings.MemberMappings; +using Riok.Mapperly.Emit; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Riok.Mapperly.Emit.SyntaxFactoryHelper; + +namespace Riok.Mapperly.Descriptors.Mappings; + +/// +/// An object mapping creating the target instance via a tuple expression (eg. (A: 10, B: 20)), +/// mapping properties via ctor, not by assigning. +/// +/// +public class NewValueTupleExpressionMapping : ObjectMemberMethodMapping, INewValueTupleMapping +{ + private readonly int _argumentCount; + private const string NoMappingComment = "// Could not generate mapping"; + private const string TargetVariableName = "target"; + private readonly HashSet _constructorPropertyMappings = new(); + + public NewValueTupleExpressionMapping(ITypeSymbol sourceType, ITypeSymbol targetType, int argumentCount) + : base(sourceType, targetType) + { + _argumentCount = argumentCount; + } + + public void AddConstructorParameterMapping(ValueTupleConstructorParameterMapping mapping) => _constructorPropertyMappings.Add(mapping); + + public override MethodDeclarationSyntax? BuildMethod(SourceEmitterContext ctx) + { + return HasMemberMappings() ? base.BuildMethod(ctx) : null; + } + + public override ExpressionSyntax Build(TypeMappingBuildContext ctx) + { + if (_constructorPropertyMappings.Count != _argumentCount) + { + return ThrowNotImplementedException().WithLeadingTrivia(TriviaList(Comment(NoMappingComment))); + } + + if (HasMemberMappings()) + { + return base.Build(ctx); + } + + var ctorArgs = _constructorPropertyMappings.Select(x => x.BuildArgument(ctx, insideExpression: false)).ToArray(); + return TupleExpression(SeparatedList(ctorArgs)); + } + + public override IEnumerable BuildBody(TypeMappingBuildContext ctx) + { + if (_constructorPropertyMappings.Count != _argumentCount) + { + yield return ExpressionStatement(ThrowNotImplementedException()).WithLeadingTrivia(TriviaList(Comment(NoMappingComment))); + yield break; + } + + // (Name:.. ,..); + var ctorArgs = _constructorPropertyMappings.Select(x => x.BuildArgument(ctx, insideExpression: false)).ToArray(); + var tupleCreationExpression = TupleExpression(SeparatedList(ctorArgs)); + + if (!HasMemberMappings()) + { + yield return ReturnStatement(tupleCreationExpression); + yield break; + } + + var targetVariableName = ctx.NameBuilder.New(TargetVariableName); + yield return DeclareLocalVariable(targetVariableName, tupleCreationExpression); + + // map properties + foreach (var expression in BuildBody(ctx, IdentifierName(targetVariableName))) + { + yield return expression; + } + + // return target; + yield return ReturnVariable(targetVariableName); + } +} diff --git a/src/Riok.Mapperly/Descriptors/Mappings/ObjectMemberMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/ObjectMemberMethodMapping.cs index 0d5c4510e2..839ba53d0f 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/ObjectMemberMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/ObjectMemberMethodMapping.cs @@ -19,6 +19,8 @@ protected ObjectMemberMethodMapping(ITypeSymbol sourceType, ITypeSymbol targetTy _mapping = new ObjectMemberExistingTargetMapping(sourceType, targetType); } + public bool HasMemberMappings() => _mapping.HasMemberMappings(); + public bool HasMemberMapping(IMemberAssignmentMapping mapping) => _mapping.HasMemberMapping(mapping); public void AddMemberMapping(IMemberAssignmentMapping mapping) => _mapping.AddMemberMapping(mapping); diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs index dc5d36098e..d7833cb0a8 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceGenericTypeMapping.cs @@ -30,8 +30,8 @@ ITypeSymbol objectType public GenericMappingTypeParameters TypeParameters { get; } - public override MethodDeclarationSyntax BuildMethod(SourceEmitterContext ctx) => - base.BuildMethod(ctx).WithTypeParameterList(TypeParameterList(TypeParameters.SourceType, TypeParameters.TargetType)); + public override MethodDeclarationSyntax? BuildMethod(SourceEmitterContext ctx) => + base.BuildMethod(ctx)?.WithTypeParameterList(TypeParameterList(TypeParameters.SourceType, TypeParameters.TargetType)); protected override ExpressionSyntax BuildTargetType() { diff --git a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs index 678d39f16b..53cf87e2e5 100644 --- a/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs +++ b/src/Riok.Mapperly/Descriptors/SymbolAccessor.cs @@ -183,6 +183,11 @@ private IEnumerable GetAllMembersCore(ITypeSymbol symbol) private IEnumerable GetAllAccessibleMappableMembersCore(ITypeSymbol symbol) { + if (symbol.IsTupleType && symbol is INamedTypeSymbol namedType) + { + return namedType.TupleElements.Where(x => x is { Kind: SymbolKind.Field }).Select(MappableMember.Create).WhereNotNull(); + } + return GetAllMembers(symbol) .Where(x => x is { IsStatic: false, Kind: SymbolKind.Property or SymbolKind.Field } && x.IsAccessible()) .DistinctBy(x => x.Name) diff --git a/src/Riok.Mapperly/Emit/SourceEmitter.cs b/src/Riok.Mapperly/Emit/SourceEmitter.cs index cba4250715..ee3bafb6ee 100644 --- a/src/Riok.Mapperly/Emit/SourceEmitter.cs +++ b/src/Riok.Mapperly/Emit/SourceEmitter.cs @@ -1,6 +1,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Descriptors; +using Riok.Mapperly.Helpers; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Riok.Mapperly.Emit.SyntaxFactoryHelper; @@ -23,7 +24,7 @@ public static CompilationUnitSyntax Build(MapperDescriptor descriptor) private static IEnumerable BuildMembers(MapperDescriptor descriptor, SourceEmitterContext sourceEmitterContext) { - return descriptor.MethodTypeMappings.Select(mapping => mapping.BuildMethod(sourceEmitterContext)); + return descriptor.MethodTypeMappings.Select(mapping => mapping.BuildMethod(sourceEmitterContext)).WhereNotNull(); } private static MemberDeclarationSyntax WrapInClassesAsNeeded(INamedTypeSymbol symbol, MemberDeclarationSyntax syntax) diff --git a/src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs b/src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs index 14c74f4f61..834876b8dc 100644 --- a/src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs +++ b/src/Riok.Mapperly/Emit/SyntaxFactoryHelper.cs @@ -396,6 +396,9 @@ public static SyntaxTrivia Nullable(bool enabled) public static TypeArgumentListSyntax TypeArgumentList(params TypeSyntax[] argSyntaxes) => SyntaxFactory.TypeArgumentList(CommaSeparatedList(argSyntaxes)); + public static TypeArgumentListSyntax TypeArgumentList(IEnumerable argSyntaxes) => + SyntaxFactory.TypeArgumentList(CommaSeparatedList(argSyntaxes)); + public static ArgumentListSyntax ArgumentList(params ArgumentSyntax[] args) => SyntaxFactory.ArgumentList(CommaSeparatedList(args)); public static SeparatedSyntaxList CommaSeparatedList(IEnumerable nodes, bool insertTrailingComma = false) diff --git a/test/Riok.Mapperly.IntegrationTests/BaseMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/BaseMapperTest.cs index 0eaa946613..a769805480 100644 --- a/test/Riok.Mapperly.IntegrationTests/BaseMapperTest.cs +++ b/test/Riok.Mapperly.IntegrationTests/BaseMapperTest.cs @@ -72,6 +72,7 @@ public static TestObject NewTestObj() NullableFlattening = new() { IdValue = 100 }, UnflatteningIdValue = 20, NullableUnflatteningIdValue = 200, + TupleValue = ("10", "20"), RecursiveObject = new(5) { EnumValue = TestEnum.Value10, diff --git a/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs b/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs index 574d0fcb92..37c189a0fa 100644 --- a/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs +++ b/test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs @@ -47,6 +47,8 @@ public TestObjectDto(int ctorValue, int unknownValue = 10, int ctorValue2 = 100) public string StringNullableTargetNotNullable { get; set; } = string.Empty; + public (int A, int)? TupleValue { get; set; } + public TestObjectDto? RecursiveObject { get; set; } public TestObject? SourceTargetSameObjectType { get; set; } diff --git a/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs b/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs index c01df23838..9d69eda85f 100644 --- a/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs +++ b/test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs @@ -44,6 +44,8 @@ public TestObject(int ctorValue, int unknownValue = 10, int ctorValue2 = 100) public string? StringNullableTargetNotNullable { get; set; } + public (string A, string)? TupleValue { get; set; } + public TestObject? RecursiveObject { get; set; } public TestObject? SourceTargetSameObjectType { get; set; } diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/DeepCloningMapperTest.RunMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/DeepCloningMapperTest.RunMappingShouldWork.verified.txt index a4c1c704c6..699a80182d 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/DeepCloningMapperTest.RunMappingShouldWork.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/DeepCloningMapperTest.RunMappingShouldWork.verified.txt @@ -19,6 +19,10 @@ }, NestedNullableTargetNotNullable: {}, StringNullableTargetNotNullable: fooBar3, + TupleValue: { + Item1: 10, + Item2: 20 + }, RecursiveObject: { CtorValue: 5, CtorValue2: 100, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs index 1dd22355eb..89d6ee9a40 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs @@ -33,6 +33,11 @@ public static partial class DeepCloningMapper target.NestedNullableTargetNotNullable = MapToTestObjectNested(src.NestedNullableTargetNotNullable); } + if (src.TupleValue != null) + { + target.TupleValue = (A: src.TupleValue.Value.A, src.TupleValue.Value.Item2); + } + if (src.RecursiveObject != null) { target.RecursiveObject = Copy(src.RecursiveObject); diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/MapperTest.RunMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/MapperTest.RunMappingShouldWork.verified.txt index 6f6e920259..a24ad94486 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/MapperTest.RunMappingShouldWork.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/MapperTest.RunMappingShouldWork.verified.txt @@ -20,6 +20,10 @@ }, NestedNullableTargetNotNullable: {}, StringNullableTargetNotNullable: fooBar3, + TupleValue: { + Item1: 10, + Item2: 20 + }, RecursiveObject: { CtorValue: 5, CtorValue2: 100, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/MapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/MapperTest.SnapshotGeneratedSource.verified.cs index 69c860dde3..7ea17191c5 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/MapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/MapperTest.SnapshotGeneratedSource.verified.cs @@ -78,6 +78,11 @@ public partial class TestMapper target.StringNullableTargetNotNullable = testObject.StringNullableTargetNotNullable; } + if (testObject.TupleValue != null) + { + target.TupleValue = (A: ParseableInt(testObject.TupleValue.Value.A), ParseableInt(testObject.TupleValue.Value.Item2)); + } + if (testObject.RecursiveObject != null) { target.RecursiveObject = MapToDto(testObject.RecursiveObject); @@ -157,6 +162,11 @@ public partial class TestMapper target.NestedNullable = MapToTestObjectNested(dto.NestedNullable); } + if (dto.TupleValue != null) + { + target.TupleValue = (A: dto.TupleValue.Value.A.ToString(), dto.TupleValue.Value.Item2.ToString()); + } + if (dto.RecursiveObject != null) { target.RecursiveObject = MapFromDto(dto.RecursiveObject); @@ -243,6 +253,11 @@ public partial class TestMapper target.StringNullableTargetNotNullable = source.StringNullableTargetNotNullable; } + if (source.TupleValue != null) + { + target.TupleValue = (A: ParseableInt(source.TupleValue.Value.A), ParseableInt(source.TupleValue.Value.Item2)); + } + if (source.RecursiveObject != null) { target.RecursiveObject = MapToDto(source.RecursiveObject); diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt index d67b3bf99f..a5818925d2 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt @@ -15,6 +15,10 @@ }, NestedNullableTargetNotNullable: {}, StringNullableTargetNotNullable: fooBar3, + TupleValue: { + Item1: 10, + Item2: 20 + }, RecursiveObject: { CtorValue: 5, CtorValue2: 100, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.RunMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.RunMappingShouldWork.verified.txt index 9196056a62..13dfd1453a 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.RunMappingShouldWork.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.RunMappingShouldWork.verified.txt @@ -20,6 +20,10 @@ }, NestedNullableTargetNotNullable: {}, StringNullableTargetNotNullable: fooBar3, + TupleValue: { + Item1: 10, + Item2: 20 + }, RecursiveObject: { CtorValue: 5, CtorValue2: 100, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.SnapshotGeneratedSource.verified.cs index 6321bdf662..9af3f7e867 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/NET_48/StaticMapperTest.SnapshotGeneratedSource.verified.cs @@ -77,6 +77,11 @@ public static partial class StaticTestMapper target.StringNullableTargetNotNullable = src.StringNullableTargetNotNullable; } + if (src.TupleValue != null) + { + target.TupleValue = (A: ParseableInt(src.TupleValue.Value.A), ParseableInt(src.TupleValue.Value.Item2)); + } + if (src.RecursiveObject != null) { target.RecursiveObject = MapToDtoExt(src.RecursiveObject); @@ -171,6 +176,11 @@ public static partial class StaticTestMapper target.StringNullableTargetNotNullable = testObject.StringNullableTargetNotNullable; } + if (testObject.TupleValue != null) + { + target.TupleValue = (A: ParseableInt(testObject.TupleValue.Value.A), ParseableInt(testObject.TupleValue.Value.Item2)); + } + if (testObject.RecursiveObject != null) { target.RecursiveObject = MapToDtoExt(testObject.RecursiveObject); @@ -250,6 +260,11 @@ public static partial class StaticTestMapper target.NestedNullable = MapToTestObjectNested(dto.NestedNullable); } + if (dto.TupleValue != null) + { + target.TupleValue = (A: dto.TupleValue.Value.A.ToString(), dto.TupleValue.Value.Item2.ToString()); + } + if (dto.RecursiveObject != null) { target.RecursiveObject = MapFromDto(dto.RecursiveObject); @@ -336,6 +351,11 @@ public static partial class StaticTestMapper target.StringNullableTargetNotNullable = source.StringNullableTargetNotNullable; } + if (source.TupleValue != null) + { + target.TupleValue = (A: ParseableInt(source.TupleValue.Value.A), ParseableInt(source.TupleValue.Value.Item2)); + } + if (source.RecursiveObject != null) { target.RecursiveObject = MapToDtoExt(source.RecursiveObject); diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/DeepCloningMapperTest.RunMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/DeepCloningMapperTest.RunMappingShouldWork.verified.txt index 90335d9b16..6fa816fe88 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/DeepCloningMapperTest.RunMappingShouldWork.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/DeepCloningMapperTest.RunMappingShouldWork.verified.txt @@ -19,6 +19,10 @@ }, NestedNullableTargetNotNullable: {}, StringNullableTargetNotNullable: fooBar3, + TupleValue: { + Item1: 10, + Item2: 20 + }, RecursiveObject: { CtorValue: 5, CtorValue2: 100, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs index 4bc910514f..eb9b754ab8 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs @@ -30,6 +30,11 @@ public static partial class DeepCloningMapper target.NestedNullableTargetNotNullable = MapToTestObjectNested(src.NestedNullableTargetNotNullable); } + if (src.TupleValue != null) + { + target.TupleValue = (A: src.TupleValue.Value.A, src.TupleValue.Value.Item2); + } + if (src.RecursiveObject != null) { target.RecursiveObject = Copy(src.RecursiveObject); diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/MapperTest.RunMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/MapperTest.RunMappingShouldWork.verified.txt index 4d03e0e639..9c57b7ac72 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/MapperTest.RunMappingShouldWork.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/MapperTest.RunMappingShouldWork.verified.txt @@ -20,6 +20,10 @@ }, NestedNullableTargetNotNullable: {}, StringNullableTargetNotNullable: fooBar3, + TupleValue: { + Item1: 10, + Item2: 20 + }, RecursiveObject: { CtorValue: 5, CtorValue2: 100, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/MapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/MapperTest.SnapshotGeneratedSource.verified.cs index 63583c5592..9d9c49970a 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/MapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/MapperTest.SnapshotGeneratedSource.verified.cs @@ -75,6 +75,11 @@ public partial class TestMapper target.StringNullableTargetNotNullable = testObject.StringNullableTargetNotNullable; } + if (testObject.TupleValue != null) + { + target.TupleValue = (A: ParseableInt(testObject.TupleValue.Value.A), ParseableInt(testObject.TupleValue.Value.Item2)); + } + if (testObject.RecursiveObject != null) { target.RecursiveObject = MapToDto(testObject.RecursiveObject); @@ -153,6 +158,11 @@ public partial class TestMapper target.NestedNullable = MapToTestObjectNested(dto.NestedNullable); } + if (dto.TupleValue != null) + { + target.TupleValue = (A: dto.TupleValue.Value.A.ToString(), dto.TupleValue.Value.Item2.ToString()); + } + if (dto.RecursiveObject != null) { target.RecursiveObject = MapFromDto(dto.RecursiveObject); @@ -241,6 +251,11 @@ public partial class TestMapper target.StringNullableTargetNotNullable = source.StringNullableTargetNotNullable; } + if (source.TupleValue != null) + { + target.TupleValue = (A: ParseableInt(source.TupleValue.Value.A), ParseableInt(source.TupleValue.Value.Item2)); + } + if (source.RecursiveObject != null) { target.RecursiveObject = MapToDto(source.RecursiveObject); diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt index d0a7271d51..3437d7e928 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt @@ -15,6 +15,10 @@ }, NestedNullableTargetNotNullable: {}, StringNullableTargetNotNullable: fooBar3, + TupleValue: { + Item1: 10, + Item2: 20 + }, RecursiveObject: { CtorValue: 5, CtorValue2: 100, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.RunMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.RunMappingShouldWork.verified.txt index 5ade304416..06c3a7cec3 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.RunMappingShouldWork.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.RunMappingShouldWork.verified.txt @@ -20,6 +20,10 @@ }, NestedNullableTargetNotNullable: {}, StringNullableTargetNotNullable: fooBar3, + TupleValue: { + Item1: 10, + Item2: 20 + }, RecursiveObject: { CtorValue: 5, CtorValue2: 100, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.SnapshotGeneratedSource.verified.cs index ab6f019a34..e77ec8de5d 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_4_OR_LOWER/StaticMapperTest.SnapshotGeneratedSource.verified.cs @@ -74,6 +74,11 @@ public static partial class StaticTestMapper target.StringNullableTargetNotNullable = src.StringNullableTargetNotNullable; } + if (src.TupleValue != null) + { + target.TupleValue = (A: ParseableInt(src.TupleValue.Value.A), ParseableInt(src.TupleValue.Value.Item2)); + } + if (src.RecursiveObject != null) { target.RecursiveObject = MapToDtoExt(src.RecursiveObject); @@ -167,6 +172,11 @@ public static partial class StaticTestMapper target.StringNullableTargetNotNullable = testObject.StringNullableTargetNotNullable; } + if (testObject.TupleValue != null) + { + target.TupleValue = (A: ParseableInt(testObject.TupleValue.Value.A), ParseableInt(testObject.TupleValue.Value.Item2)); + } + if (testObject.RecursiveObject != null) { target.RecursiveObject = MapToDtoExt(testObject.RecursiveObject); @@ -245,6 +255,11 @@ public static partial class StaticTestMapper target.NestedNullable = MapToTestObjectNested(dto.NestedNullable); } + if (dto.TupleValue != null) + { + target.TupleValue = (A: dto.TupleValue.Value.A.ToString(), dto.TupleValue.Value.Item2.ToString()); + } + if (dto.RecursiveObject != null) { target.RecursiveObject = MapFromDto(dto.RecursiveObject); @@ -333,6 +348,11 @@ public static partial class StaticTestMapper target.StringNullableTargetNotNullable = source.StringNullableTargetNotNullable; } + if (source.TupleValue != null) + { + target.TupleValue = (A: ParseableInt(source.TupleValue.Value.A), ParseableInt(source.TupleValue.Value.Item2)); + } + if (source.RecursiveObject != null) { target.RecursiveObject = MapToDtoExt(source.RecursiveObject); diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/DeepCloningMapperTest.RunMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/DeepCloningMapperTest.RunMappingShouldWork.verified.txt index 90335d9b16..6fa816fe88 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/DeepCloningMapperTest.RunMappingShouldWork.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/DeepCloningMapperTest.RunMappingShouldWork.verified.txt @@ -19,6 +19,10 @@ }, NestedNullableTargetNotNullable: {}, StringNullableTargetNotNullable: fooBar3, + TupleValue: { + Item1: 10, + Item2: 20 + }, RecursiveObject: { CtorValue: 5, CtorValue2: 100, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs index df5b5c8a04..a6c9b1f7c0 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/DeepCloningMapperTest.SnapshotGeneratedSource.verified.cs @@ -33,6 +33,11 @@ public static partial class DeepCloningMapper target.NestedNullableTargetNotNullable = MapToTestObjectNested(src.NestedNullableTargetNotNullable); } + if (src.TupleValue != null) + { + target.TupleValue = (A: src.TupleValue.Value.A, src.TupleValue.Value.Item2); + } + if (src.RecursiveObject != null) { target.RecursiveObject = Copy(src.RecursiveObject); diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/MapperTest.RunMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/MapperTest.RunMappingShouldWork.verified.txt index 4d03e0e639..9c57b7ac72 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/MapperTest.RunMappingShouldWork.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/MapperTest.RunMappingShouldWork.verified.txt @@ -20,6 +20,10 @@ }, NestedNullableTargetNotNullable: {}, StringNullableTargetNotNullable: fooBar3, + TupleValue: { + Item1: 10, + Item2: 20 + }, RecursiveObject: { CtorValue: 5, CtorValue2: 100, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/MapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/MapperTest.SnapshotGeneratedSource.verified.cs index 6045c2198d..5e2b66e277 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/MapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/MapperTest.SnapshotGeneratedSource.verified.cs @@ -78,6 +78,11 @@ public partial class TestMapper target.StringNullableTargetNotNullable = testObject.StringNullableTargetNotNullable; } + if (testObject.TupleValue != null) + { + target.TupleValue = (A: ParseableInt(testObject.TupleValue.Value.A), ParseableInt(testObject.TupleValue.Value.Item2)); + } + if (testObject.RecursiveObject != null) { target.RecursiveObject = MapToDto(testObject.RecursiveObject); @@ -159,6 +164,11 @@ public partial class TestMapper target.NestedNullable = MapToTestObjectNested(dto.NestedNullable); } + if (dto.TupleValue != null) + { + target.TupleValue = (A: dto.TupleValue.Value.A.ToString(), dto.TupleValue.Value.Item2.ToString()); + } + if (dto.RecursiveObject != null) { target.RecursiveObject = MapFromDto(dto.RecursiveObject); @@ -247,6 +257,11 @@ public partial class TestMapper target.StringNullableTargetNotNullable = source.StringNullableTargetNotNullable; } + if (source.TupleValue != null) + { + target.TupleValue = (A: ParseableInt(source.TupleValue.Value.A), ParseableInt(source.TupleValue.Value.Item2)); + } + if (source.RecursiveObject != null) { target.RecursiveObject = MapToDto(source.RecursiveObject); diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt index d0a7271d51..3437d7e928 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.RunExtensionMappingShouldWork.verified.txt @@ -15,6 +15,10 @@ }, NestedNullableTargetNotNullable: {}, StringNullableTargetNotNullable: fooBar3, + TupleValue: { + Item1: 10, + Item2: 20 + }, RecursiveObject: { CtorValue: 5, CtorValue2: 100, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.RunMappingShouldWork.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.RunMappingShouldWork.verified.txt index 5ade304416..06c3a7cec3 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.RunMappingShouldWork.verified.txt +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.RunMappingShouldWork.verified.txt @@ -20,6 +20,10 @@ }, NestedNullableTargetNotNullable: {}, StringNullableTargetNotNullable: fooBar3, + TupleValue: { + Item1: 10, + Item2: 20 + }, RecursiveObject: { CtorValue: 5, CtorValue2: 100, diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.SnapshotGeneratedSource.verified.cs index 699997e00c..c79d3b0e7c 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/Roslyn_4_5/StaticMapperTest.SnapshotGeneratedSource.verified.cs @@ -77,6 +77,11 @@ public static partial class StaticTestMapper target.StringNullableTargetNotNullable = src.StringNullableTargetNotNullable; } + if (src.TupleValue != null) + { + target.TupleValue = (A: ParseableInt(src.TupleValue.Value.A), ParseableInt(src.TupleValue.Value.Item2)); + } + if (src.RecursiveObject != null) { target.RecursiveObject = MapToDtoExt(src.RecursiveObject); @@ -173,6 +178,11 @@ public static partial class StaticTestMapper target.StringNullableTargetNotNullable = testObject.StringNullableTargetNotNullable; } + if (testObject.TupleValue != null) + { + target.TupleValue = (A: ParseableInt(testObject.TupleValue.Value.A), ParseableInt(testObject.TupleValue.Value.Item2)); + } + if (testObject.RecursiveObject != null) { target.RecursiveObject = MapToDtoExt(testObject.RecursiveObject); @@ -254,6 +264,11 @@ public static partial class StaticTestMapper target.NestedNullable = MapToTestObjectNested(dto.NestedNullable); } + if (dto.TupleValue != null) + { + target.TupleValue = (A: dto.TupleValue.Value.A.ToString(), dto.TupleValue.Value.Item2.ToString()); + } + if (dto.RecursiveObject != null) { target.RecursiveObject = MapFromDto(dto.RecursiveObject); @@ -342,6 +357,11 @@ public static partial class StaticTestMapper target.StringNullableTargetNotNullable = source.StringNullableTargetNotNullable; } + if (source.TupleValue != null) + { + target.TupleValue = (A: ParseableInt(source.TupleValue.Value.A), ParseableInt(source.TupleValue.Value.Item2)); + } + if (source.RecursiveObject != null) { target.RecursiveObject = MapToDtoExt(source.RecursiveObject); diff --git a/test/Riok.Mapperly.Tests/Mapping/ValueTupleTest.cs b/test/Riok.Mapperly.Tests/Mapping/ValueTupleTest.cs new file mode 100644 index 0000000000..5c354a412d --- /dev/null +++ b/test/Riok.Mapperly.Tests/Mapping/ValueTupleTest.cs @@ -0,0 +1,512 @@ +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.Diagnostics; + +namespace Riok.Mapperly.Tests.Mapping; + +[UsesVerify] +public class ValueTupleTest +{ + [Fact] + public void TupleToTuple() + { + var source = TestSourceBuilder.Mapping("(int, string)", "(int, string)"); + + TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return source;"); + } + + [Fact] + public void TupleToTupleWithDeepCloning() + { + var source = TestSourceBuilder.Mapping("(int, string)", "(int, string)", TestSourceBuilderOptions.WithDeepCloning); + + TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (source.Item1, source.Item2);"); + } + + [Fact] + public void TupleToDifferentTypeTuple() + { + var source = TestSourceBuilder.Mapping("(int, string)", "(long, int)"); + + TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return ((long)source.Item1, int.Parse(source.Item2));"); + } + + [Fact] + public void NamedTupleToTuple() + { + var source = TestSourceBuilder.Mapping("(int A, string B)", "(int, string)"); + + TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (source.A, source.B);"); + } + + [Fact] + public void TupleToNamedTuple() + { + var source = TestSourceBuilder.Mapping("(int, string)", "(int A, string B)"); + + TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (A: source.Item1, B: source.Item2);"); + } + + [Fact] + public void NamedTupleToNamedTuple() + { + var source = TestSourceBuilder.Mapping("(int A, string B)", "(int A, string B)"); + + TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return source;"); + } + + [Fact] + public void NamedTupleToNamedTupleWithDeepCloning() + { + var source = TestSourceBuilder.Mapping("(int A, string B)", "(int A, string B)", TestSourceBuilderOptions.WithDeepCloning); + + TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (A: source.A, B: source.B);"); + } + + [Fact] + public void NamedTupleToNamedTupleWithPositionalResolve() + { + var source = TestSourceBuilder.Mapping("(int A, string B)", "(int C, string D)", TestSourceBuilderOptions.WithDeepCloning); + + TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (C: source.A, D: source.B);"); + } + + [Fact] + public void PartiallyNamedTupleToPartiallyNamedTuple() + { + var source = TestSourceBuilder.Mapping("(int, string A)", "(int A, string)"); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped) + .HaveSingleMethodBody("return (A: int.Parse(source.A), source.A);"); + } + + [Fact] + public void TupleToTupleNamedItems() + { + var source = TestSourceBuilder.Mapping("(int, string)", "(int Item2, string Item1)"); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody("return (Item2: int.Parse(source.Item2), Item1: source.Item1.ToString());"); + } + + [Fact] + public void TupleNamedItemsToTuple() + { + var source = TestSourceBuilder.Mapping("(int Item2, string Item1)", "(int, string)"); + + TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (int.Parse(source.Item1), source.Item2.ToString());"); + } + + [Fact] + public void TupleToClassShouldNotDiagnosticUnmapped() + { + var source = TestSourceBuilder.Mapping( + "(string, int)", + "A", + "class A { public string Item1 { get; set; } public int Item2 { get; set; } }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::A(); + target.Item1 = source.Item1; + target.Item2 = source.Item2; + return target; + """ + ); + } + + [Fact] + public void NamedTupleToClassShouldNotDiagnosticUnmapped() + { + var source = TestSourceBuilder.Mapping( + "(string A, int B)", + "C", + "class C { public string A { get; set; } public int B { get; set; } }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::C(); + target.A = source.A; + target.B = source.B; + return target; + """ + ); + } + + [Fact] + public void ClassToTuple() + { + var source = TestSourceBuilder.Mapping( + "A", + "(int B, string C)", + "public class A { public int B { get;set;} public int C {get;set;} }" + ); + + TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (B: source.B, C: source.C.ToString());"); + } + + [Fact] + public void TupleToTupleWithIgnoredSource() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapperIgnoreSource("C")] + partial (int, string) Map((int A, string B, int C) source); + """ + ); + + TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (source.A, source.B);"); + } + + [Fact] + public void TupleToTupleWithIgnoredSourceByPosition() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapperIgnoreSource("Item3")] + partial (int, string) Map((int A, string B, int) source); + """ + ); + + TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (source.A, source.B);"); + } + + [Fact] + public void ClassToTupleWithIgnoredSource() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapperIgnoreSource("A")] + partial (int, int) Map(B source); + """, + "public class B { public int Item1 { get;set;} public int A {get;set;} public int Item2 {get;set;} }" + ); + + TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (source.Item1, source.Item2);"); + } + + [Fact] + public void IgnoredNamedSourceWithPositionalShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapperIgnoreSource("Item3")] + partial (int, string) Map((int A, string B, int C) source); + """ + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.IgnoredSourceMemberNotFound) + .HaveSingleMethodBody("return (source.A, source.B);"); + } + + [Fact] + public void TupleWithNonExistentIgnoreSourceShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapperIgnoreSource("D")] + partial (int, string) Map((int A, string B) source); + """ + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.IgnoredSourceMemberNotFound) + .HaveSingleMethodBody("return (source.A, source.B);"); + } + + [Fact] + public void InvalidTupleShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapperIgnoreSource("A")] + partial (int, int) Map((int, int A) source); + """ + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound) + .HaveDiagnostic(DiagnosticDescriptors.NoConstructorFound) + .HaveSingleMethodBody( + """ + // Could not generate mapping + throw new System.NotImplementedException(); + """ + ); + } + + [Fact] + public void IgnoreTargetTuple() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapperIgnoreTarget("A")] + partial (int, int A) Map((string, int A) source); + """ + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.NoConstructorFound) + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped) + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody( + """ + // Could not generate mapping + throw new System.NotImplementedException(); + """ + ); + } + + [Fact] + public void IgnoreTargetByPosition() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapperIgnoreTarget("Item1")] + partial (int, int A) Map((string, int A) source); + """ + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.NoConstructorFound) + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped) + .HaveAssertedAllDiagnostics() + .HaveSingleMethodBody( + """ + // Could not generate mapping + throw new System.NotImplementedException(); + """ + ); + } + + [Fact] + public void IgnoreTargetWithNonExistentTargetShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapperIgnoreTarget("B")] + partial (int, int A) Map((string, int) source); + """ + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.IgnoredTargetMemberNotFound) + .HaveSingleMethodBody("return (int.Parse(source.Item1), A: source.Item2);"); + } + + [Fact] + public void IgnoreTargetWithNonExistentPositionalShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapperIgnoreTarget("Item3")] + partial (int, int A) Map((string, int) source); + """ + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.IgnoredTargetMemberNotFound) + .HaveSingleMethodBody("return (int.Parse(source.Item1), A: source.Item2);"); + } + + [Fact] + public void TupleToTupleWithMapProperty() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapProperty("C", "A")] + [MapProperty("Item3", "Item2")] + partial (int A, int) Map((int B, int C, int) source); + """ + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotMapped) + .HaveSingleMethodBody("return (A: source.C, source.Item3);"); + } + + [Fact] + public Task TuplePropertyToTupleProperty() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + "class A { public (int A, int) Value { get; set; } }", + "class B { public (string A, int) Value { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task MapPropertyShouldMapToTupleField() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ +[MapProperty("Item2", "Item1.Value")] +partial (A, int) Map((B, string) source); +""", + "class A { public int Value { get; set; } }", + "class B { public int Value { get; set; } }" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public Task MapPropertyShouldMapNestedTuple() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ +[MapProperty("Item2", "Item1.Item1")] +partial ((int, int), int) Map(((int, int), string) source); +""" + ); + + return TestHelper.VerifyGenerator(source); + } + + [Fact] + public void TupleToValueTuple() + { + var source = TestSourceBuilder.Mapping("(int A, string B)", "ValueTuple"); + + TestHelper.GenerateMapper(source).Should().HaveSingleMethodBody("return (source.A, source.B);"); + } + + [Fact] + public void QueryableTupleToQueryableTuple() + { + var source = TestSourceBuilder.Mapping("System.Linq.IQueryable<(int A, string B)>", "System.Linq.IQueryable<(int, string)>"); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ +#nullable disable +return System.Linq.Queryable.Select(source, x => new global::System.ValueTuple(x.A, x.B)); +#nullable enable +""" + ); + } + + [Fact] + public void QueryableTupleToIQueryableValueTuple() + { + var source = TestSourceBuilder.Mapping( + "System.Linq.IQueryable<(int A, string B)>", + "System.Linq.IQueryable>" + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveSingleMethodBody( + """ +#nullable disable +return System.Linq.Queryable.Select(source, x => new global::System.ValueTuple(x.A, x.B)); +#nullable enable +""" + ); + } + + [Fact] + public void TupleToTupleWithManyMapPropertyShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapProperty("B", "A")] + [MapProperty("B", "A")] + partial (int A, int) Map((int, string B) source); + """ + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.MultipleConfigurationsForConstructorParameter); + } + + [Fact] + public void TupleToTupleWithMapPropertyWithImplicitNameShouldDiagnostic() + { + var source = TestSourceBuilder.MapperWithBodyAndTypes( + """ + [MapProperty("Item2", "Item1")] + partial (int A, int B) Map((int C, int D) source); + """ + ); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.ConfiguredMappingTargetMemberNotFound); + } + + [Fact] + public void TupleMappingDisabledShouldDiagnostic() + { + var source = TestSourceBuilder.Mapping( + "(int, string)", + "(string, int)", + TestSourceBuilderOptions.WithDisabledMappingConversion(MappingConversionType.Tuple) + ); + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.CouldNotCreateMapping) + .HaveSingleMethodBody( + """ + // Could not generate mapping + throw new System.NotImplementedException(); + """ + ); + } + + [Fact] + public void ClassToTupleWithNoMappingsShouldDiagnostic() + { + var source = TestSourceBuilder.Mapping("A", "(int, int)", "public class A { }"); + + TestHelper + .GenerateMapper(source, TestHelperOptions.AllowDiagnostics) + .Should() + .HaveDiagnostic(DiagnosticDescriptors.SourceMemberNotFound) + .HaveSingleMethodBody( + """ + // Could not generate mapping + throw new System.NotImplementedException(); + """ + ); + } +} diff --git a/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.MapPropertyShouldMapNestedTuple#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.MapPropertyShouldMapNestedTuple#Mapper.g.verified.cs new file mode 100644 index 0000000000..d8432bf55c --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.MapPropertyShouldMapNestedTuple#Mapper.g.verified.cs @@ -0,0 +1,12 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + private partial ((int, int), int) Map(((int, int), string) source) + { + var target = (source.Item1, int.Parse(source.Item2)); + target.Item1.Item1 = int.Parse(source.Item2); + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.MapPropertyShouldMapToTupleField#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.MapPropertyShouldMapToTupleField#Mapper.g.verified.cs new file mode 100644 index 0000000000..ced7e60ab6 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.MapPropertyShouldMapToTupleField#Mapper.g.verified.cs @@ -0,0 +1,19 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + private partial (global::A, int) Map((global::B, string) source) + { + var target = (MapToA(source.Item1), int.Parse(source.Item2)); + target.Item1.Value = int.Parse(source.Item2); + return target; + } + + private global::A MapToA(global::B source) + { + var target = new global::A(); + target.Value = source.Value; + return target; + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.TuplePropertyToTupleProperty#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.TuplePropertyToTupleProperty#Mapper.g.verified.cs new file mode 100644 index 0000000000..c791967bf4 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ValueTupleTest.TuplePropertyToTupleProperty#Mapper.g.verified.cs @@ -0,0 +1,12 @@ +//HintName: Mapper.g.cs +// +#nullable enable +public partial class Mapper +{ + private partial global::B Map(global::A source) + { + var target = new global::B(); + target.Value = (A: source.Value.A.ToString(), source.Value.Item2); + return target; + } +} \ No newline at end of file