Skip to content

Commit

Permalink
feat: Add option to use other mappers
Browse files Browse the repository at this point in the history
  • Loading branch information
latonz committed Aug 28, 2023
1 parent 07e6ce8 commit 497a0d8
Show file tree
Hide file tree
Showing 30 changed files with 1,125 additions and 94 deletions.
20 changes: 0 additions & 20 deletions docs/docs/configuration/user-implemented-methods.md

This file was deleted.

87 changes: 87 additions & 0 deletions docs/docs/configuration/user-implemented-methods.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
---
sidebar_position: 5
description: Manually implement mappings
---

import Tabs from '@theme/Tabs';

# User implemented mapping methods

If Mapperly cannot generate a mapping, one can implement it manually simply by providing a method body in the mapper declaration:

```csharp
[Mapper]
public partial class CarMapper
{
public partial CarDto CarToCarDto(Car car);

private int TimeSpanToHours(TimeSpan t) => t.Hours;
}
```

Whenever Mapperly needs a mapping from `TimeSpan` to `int` inside the `CarMapper` implementation, it will use the provided implementation.

## Use external mappings

Mapperly can also consider mappings implemented in other classes.
In order for Mapperly to find the mappings, they must be made known with `UseMapper` / `UseStaticMapper`.

<!-- do not indent this, it won't work, https://stackoverflow.com/a/67579641/3302887 -->

<Tabs>
<TabItem value="static" label="Static">

For static mappings, `UseStaticMapper` can be used:
```csharp
[Mapper]
// highlight-start
[UseStaticMapper<BananaMapper>] // for c# language level ≥ 11
[UseStaticMapper(typeof(BananaMapper))] // for c# language level < 11
// highlight-end
public static partial class BoxMapper
{
public static partial BananaBox MapBananaBox(BananaBoxDto dto);
}

public static class BananaMapper
{
public static Banana MapBanana(BananaDto dto)
=> new Banana(dto.Weigth);
}
```

</TabItem>
<TabItem value="instance" label="Instance">

To use the mappings of an object instance `UseMapper` can be used:

```csharp
[Mapper]
public static partial class BoxMapper
{
// highlight-start
[UseMapper]
private readonly BananaMapper _bananaMapper = new();
// highlight-end
public static partial BananaBox MapBananaBox(BananaBoxDto dto);
}

public static class BananaMapper
{
public static Banana MapBanana(BananaDto dto)
=> new Banana(dto.Weigth);
}
```

:::info
The initialization of fields and properties annotated with `UseMapper` needs to be done by the user.
:::

</TabItem>
</Tabs>

Whenever Mapperly needs a mapping from `BananaBox` to `BananaBoxDto` inside the `BoxMapper` implementation,
it will use the provided implementation by the `BananaMapper`.

Used mappers themselves can be Mapperly backed classes.
2 changes: 1 addition & 1 deletion docs/docs/getting-started/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ Try to rebuild the solution or restart the IDE. This is a bug of the IDE.

## My advanced use case isn't supported by Mapperly or needs lots of configuration. What should I do?

Write the mapping for that class manually. You can mix automatically generated mappings and [user implemented mappings](../configuration/user-implemented-methods.md) without problems.
Write the mapping for that class manually. You can mix automatically generated mappings and [user implemented mappings](../configuration/user-implemented-methods.mdx) without problems.
8 changes: 8 additions & 0 deletions src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,11 @@ Riok.Mapperly.Abstractions.MapperIgnoreSourceValueAttribute.SourceValue.get -> S
Riok.Mapperly.Abstractions.MapperIgnoreTargetValueAttribute.TargetValue.get -> System.Enum?
Riok.Mapperly.Abstractions.MapperAttribute.AllowNullPropertyAssignment.get -> bool
Riok.Mapperly.Abstractions.MapperAttribute.AllowNullPropertyAssignment.set -> void
Riok.Mapperly.Abstractions.UseMapperAttribute
Riok.Mapperly.Abstractions.UseMapperAttribute.UseMapperAttribute() -> void
Riok.Mapperly.Abstractions.UseStaticMapperAttribute
Riok.Mapperly.Abstractions.UseStaticMapperAttribute.UseStaticMapperAttribute(System.Type! mapperType) -> void
Riok.Mapperly.Abstractions.UseStaticMapperAttribute<T>
Riok.Mapperly.Abstractions.UseStaticMapperAttribute<T>.UseStaticMapperAttribute() -> void
Riok.Mapperly.Abstractions.MapperAttribute.UseStaticMappers.get -> System.Type![]!

