Skip to content

Commit

Permalink
fix: support mapping to target readonly collections (#252)
Browse files Browse the repository at this point in the history
Introduces a new concept of ExistingTargetMappings,
which are mappings which map to an existing target.
Currently only Collection-, Dictionary- and ObjectPropertyMappings
support existing target mappings.
For an object property an existing target mapping is tried to be
built if it the source and target property is readable,
but the target property is not setable.
  • Loading branch information
latonz authored Jan 25, 2023
1 parent fcaf5a5 commit 8fa4116
Show file tree
Hide file tree
Showing 75 changed files with 1,326 additions and 512 deletions.
113 changes: 15 additions & 98 deletions src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,15 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Configuration;
using Riok.Mapperly.Descriptors.MappingBodyBuilder;
using Riok.Mapperly.Descriptors.MappingBuilder;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.MappingBodyBuilders;
using Riok.Mapperly.Descriptors.MappingBuilders;
using Riok.Mapperly.Descriptors.ObjectFactories;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Descriptors;

public class DescriptorBuilder
{
private delegate ITypeMapping? MappingBuilder(MappingBuilderContext context);

private static readonly IReadOnlyCollection<MappingBuilder> _mappingBuilders = new MappingBuilder[]
{
NullableMappingBuilder.TryBuildMapping,
SpecialTypeMappingBuilder.TryBuildMapping,
DirectAssignmentMappingBuilder.TryBuildMapping,
DictionaryMappingBuilder.TryBuildMapping,
EnumerableMappingBuilder.TryBuildMapping,
ImplicitCastMappingBuilder.TryBuildMapping,
ParseMappingBuilder.TryBuildMapping,
CtorMappingBuilder.TryBuildMapping,
StringToEnumMappingBuilder.TryBuildMapping,
EnumToStringMappingBuilder.TryBuildMapping,
EnumMappingBuilder.TryBuildMapping,
ExplicitCastMappingBuilder.TryBuildMapping,
ToStringMappingBuilder.TryBuildMapping,
NewInstanceObjectPropertyMappingBuilder.TryBuildMapping,
};

private readonly SourceProductionContext _context;
private readonly ITypeSymbol _mapperSymbol;
private readonly MapperDescriptor _mapperDescriptor;
Expand All @@ -41,12 +20,9 @@ public class DescriptorBuilder
// Usually these are derived from the mapper attribute or default values.
private readonly Dictionary<Type, Attribute> _defaultConfigurations = new();

// queue of mappings which don't have the body built yet
private readonly Queue<(ITypeMapping, MappingBuilderContext)> _mappingsToBuildBody = new();

private readonly MappingCollection _mappings = new();

private readonly MethodNameBuilder _methodNameBuilder = new();
private readonly MappingBodyBuilder _mappingBodyBuilder;

public DescriptorBuilder(
SourceProductionContext sourceContext,
Expand All @@ -56,9 +32,12 @@ public DescriptorBuilder(
{
_mapperSymbol = mapperSymbol;
_context = sourceContext;
_mapperDescriptor = new MapperDescriptor(mapperSyntax, mapperSymbol, _methodNameBuilder);
_mappingBodyBuilder = new MappingBodyBuilder(_mappings);
Compilation = compilation;
WellKnownTypes = new WellKnownTypes(Compilation);
_mapperDescriptor = new MapperDescriptor(mapperSyntax, mapperSymbol, _methodNameBuilder);
MappingBuilder = new MappingBuilder(this, _mappings);
ExistingTargetMappingBuilder = new ExistingTargetMappingBuilder(this, _mappings);
MapperConfiguration = Configure();
}

Expand All @@ -72,6 +51,10 @@ public DescriptorBuilder(

public MapperAttribute MapperConfiguration { get; }

public MappingBuilder MappingBuilder { get; }

public ExistingTargetMappingBuilder ExistingTargetMappingBuilder { get; }

private MapperAttribute Configure()
{
var mapperAttribute = AttributeDataAccessor.AccessFirstOrDefault<MapperAttribute>(Compilation, _mapperSymbol) ?? new();
Expand All @@ -91,7 +74,7 @@ public MapperDescriptor Build()
ReserveMethodNames();
ExtractObjectFactories();
ExtractUserMappings();
BuildMappingBodies();
_mappingBodyBuilder.BuildMappingBodies();
BuildMappingMethodNames();
BuildReferenceHandlingParameters();
AddMappingsToDescriptor();
Expand All @@ -103,69 +86,22 @@ private void ExtractObjectFactories()
var ctx = new SimpleMappingBuilderContext(this);
ObjectFactories = ObjectFactoryBuilder.ExtractObjectFactories(ctx, _mapperSymbol);
}
public ITypeMapping? FindMapping(ITypeSymbol sourceType, ITypeSymbol targetType)
=> _mappings.FindMapping(sourceType, targetType);

public ITypeMapping? FindOrBuildMapping(
ITypeSymbol sourceType,
ITypeSymbol targetType)
{
if (_mappings.FindMapping(sourceType, targetType) is { } foundMapping)
return foundMapping;

if (BuildDelegateMapping(null, sourceType, targetType) is not { } mapping)
return null;

_mappings.AddMapping(mapping);
return mapping;
}

public ITypeMapping? BuildMappingWithUserSymbol(
ISymbol userSymbol,
ITypeSymbol sourceType,
ITypeSymbol targetType)
{
if (BuildDelegateMapping(userSymbol, sourceType, targetType) is not { } mapping)
return null;

_mappings.AddMapping(mapping);
return mapping;
}

public ITypeMapping? BuildDelegateMapping(
ISymbol? userSymbol,
ITypeSymbol sourceType,
ITypeSymbol targetType)
{
var ctx = new MappingBuilderContext(this, sourceType, targetType, userSymbol);
foreach (var mappingBuilder in _mappingBuilders)
{
if (mappingBuilder(ctx) is { } mapping)
{
_mappingsToBuildBody.Enqueue((mapping, ctx));
return mapping;
}
}

return null;
}

internal void ReportDiagnostic(DiagnosticDescriptor descriptor, Location? location, params object[] messageArgs)
=> _context.ReportDiagnostic(Diagnostic.Create(descriptor, location ?? _mapperDescriptor.Syntax.GetLocation(), messageArgs));

private void ExtractUserMappings()
{
var defaultContext = new SimpleMappingBuilderContext(this);
foreach (var userMapping in UserMethodMappingBuilder.ExtractUserMappings(defaultContext, _mapperSymbol))
foreach (var userMapping in UserMethodMappingExtractor.ExtractUserMappings(defaultContext, _mapperSymbol))
{
_mappings.AddMapping(userMapping);

var ctx = new MappingBuilderContext(
this,
userMapping.SourceType,
userMapping.TargetType,
userMapping.Method);
_mappingsToBuildBody.Enqueue((userMapping, ctx));
_mappings.AddMapping(userMapping);
_mappings.EnqueueMappingToBuildBody(userMapping, ctx);
}
}

Expand All @@ -177,25 +113,6 @@ private void ReserveMethodNames()
}
}

private void BuildMappingBodies()
{
foreach (var (typeMapping, ctx) in _mappingsToBuildBody.DequeueAll())
{
switch (typeMapping)
{
case NewInstanceObjectPropertyMapping mapping:
NewInstanceObjectPropertyMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
case ObjectPropertyMapping mapping:
ObjectPropertyMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
case UserDefinedNewInstanceMethodMapping mapping:
UserMethodMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
}
}
}

private void BuildMappingMethodNames()
{
foreach (var methodMapping in _mappings.MethodMappings)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.Mappings.ExistingTarget;
using Riok.Mapperly.Descriptors.Mappings.PropertyMappings;

namespace Riok.Mapperly.Descriptors.MappingBodyBuilders;

public class MappingBodyBuilder
{
private readonly MappingCollection _mappings;

public MappingBodyBuilder(MappingCollection mappings)
{
_mappings = mappings;
}

public void BuildMappingBodies()
{
foreach (var (typeMapping, ctx) in _mappings.DequeueMappingsToBuildBody())
{
switch (typeMapping)
{
case NewInstanceObjectPropertyMapping mapping:
NewInstanceObjectPropertyMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
case IPropertyAssignmentTypeMapping mapping:
ObjectPropertyMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
case UserDefinedNewInstanceMethodMapping mapping:
UserMethodMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
case UserDefinedExistingTargetMethodMapping mapping:
UserMethodMappingBodyBuilder.BuildMappingBody(ctx, mapping);
break;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.MappingBuilder;
using Riok.Mapperly.Descriptors.MappingBuilders;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.Mappings.PropertyMappings;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Descriptors.MappingBodyBuilder;
namespace Riok.Mapperly.Descriptors.MappingBodyBuilders;

public static class NewInstanceObjectPropertyMappingBodyBuilder
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.MappingBuilder;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.MappingBuilders;
using Riok.Mapperly.Descriptors.Mappings.PropertyMappings;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Descriptors.MappingBodyBuilder;
namespace Riok.Mapperly.Descriptors.MappingBodyBuilders;

public static class ObjectPropertyMappingBodyBuilder
{
public static void BuildMappingBody(MappingBuilderContext ctx, ObjectPropertyMapping mapping)
public static void BuildMappingBody(MappingBuilderContext ctx, IPropertyAssignmentTypeMapping mapping)
{
var mappingCtx = new ObjectPropertyMappingBuilderContext<ObjectPropertyMapping>(ctx, mapping);
var mappingCtx = new ObjectPropertyMappingBuilderContext<IPropertyAssignmentTypeMapping>(ctx, mapping);
BuildMappingBody(mappingCtx);
}

Expand Down Expand Up @@ -87,7 +86,7 @@ public static bool ValidateMappingSpecification(
bool allowInitOnlyMember = false)
{
// the target property path is readonly or not accessible
if (targetPropertyPath.Member.IsReadOnly || targetPropertyPath.Member.SetMethod?.IsAccessible() != true)
if (!targetPropertyPath.Member.CanSet())
{
ctx.BuilderContext.ReportDiagnostic(
DiagnosticDescriptors.CannotMapToReadOnlyProperty,
Expand All @@ -101,7 +100,7 @@ public static bool ValidateMappingSpecification(
}

// a target property path part is write only or not accessible
if (targetPropertyPath.ObjectPath.Any(p => p.IsWriteOnly || p.GetMethod?.IsAccessible() != true))
if (targetPropertyPath.ObjectPath.Any(p => !p.CanGet()))
{
ctx.BuilderContext.ReportDiagnostic(
DiagnosticDescriptors.CannotMapToWriteOnlyPropertyPath,
Expand Down Expand Up @@ -130,7 +129,7 @@ public static bool ValidateMappingSpecification(
}

// a source property path is write only or not accessible
if (sourcePropertyPath.Path.Any(p => p.IsWriteOnly || p.GetMethod?.IsAccessible() != true))
if (sourcePropertyPath.Path.Any(p => !p.CanGet()))
{
ctx.BuilderContext.ReportDiagnostic(
DiagnosticDescriptors.CannotMapFromWriteOnlyProperty,
Expand Down Expand Up @@ -163,12 +162,16 @@ private static void BuildPropertyAssignmentMapping(
PropertyPath sourcePropertyPath,
PropertyPath targetPropertyPath)
{
if (TryAddExistingTargetMapping(ctx, sourcePropertyPath, targetPropertyPath))
return;

if (!ValidateMappingSpecification(ctx, sourcePropertyPath, targetPropertyPath))
return;

// nullability is handled inside the property mapping
var delegateMapping = ctx.BuilderContext.FindMapping(sourcePropertyPath.Member.Type, targetPropertyPath.Member.Type)
?? ctx.BuilderContext.FindOrBuildMapping(sourcePropertyPath.Member.Type.NonNullable(),
?? ctx.BuilderContext.FindOrBuildMapping(
sourcePropertyPath.Member.Type.NonNullable(),
targetPropertyPath.Member.Type.NonNullable());

// couldn't build the mapping
Expand Down Expand Up @@ -216,4 +219,34 @@ private static void BuildPropertyAssignmentMapping(
targetPropertyPath,
new PropertyMapping(delegateMapping, sourcePropertyPath, false, true)));
}

private static bool TryAddExistingTargetMapping(
ObjectPropertyMappingBuilderContext ctx,
PropertyPath sourcePropertyPath,
PropertyPath targetPropertyPath)
{
// if the property is readonly
// and the target and source path is readable,
// we try to create an existing target mapping
if (targetPropertyPath.Member.CanSet()
|| !targetPropertyPath.Path.All(op => op.CanGet())
|| !sourcePropertyPath.Path.All(op => op.CanGet()))
{
return false;
}

var existingTargetMapping = ctx.BuilderContext.FindOrBuildExistingTargetMapping(
sourcePropertyPath.Member.Type,
targetPropertyPath.Member.Type);
if (existingTargetMapping == null)
return false;


var propertyMapping = new PropertyExistingTargetMapping(
existingTargetMapping,
sourcePropertyPath,
targetPropertyPath);
ctx.AddPropertyAssignmentMapping(propertyMapping);
return true;
}
}
Loading

0 comments on commit 8fa4116

Please sign in to comment.