Skip to content

Commit

Permalink
feat: Add option to use other mappers (#661)
Browse files Browse the repository at this point in the history
  • Loading branch information
latonz authored Aug 29, 2023
1 parent 8e3991f commit 975f374
Show file tree
Hide file tree
Showing 34 changed files with 1,138 additions and 86 deletions.
1 change: 0 additions & 1 deletion docs/docs/configuration/analyzer-diagnostics.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ You won't see the updated mapper code or mapper diagnostics until you perform a

This is done for performance reasons,
otherwise the IDE could become laggy.
Improvements are tracked in [#72](https://github.com/riok/mapperly/issues/72).
:::

## Editorconfig
Expand Down
1 change: 0 additions & 1 deletion docs/docs/configuration/generated-source.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,4 @@ You won't see the updated mapper code or mapper diagnostics until you perform a

This is done for performance reasons,
otherwise the IDE could become laggy.
Improvements are tracked in [#72](https://github.com/riok/mapperly/issues/72).
:::
20 changes: 0 additions & 20 deletions docs/docs/configuration/user-implemented-methods.md

This file was deleted.

88 changes: 88 additions & 0 deletions docs/docs/configuration/user-implemented-methods.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
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.
6 changes: 6 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,9 @@ 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
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
12 changes: 8 additions & 4 deletions src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Riok.Mapperly.Configuration;
/// <summary>
/// Creates <see cref="Attribute"/> instances by resolving attribute data from provided symbols.
/// </summary>
internal class AttributeDataAccessor
public class AttributeDataAccessor
{
private const string NameOfOperatorName = "nameof";
private const char FullNameOfPrefix = '@';
Expand All @@ -27,6 +27,9 @@ public T AccessSingle<T>(ISymbol symbol)
where TAttribute : Attribute
where TData : notnull => Access<TAttribute, TData>(symbol).FirstOrDefault();

public bool HasAttribute<TAttribute>(ISymbol symbol)
where TAttribute : Attribute => _symbolAccessor.GetAttributes<TAttribute>(symbol).Any();

public IEnumerable<TAttribute> Access<TAttribute>(ISymbol symbol)
where TAttribute : Attribute => Access<TAttribute, TAttribute>(symbol);

Expand Down Expand Up @@ -60,13 +63,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
5 changes: 2 additions & 3 deletions src/Riok.Mapperly/Configuration/MapperConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Configuration;
Expand All @@ -10,9 +9,9 @@ public class MapperConfiguration
private readonly MappingConfiguration _defaultConfiguration;
private readonly AttributeDataAccessor _dataAccessor;

public MapperConfiguration(SymbolAccessor symbolAccessor, ISymbol mapperSymbol)
public MapperConfiguration(AttributeDataAccessor dataAccessor, ISymbol mapperSymbol)
{
_dataAccessor = new AttributeDataAccessor(symbolAccessor);
_dataAccessor = dataAccessor;
Mapper = _dataAccessor.AccessSingle<MapperAttribute>(mapperSymbol);
_defaultConfiguration = new MappingConfiguration(
new EnumMappingConfiguration(
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);
15 changes: 14 additions & 1 deletion 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 @@ -33,11 +34,14 @@ SymbolAccessor symbolAccessor
_mapperDescriptor = new MapperDescriptor(mapperSyntax, mapperSymbol, _methodNameBuilder);
_symbolAccessor = symbolAccessor;
_mappingBodyBuilder = new MappingBodyBuilder(_mappings);

var attributeAccessor = new AttributeDataAccessor(symbolAccessor);
_builderContext = new SimpleMappingBuilderContext(
compilation,
new MapperConfiguration(symbolAccessor, mapperSymbol),
new MapperConfiguration(attributeAccessor, mapperSymbol),
wellKnownTypes,
_symbolAccessor,
attributeAccessor,
_mapperDescriptor,
_diagnostics,
new MappingBuilder(_mappings),
Expand All @@ -50,6 +54,7 @@ SymbolAccessor symbolAccessor
ReserveMethodNames();
ExtractObjectFactories();
ExtractUserMappings();
ExtractExternalMappings();
_mappingBodyBuilder.BuildMappingBodies();
BuildMappingMethodNames();
BuildReferenceHandlingParameters();
Expand Down Expand Up @@ -79,6 +84,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,53 @@
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)
{
var staticExternalMappers = ctx.AttributeAccessor
.Access<UseStaticMapperAttribute, UseStaticMapperConfiguration>(mapperSymbol)
.Concat(ctx.AttributeAccessor.Access<UseStaticMapperAttribute<object>, UseStaticMapperConfiguration>(mapperSymbol))
.SelectMany(
x =>
UserMethodMappingExtractor.ExtractUserImplementedMappings(
ctx,
x.MapperType,
x.MapperType.FullyQualifiedIdentifierName(),
true
)
);

var externalInstanceMappers = ctx.SymbolAccessor
.GetAllMembers(mapperSymbol)
.Where(x => ctx.AttributeAccessor.HasAttribute<UseMapperAttribute>(x))
.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
Loading

0 comments on commit 975f374

Please sign in to comment.