Check warning on line 114 in src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt

View workflow job for this annotation

GitHub Actions / MappingBenchmarks

Symbol 'Riok.Mapperly.Abstractions.MapperAttribute.UseStaticMappers.get -> System.Type![]!' is part of the declared API, but is either not public or could not be found

Check warning on line 114 in src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt

View workflow job for this annotation

GitHub Actions / SourceGeneratorBenchmarks

Symbol 'Riok.Mapperly.Abstractions.MapperAttribute.UseStaticMappers.get -> System.Type![]!' is part of the declared API, but is either not public or could not be found
Riok.Mapperly.Abstractions.MapperAttribute.UseStaticMappers.set -> void

Check warning on line 115 in src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt

View workflow job for this annotation

GitHub Actions / MappingBenchmarks

Symbol 'Riok.Mapperly.Abstractions.MapperAttribute.UseStaticMappers.set -> void' is part of the declared API, but is either not public or could not be found

Check warning on line 115 in src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt

View workflow job for this annotation

GitHub Actions / SourceGeneratorBenchmarks

Symbol 'Riok.Mapperly.Abstractions.MapperAttribute.UseStaticMappers.set -> void' is part of the declared API, but is either not public or could not be found
8 changes: 8 additions & 0 deletions src/Riok.Mapperly.Abstractions/UseMapperAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Riok.Mapperly.Abstractions;

/// <summary>
/// Considers all accessible mapping methods provided by the type of this member.
/// Includes static and instance methods.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class UseMapperAttribute : Attribute { }
21 changes: 21 additions & 0 deletions src/Riok.Mapperly.Abstractions/UseStaticMapperAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Riok.Mapperly.Abstractions;

/// <summary>
/// Considers all static mapping methods provided by the type.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class UseStaticMapperAttribute : Attribute
{
/// <summary>
/// Considers all static mapping methods provided by the <paramref name="mapperType"/>.
/// </summary>
/// <param name="mapperType">The type of which mapping methods will be included.</param>
public UseStaticMapperAttribute(Type mapperType) { }
}

/// <summary>
/// Considers all static mapping methods provided by the generic type.
/// </summary>
/// <typeparam name="T">The type of which mapping methods will be included.</typeparam>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class UseStaticMapperAttribute<T> : Attribute { }
8 changes: 8 additions & 0 deletions src/Riok.Mapperly/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,11 @@ RMG044 | Mapper | Warning | An ignored enum member can not be found on the s
RMG045 | Mapper | Warning | An ignored enum member can not be found on the target enum
RMG046 | Mapper | Error | The used C# language version is not supported by Mapperly, Mapperly requires at least C# 9.0
RMG047 | Mapper | Error | Cannot map to member path due to modifying a temporary value, see CS1612

## Release 3.2

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
RMG048 | Mapper | Error | Used mapper members cannot be nullable
19 changes: 14 additions & 5 deletions src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Descriptors;
Expand Down Expand Up @@ -60,13 +61,14 @@ public IEnumerable<TData> Access<TAttribute, TData>(ISymbol symbol)
var attr = Create<TData>(typeArguments, attrData.ConstructorArguments, syntaxArguments);

var syntaxIndex = attrData.ConstructorArguments.Length;
var propertiesByName = dataType.GetProperties().GroupBy(x => x.Name).ToDictionary(x => x.Key, x => x.First());
foreach (var namedArgument in attrData.NamedArguments)
{
var prop = dataType.GetProperty(namedArgument.Key);
if (prop == null)
if (!propertiesByName.TryGetValue(namedArgument.Key, out var prop))
throw new InvalidOperationException($"Could not get property {namedArgument.Key} of attribute {attrType.FullName}");

prop.SetValue(attr, BuildArgumentValue(namedArgument.Value, prop.PropertyType, syntaxArguments[syntaxIndex]));
var value = BuildArgumentValue(namedArgument.Value, prop.PropertyType, syntaxArguments[syntaxIndex]);
prop.SetValue(attr, value);
syntaxIndex++;
}

Expand Down Expand Up @@ -161,13 +163,20 @@ is InvocationExpressionSyntax
throw new InvalidOperationException($"Cannot create {nameof(StringMemberPath)} from {arg.Kind}");
}

