Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added tuple mapping support #467

Merged
merged 1 commit into from Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/docs/configuration/conversions.md
Expand Up @@ -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)`. |
Expand Down
7 changes: 7 additions & 0 deletions src/Riok.Mapperly.Abstractions/MappingConversionType.cs
Expand Up @@ -107,6 +107,13 @@ public enum MappingConversionType
/// </summary>
Memory = 1 << 14,

/// <summary>
/// If the target is a <see cref="ValueTuple{T, U}"/> or tuple expression (A: 10, B: 12).
/// Supports positional and named mapping.
/// Only uses <see cref="ValueTuple{T, U}"/> in <see cref="IQueryable{T}"/>.
/// </summary>
Tuple = 1 << 15,

/// <summary>
/// Enables all supported conversions.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
Expand Up @@ -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
Expand Down
@@ -0,0 +1,15 @@
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.Mappings.MemberMappings;

namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext;

/// <summary>
/// A <see cref="IMembersBuilderContext{T}"/> for mappings which create the target object
/// via a tuple expression (eg. (A: source.A, B: MapToB(source.B))).
/// </summary>
/// <typeparam name="T">The mapping type.</typeparam>
public interface INewValueTupleBuilderContext<out T> : IMembersBuilderContext<T>
where T : IMapping
{
void AddTupleConstructorParameterMapping(ValueTupleConstructorParameterMapping mapping);
}
@@ -0,0 +1,22 @@
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.Mappings.MemberMappings;

namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext;

/// <summary>
/// An implementation of <see cref="INewValueTupleBuilderContext{T}"/>.
/// </summary>
/// <typeparam name="T">The type of the mapping.</typeparam>
public class NewValueTupleConstructorBuilderContext<T> : MembersMappingBuilderContext<T>, INewValueTupleBuilderContext<T>
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);
}
}
@@ -0,0 +1,34 @@
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.Mappings.MemberMappings;

namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext;

/// <summary>
/// An implementation of <see cref="INewValueTupleBuilderContext{T}"/>.
/// </summary>
/// <typeparam name="T">The type of the mapping.</typeparam>
public class NewValueTupleExpressionBuilderContext<T> : MembersContainerBuilderContext<T>, INewValueTupleBuilderContext<T>
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))
{
// remove the mapping used to map the tuple constructor
value.RemoveAll(x => x.Target.Path.Count == 1);
TimothyMakkison marked this conversation as resolved.
Show resolved Hide resolved

// remove from dictionary and target members if there aren't any more mappings
if (!value.Any())
{
MemberConfigsByRootTargetName.Remove(mapping.Parameter.Name);
TargetMembers.Remove(mapping.Parameter.Name);
}
}

SetSourceMemberMapped(mapping.DelegateMapping.SourcePath);
Mapping.AddConstructorParameterMapping(mapping);
}
}
Expand Up @@ -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;
Expand Down
@@ -0,0 +1,226 @@
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<NewValueTupleConstructorMapping>(ctx, expressionMapping);
BuildTupleConstructorMapping(mappingCtx);
mappingCtx.AddDiagnostics();
}

public static void BuildMappingBody(MappingBuilderContext ctx, NewValueTupleExpressionMapping expressionMapping)
{
var mappingCtx = new NewValueTupleExpressionBuilderContext<NewValueTupleExpressionMapping>(ctx, expressionMapping);
BuildTupleConstructorMapping(mappingCtx);
ObjectMemberMappingBodyBuilder.BuildMappingBody(mappingCtx);
mappingCtx.AddDiagnostics();
}

private static void BuildTupleConstructorMapping(INewValueTupleBuilderContext<INewValueTupleMapping> ctx)
{
if (ctx.Mapping.TargetType is not INamedTypeSymbol namedTargetType)
{
ctx.BuilderContext.ReportDiagnostic(DiagnosticDescriptors.NoConstructorFound, ctx.BuilderContext.Target);
return;

Check warning on line 35 in src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs

View check run for this annotation

Codecov / codecov/patch

src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs#L33-L35

Added lines #L33 - L35 were not covered by tests
}

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.ContainsKey(x));

ctx.TargetMembers.RemoveRange(removableMappedTargetMemberNames);
foreach (var constructorParameterMapping in constructorParameterMappings)
{
ctx.AddTupleConstructorParameterMapping(constructorParameterMapping);
}
}

private static bool TryBuildTupleConstructorMapping(
INewValueTupleBuilderContext<INewValueTupleMapping> ctx,
INamedTypeSymbol namedTargetType,
out HashSet<ValueTupleConstructorParameterMapping> constructorParameterMappings,
out HashSet<string> mappedTargetMemberNames
)
{
mappedTargetMemberNames = new HashSet<string>();
constructorParameterMappings = new HashSet<ValueTupleConstructorParameterMapping>();

foreach (var targetMember in namedTargetType.TupleElements)
{
if (!ctx.TargetMembers.ContainsKey(targetMember.Name))
{
return false;
}

if (!TryFindConstructorParameterSourcePath(ctx, targetMember, out var sourcePath))
TimothyMakkison marked this conversation as resolved.
Show resolved Hide resolved
{
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;

Check warning on line 99 in src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs

View check run for this annotation

Codecov / codecov/patch

src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs#L89-L99

Added lines #L89 - L99 were not covered by tests
}

if (delegateMapping.Equals(ctx.Mapping))
{
ctx.BuilderContext.ReportDiagnostic(
DiagnosticDescriptors.ReferenceLoopInCtorMapping,
ctx.Mapping.SourceType,
sourcePath.FullName,
ctx.Mapping.TargetType,
targetMember.Name
);
return false;

Check warning on line 111 in src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs

View check run for this annotation

Codecov / codecov/patch

src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs#L103-L111

Added lines #L103 - L111 were not covered by tests
}

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<INewValueTupleMapping> 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);
}

// remove nested targets
var initMemberPaths = memberConfigs.Where(x => x.Target.Path.Count == 1).ToArray();

// if all memberConfigs are nested than do normal mapping
if (initMemberPaths.Length == 0)
{
return TryBuildConstructorParameterSourcePath(ctx, field, out sourcePath);
}

if (initMemberPaths.Length > 1)
{
ctx.BuilderContext.ReportDiagnostic(
DiagnosticDescriptors.MultipleConfigurationsForConstructorParameter,
field.Type,
field.Name
);
}

var memberConfig = initMemberPaths.First();

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;

Check warning on line 171 in src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs

View check run for this annotation

Codecov / codecov/patch

src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs#L164-L171

Added lines #L164 - L171 were not covered by tests
}

return true;
}

private static bool TryBuildConstructorParameterSourcePath(
INewValueTupleBuilderContext<INewValueTupleMapping> 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, try to use the positional fields
// if source is a tuple compare the underlying field ie, Item1, Item2
if (ctx.Mapping.SourceType.IsTupleType)
{
if (ctx.Mapping.SourceType is not INamedTypeSymbol namedType)
return false;

Check warning on line 204 in src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs

View check run for this annotation

Codecov / codecov/patch

src/Riok.Mapperly/Descriptors/MappingBodyBuilders/NewValueTupleMappingBodyBuilder.cs#L204

Added line #L204 was not covered by tests

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;
}
}
Expand Up @@ -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)
TimothyMakkison marked this conversation as resolved.
Show resolved Hide resolved
return null;

if (SymbolEqualityComparer.Default.Equals(ctx.Source, ctx.Compilation.ObjectType))
return null;

Expand Down
Expand Up @@ -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;
}
Expand Down
@@ -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;
Expand Down Expand Up @@ -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
Expand Down