Skip to content

Commit

Permalink
feat: support flattening
Browse files Browse the repository at this point in the history
  • Loading branch information
latonz committed Feb 28, 2022
1 parent 522e2b4 commit 9f74d8d
Show file tree
Hide file tree
Showing 98 changed files with 1,830 additions and 281 deletions.
11 changes: 9 additions & 2 deletions README.md
Expand Up @@ -45,7 +45,7 @@ The attributes defined in `Riok.Mapperly.Abstractions` can be used to customize
The `MapperAttribute` provides options to customize the generated mapper class.
The generated class name, the instance field name and the default enum mapping strategy is adjustable.

### Copy behaviour
#### Copy behaviour

By default, Mapperly does not create deep copies of objects to improve performance.
If an object can be directly assigned to the target, it will do so
Expand All @@ -56,9 +56,16 @@ To create deep copies, set the `UseDeepCloning` property on the `MapperAttribute

On each mapping method declaration property mappings can be customized.
If a property on the target has a different name than on the source, the `MapPropertyAttribute` can be applied.
Flattening is not yet supported.
If a property should be ignored, the `MapperIgnoreAttribute` can be used.

#### Flattening and unflattening

It is pretty common to flatten objects during mapping, e.g. `Car.Make.Id => Car.MakeId`.
Mapperly tries to figure out flattenings automatically by making use of the pascal case c# notation.
If Mapperly can't resolve the target or source property correctly, it is possible to manually configure it by applying the `MapPropertyAttribute`
by either using the source and target property path names as arrays or using a dot separated property access path string (e.g. `[MapProperty(Source = new[] { nameof(Car), nameof(Car.Make), nameof(Car.Make.Id) }, Target = new[] { nameof(Car), nameof(Car.MakeId) })]` or `[MapProperty(Source = "Car.Make.Id", Target = "Car.MakeId")]`).
Note: unflattening is not yet automatically configured by Mapperly and needs to be configured manually via `MapPropertyAttribute`.

#### Enum

The enum mapping can be customized by setting the strategy to use.
Expand Down
7 changes: 7 additions & 0 deletions Riok.Mapperly.sln
Expand Up @@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riok.Mapperly.Abstractions"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riok.Mapperly.Tests", "test\Riok.Mapperly.Tests\Riok.Mapperly.Tests.csproj", "{284E2122-CE48-4A5A-A045-3A3F941DA5C3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riok.Mapperly.Abstractions.Test", "test\Riok.Mapperly.Abstractions.Test\Riok.Mapperly.Abstractions.Test.csproj", "{C3C40A0A-168F-4A66-B9F9-FC80D2F26306}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -40,11 +42,16 @@ Global
{284E2122-CE48-4A5A-A045-3A3F941DA5C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{284E2122-CE48-4A5A-A045-3A3F941DA5C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{284E2122-CE48-4A5A-A045-3A3F941DA5C3}.Release|Any CPU.Build.0 = Release|Any CPU
{C3C40A0A-168F-4A66-B9F9-FC80D2F26306}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C3C40A0A-168F-4A66-B9F9-FC80D2F26306}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3C40A0A-168F-4A66-B9F9-FC80D2F26306}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C3C40A0A-168F-4A66-B9F9-FC80D2F26306}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{CB991FD7-B9B7-47C0-A060-66EBE136DBDA} = {B65AF89A-4A3B-473C-83C8-5F0CB0EED30E}
{FDA97A46-DB21-4B72-9958-6D61C508B1CD} = {3598BE50-28D5-4BF4-BEA7-09E5FEA2910C}
{E45D5E6D-8CC9-4DAD-8E1C-723625475744} = {B65AF89A-4A3B-473C-83C8-5F0CB0EED30E}
{284E2122-CE48-4A5A-A045-3A3F941DA5C3} = {3598BE50-28D5-4BF4-BEA7-09E5FEA2910C}
{C3C40A0A-168F-4A66-B9F9-FC80D2F26306} = {3598BE50-28D5-4BF4-BEA7-09E5FEA2910C}
EndGlobalSection
EndGlobal
31 changes: 27 additions & 4 deletions src/Riok.Mapperly.Abstractions/MapPropertyAttribute.cs
Expand Up @@ -6,12 +6,25 @@ namespace Riok.Mapperly.Abstractions;
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public sealed class MapPropertyAttribute : Attribute
{
private const string PropertyAccessSeparatorStr = ".";
private const char PropertyAccessSeparator = '.';

/// <summary>
/// Maps a specified source property to the specified target property.
/// </summary>
/// <param name="source">The name of the source property. The use of `nameof()` is encouraged.</param>
/// <param name="target">The name of the target property. The use of `nameof()` is encouraged.</param>
/// <param name="source">The name of the source property. The use of `nameof()` is encouraged. A path can be specified by joining property names with a '.'.</param>
/// <param name="target">The name of the target property. The use of `nameof()` is encouraged. A path can be specified by joining property names with a '.'.</param>
public MapPropertyAttribute(string source, string target)
: this(source.Split(PropertyAccessSeparator), target.Split(PropertyAccessSeparator))
{
}

/// <summary>
/// Maps a specified source property to the specified target property.
/// </summary>
/// <param name="source">The path of the source property. The use of `nameof()` is encouraged.</param>
/// <param name="target">The path of the target property. The use of `nameof()` is encouraged.</param>
public MapPropertyAttribute(string[] source, string[] target)
{
Source = source;
Target = target;
Expand All @@ -20,10 +33,20 @@ public MapPropertyAttribute(string source, string target)
/// <summary>
/// Gets the name of the source property.
/// </summary>
public string Source { get; }
public IReadOnlyCollection<string> Source { get; }

/// <summary>
/// Gets the full name of the source property path.
/// </summary>
public string SourceFullName => string.Join(PropertyAccessSeparatorStr, Source);

/// <summary>
/// Gets the name of the target property.
/// </summary>
public string Target { get; }
public IReadOnlyCollection<string> Target { get; }

/// <summary>
/// Gets the full name of the target property path.
/// </summary>
public string TargetFullName => string.Join(PropertyAccessSeparatorStr, Target);
}
4 changes: 4 additions & 0 deletions src/Riok.Mapperly/AnalyzerReleases.Shipped.md
Expand Up @@ -12,3 +12,7 @@ RMG005 | Mapper | Error | Mapping target property not found.
RMG006 | Mapper | Error | Mapping source property not found.
RMG007 | Mapper | Error | Could not map property.
RMG008 | Mapper | Error | Could not create mapping.
RMG009 | Mapper | Info | Can not map to read only property.
RMG010 | Mapper | Info | Can not map from write only property.
RMG011 | Mapper | Info | Can not map to write only property path.
RMG012 | Mapper | Info | Mapping source property not found.
64 changes: 45 additions & 19 deletions src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs
Expand Up @@ -31,7 +31,7 @@ public static IEnumerable<T> Access<T>(Compilation compilation, ISymbol symbol)
{
var attr = (T)Activator.CreateInstance(
attrType,
BuildConstructorArguments(attrData));
BuildArgumentValues(attrData.ConstructorArguments).ToArray());

foreach (var namedArgument in attrData.NamedArguments)
{
Expand All @@ -46,24 +46,50 @@ public static IEnumerable<T> Access<T>(Compilation compilation, ISymbol symbol)
}
}

private static object?[] BuildConstructorArguments(AttributeData attrData)
private static IEnumerable<object?> BuildArgumentValues(IEnumerable<TypedConstant> values)
{
return attrData.ConstructorArguments
.Select(arg =>
{
if (arg.Value == null)
return null;
// box enum values to resolve correct ctor
if (arg.Type is not INamedTypeSymbol namedType || namedType.EnumUnderlyingType == null)
return arg.Value;
var assemblyName = arg.Type!.ContainingAssembly.Name;
var qualifiedTypeName = Assembly.CreateQualifiedName(assemblyName, arg.Type.ToDisplayString());
return Type.GetType(qualifiedTypeName) is { } type
? Enum.ToObject(type, arg.Value)
: arg.Value;
})
.ToArray();
return values.Select(arg => arg.Kind switch
{
_ when arg.IsNull => null,
TypedConstantKind.Enum => GetEnumValue(arg),
TypedConstantKind.Array => BuildArrayValue(arg),
TypedConstantKind.Primitive => arg.Value,
_ => throw new ArgumentOutOfRangeException(
$"{nameof(AttributeDataAccessor)} does not support constructor arguments of kind {arg.Kind.ToString()}"),
});
}

private static object?[] BuildArrayValue(TypedConstant arg)
{
var arrayTypeSymbol = arg.Type as IArrayTypeSymbol
?? throw new InvalidOperationException("Array typed constant is not of type " + nameof(IArrayTypeSymbol));

var elementType = GetReflectionType(arrayTypeSymbol.ElementType);

var values = BuildArgumentValues(arg.Values).ToArray();
var typedValues = Array.CreateInstance(elementType, values.Length);
Array.Copy(values, typedValues, values.Length);
return (object?[])typedValues;
}

private static object? GetEnumValue(TypedConstant arg)
{
var enumType = GetReflectionType(arg.Type ?? throw new InvalidOperationException("Type is null"));
return arg.Value == null
? null
: Enum.ToObject(enumType, arg.Value);
}

private static Type GetReflectionType(ITypeSymbol type)
{
// other special types not yet supported since they are not used yet.
if (type.SpecialType == SpecialType.System_String)
return typeof(string);

var assemblyName = type.ContainingAssembly.Name;
var qualifiedTypeName = Assembly.CreateQualifiedName(assemblyName, type.ToDisplayString());
return Type.GetType(qualifiedTypeName)
?? throw new InvalidOperationException($"Type {qualifiedTypeName} not found");
}

}
2 changes: 1 addition & 1 deletion src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
Expand Up @@ -2,7 +2,7 @@
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Configuration;
using Riok.Mapperly.Descriptors.MappingBuilder;
using Riok.Mapperly.Descriptors.TypeMappings;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Descriptors;
Expand Down
2 changes: 1 addition & 1 deletion src/Riok.Mapperly/Descriptors/MapperDescriptor.cs
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Descriptors.TypeMappings;
using Riok.Mapperly.Descriptors.Mappings;

namespace Riok.Mapperly.Descriptors;

Expand Down
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Descriptors.TypeMappings;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Descriptors.MappingBuilder;
Expand Down
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Descriptors.TypeMappings;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;

Expand Down
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Descriptors.TypeMappings;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Descriptors.MappingBuilder;
Expand Down
@@ -1,6 +1,6 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.TypeMappings;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;

Expand Down
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Descriptors.TypeMappings;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Descriptors.MappingBuilder;
Expand Down
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Descriptors.TypeMappings;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;

Expand Down
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis.CSharp;
using Riok.Mapperly.Descriptors.TypeMappings;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Descriptors.MappingBuilder;
Expand Down
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis.CSharp;
using Riok.Mapperly.Descriptors.TypeMappings;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Descriptors.MappingBuilder;
Expand Down
@@ -1,5 +1,5 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Descriptors.TypeMappings;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Diagnostics;
using Riok.Mapperly.Helpers;

Expand Down

0 comments on commit 9f74d8d

Please sign in to comment.