private static object?[] BuildArrayValue(TypedConstant arg, Type targetType)
private static IReadOnlyCollection<object?> BuildArrayValue(TypedConstant arg, Type targetType)
{
if (!targetType.IsGenericType || targetType.GetGenericTypeDefinition() != typeof(IReadOnlyCollection<>))
throw new InvalidOperationException($"{nameof(IReadOnlyCollection<object>)} is the only supported array type");

var elementTargetType = targetType.GetGenericArguments()[0];
return arg.Values.Select(x => BuildArgumentValue(x, elementTargetType, null)).ToArray();
var listType = typeof(List<>).MakeGenericType(elementTargetType);
var list = (IList)Activator.CreateInstance(listType, arg.Values.Length);
foreach (var value in arg.Values.Select(x => BuildArgumentValue(x, elementTargetType, null)))
{
list.Add(value);
}

return (IReadOnlyCollection<object?>)list;
}

private static object? GetEnumValue(TypedConstant arg, Type targetType)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Microsoft.CodeAnalysis;

namespace Riok.Mapperly.Configuration;

public record UseStaticMapperConfiguration(ITypeSymbol MapperType);
10 changes: 10 additions & 0 deletions src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Abstractions.ReferenceHandling;
using Riok.Mapperly.Configuration;
using Riok.Mapperly.Descriptors.ExternalMappings;
using Riok.Mapperly.Descriptors.MappingBodyBuilders;
using Riok.Mapperly.Descriptors.MappingBuilders;
using Riok.Mapperly.Descriptors.ObjectFactories;
Expand Down Expand Up @@ -50,6 +51,7 @@ SymbolAccessor symbolAccessor
ReserveMethodNames();
ExtractObjectFactories();
ExtractUserMappings();
ExtractExternalMappings();
_mappingBodyBuilder.BuildMappingBodies();
BuildMappingMethodNames();
BuildReferenceHandlingParameters();
Expand Down Expand Up @@ -79,6 +81,14 @@ private void ExtractUserMappings()
}
}

private void ExtractExternalMappings()
{
foreach (var externalMapping in ExternalMappingsExtractor.ExtractExternalMappings(_builderContext, _mapperDescriptor.Symbol))
{
_mappings.Add(externalMapping);
}
}

private void ReserveMethodNames()
{
foreach (var methodSymbol in _symbolAccessor.GetAllMembers(_mapperDescriptor.Symbol))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Configuration;
using Riok.Mapperly.Descriptors.Mappings.UserMappings;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Descriptors.ExternalMappings;

internal static class ExternalMappingsExtractor
{
public static IEnumerable<IUserMapping> ExtractExternalMappings(SimpleMappingBuilderContext ctx, INamedTypeSymbol mapperSymbol)
{
// TODO extract attribute data accessor

Check warning on line 14 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / MappingBenchmarks

TODO extract attribute data accessor

Check warning on line 14 in src/Riok.Mapperly/Descriptors/ExternalMappings/ExternalMappingsExtractor.cs

View workflow job for this annotation

GitHub Actions / SourceGeneratorBenchmarks

TODO extract attribute data accessor
var accessor = new AttributeDataAccessor(ctx.SymbolAccessor);

var staticExternalMappers = accessor
.Access<UseStaticMapperAttribute, UseStaticMapperConfiguration>(mapperSymbol)
.Concat(accessor.Access<UseStaticMapperAttribute<object>, UseStaticMapperConfiguration>(mapperSymbol))
.SelectMany(
x =>
UserMethodMappingExtractor.ExtractUserImplementedMappings(
ctx,
x.MapperType,
x.MapperType.FullyQualifiedIdentifierName(),
true
)
);

var externalInstanceMappers = ctx.SymbolAccessor
.GetAllMembers(mapperSymbol)
.SelectMany(x => ValidateAndExtractExternalInstanceMappings(ctx, x));

return staticExternalMappers.Concat(externalInstanceMappers);
}

private static IEnumerable<IUserMapping> ValidateAndExtractExternalInstanceMappings(SimpleMappingBuilderContext ctx, ISymbol symbol)
{
var (name, type, nullableAnnotation) = symbol switch
{
IFieldSymbol field => (field.Name, field.Type, field.NullableAnnotation),
IPropertySymbol prop => (prop.Name, prop.Type, prop.NullableAnnotation),
_ => (string.Empty, null, NullableAnnotation.None),
};

if (type == null)
return Enumerable.Empty<IUserMapping>();

if (nullableAnnotation != NullableAnnotation.Annotated)
return UserMethodMappingExtractor.ExtractUserImplementedMappings(ctx, type, name, false);

ctx.ReportDiagnostic(DiagnosticDescriptors.ExternalMapperMemberCannotBeNullable, symbol, symbol.ToDisplayString());
return Enumerable.Empty<IUserMapping>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private SwitchExpressionArmSyntax BuildArmIgnoreCase(string ignoreCaseSwitchDesi
.WithDesignation(SingleVariableDesignation(Identifier(ignoreCaseSwitchDesignatedVariableName)));

// source.Value1
var typeMemberAccess = MemberAccess(FullyQualifiedIdentifierName(field.ContainingType.NonNullable()), field.Name);
var typeMemberAccess = MemberAccess(field.ContainingType.NonNullable().FullyQualifiedIdentifierName(), field.Name);

// when s.Equals(nameof(source.Value1), StringComparison.OrdinalIgnoreCase)
var whenClause = WhenClause(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Riok.Mapperly.Descriptors.Mappings.ExistingTarget;
using Riok.Mapperly.Emit;
using Riok.Mapperly.Helpers;
using Riok.Mapperly.Symbols;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Riok.Mapperly.Emit.SyntaxFactoryHelper;

namespace Riok.Mapperly.Descriptors.Mappings.UserMappings;

Expand All @@ -16,8 +16,10 @@ public class UserImplementedExistingTargetMethodMapping : ExistingTargetMapping,
private readonly MethodParameter _sourceParameter;
private readonly MethodParameter _targetParameter;
private readonly MethodParameter? _referenceHandlerParameter;
private readonly string? _receiver;

public UserImplementedExistingTargetMethodMapping(
string? receiver,
IMethodSymbol method,
MethodParameter sourceParameter,
MethodParameter targetParameter,
Expand All @@ -29,24 +31,20 @@ public UserImplementedExistingTargetMethodMapping(
_sourceParameter = sourceParameter;
_targetParameter = targetParameter;
_referenceHandlerParameter = referenceHandlerParameter;
_receiver = receiver;
}

public IMethodSymbol Method { get; }

public ExpressionSyntax Build(TypeMappingBuildContext ctx) =>
throw new InvalidOperationException(
$"{nameof(UserImplementedExistingTargetMethodMapping)} {ctx.Source}, {ctx.ReferenceHandler} does not support {nameof(Build)}"
);

public override IEnumerable<StatementSyntax> Build(TypeMappingBuildContext ctx, ExpressionSyntax target)
{
// if the user implemented method is on an interface,
// we explicitly cast to be able to use the default interface implementation or explicit implementations
if (Method.ReceiverType?.TypeKind != TypeKind.Interface)
{
yield return SyntaxFactory.ExpressionStatement(
SyntaxFactoryHelper.Invocation(
Method.Name,
yield return ExpressionStatement(
Invocation(
_receiver == null ? IdentifierName(Method.Name) : MemberAccess(_receiver, Method.Name),
_sourceParameter.WithArgument(ctx.Source),
_targetParameter.WithArgument(target),
_referenceHandlerParameter?.WithArgument(ctx.ReferenceHandler)
Expand All @@ -55,13 +53,13 @@ public override IEnumerable<StatementSyntax> Build(TypeMappingBuildContext ctx,
yield break;
}

var castedThis = SyntaxFactory.CastExpression(
SyntaxFactoryHelper.FullyQualifiedIdentifier(Method.ReceiverType!),
SyntaxFactory.ThisExpression()
var castedThis = CastExpression(
FullyQualifiedIdentifier(Method.ReceiverType!),
_receiver != null ? IdentifierName(_receiver) : ThisExpression()
);
var method = SyntaxFactoryHelper.MemberAccess(SyntaxFactory.ParenthesizedExpression(castedThis), Method.Name);
yield return SyntaxFactory.ExpressionStatement(
SyntaxFactoryHelper.Invocation(
var method = MemberAccess(ParenthesizedExpression(castedThis), Method.Name);
yield return ExpressionStatement(
Invocation(
method,
_sourceParameter.WithArgument(ctx.Source),
_targetParameter.WithArgument(target),
Expand Down
Loading

0 comments on commit 497a0d8

Please sign in to comment.