Skip to content

Commit

Permalink
feat: add default mapper attribute for assemblies
Browse files Browse the repository at this point in the history
  • Loading branch information
ni507 committed Aug 18, 2023
1 parent a5f6aef commit 099d7a8
Show file tree
Hide file tree
Showing 17 changed files with 476 additions and 96 deletions.
4 changes: 4 additions & 0 deletions docs/docs/configuration/mapper.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,7 @@ dotnet_diagnostic.RMG020.severity = error # Unmapped source member
### Strict enum mappings

To enforce strict enum mappings set `RMG037` and `RMG038` to error, see [strict enum mappings](./enum.mdx).

## Default Mapper configuration

The `DefaultMapperAttribute` allows to set default configurations applied to all mappers on the assembly level.
7 changes: 7 additions & 0 deletions src/Riok.Mapperly.Abstractions/DefaultMapperAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Riok.Mapperly.Abstractions;

/// <summary>
/// Used to set default mapper values in the assembly.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly)]
public sealed class DefaultMapperAttribute : MapperAttribute { }
2 changes: 1 addition & 1 deletion src/Riok.Mapperly.Abstractions/MapperAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Riok.Mapperly.Abstractions;
/// Marks an abstract class or an interface as a mapper.
/// </summary>
[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class)]
public sealed class MapperAttribute : Attribute
public class MapperAttribute : Attribute
{
/// <summary>
/// Strategy on how to match mapping property names.
Expand Down
2 changes: 2 additions & 0 deletions src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Riok.Mapperly.Abstractions.MapperAttribute.ThrowOnPropertyMappingNullMismatch.ge
Riok.Mapperly.Abstractions.MapperAttribute.ThrowOnPropertyMappingNullMismatch.set -> void
Riok.Mapperly.Abstractions.MapperAttribute.UseDeepCloning.get -> bool
Riok.Mapperly.Abstractions.MapperAttribute.UseDeepCloning.set -> void
Riok.Mapperly.Abstractions.DefaultMapperAttribute
Riok.Mapperly.Abstractions.DefaultMapperAttribute.DefaultMapperAttribute() -> void
Riok.Mapperly.Abstractions.MapperConstructorAttribute
Riok.Mapperly.Abstractions.MapperConstructorAttribute.MapperConstructorAttribute() -> void
Riok.Mapperly.Abstractions.MapperIgnoreObsoleteMembersAttribute
Expand Down
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.1

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
RMG048 | Mapper | Error | Cannot use multiple attributes in the same assembly
48 changes: 41 additions & 7 deletions src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@ public AttributeDataAccessor(SymbolAccessor symbolAccessor)
_symbolAccessor = symbolAccessor;
}

public T AccessSingle<T>(ISymbol symbol)
where T : Attribute => Access<T, T>(symbol).Single();
public TData AccessSingle<TAttribute, TData>(ISymbol symbol)
where TAttribute : Attribute
where TData : notnull => Access<TAttribute, TData>(symbol).Single();

public TData? AccessAssemblyFirstOrDefault<TAttribute, TData>()
where TAttribute : Attribute
where TData : notnull => AccessAssembly<TAttribute, TData>().FirstOrDefault();

public TData? AccessFirstOrDefault<TAttribute, TData>(ISymbol symbol)
where TAttribute : Attribute
Expand All @@ -44,12 +49,35 @@ public IEnumerable<TAttribute> Access<TAttribute>(ISymbol symbol)
public IEnumerable<TData> Access<TAttribute, TData>(ISymbol symbol)
where TAttribute : Attribute
where TData : notnull
{
var attrDatas = _symbolAccessor.GetAttributes<TAttribute>(symbol);
return Access<TAttribute, TData>(attrDatas);
}

/// <summary>
/// Reads the assembly attribute data and sets it on a newly created instance of <see cref="TData"/>.
/// If <see cref="TAttribute"/> has n type parameters,
/// <see cref="TData"/> needs to have exactly the same constructors as <see cref="TAttribute"/> with additional type arguments.
/// </summary>
/// <typeparam name="TAttribute">The type of the attribute.</typeparam>
/// <typeparam name="TData">The type of the data class. If no type parameters are involved, this is usually the same as <see cref="TAttribute"/>.</typeparam>
/// <returns>The attribute data.</returns>
/// <exception cref="InvalidOperationException">If a property or ctor argument of <see cref="TData"/> could not be read on the attribute.</exception>
public IEnumerable<TData> AccessAssembly<TAttribute, TData>()
where TAttribute : Attribute
where TData : notnull
{
var attrDatas = _symbolAccessor.GetAssemblyAttributes<TAttribute>();
return Access<TAttribute, TData>(attrDatas);
}

private IEnumerable<TData> Access<TAttribute, TData>(IEnumerable<AttributeData> attrDatas)
where TAttribute : Attribute
where TData : notnull
{
var attrType = typeof(TAttribute);
var dataType = typeof(TData);

var attrDatas = _symbolAccessor.GetAttributes<TAttribute>(symbol);

foreach (var attrData in attrDatas)
{
var syntax = (AttributeSyntax?)attrData.ApplicationSyntaxReference?.GetSyntax();
Expand Down Expand Up @@ -176,8 +204,14 @@ is InvocationExpressionSyntax
return null;

var enumRoslynType = arg.Type ?? throw new InvalidOperationException("Type is null");
return targetType == typeof(IFieldSymbol)
? enumRoslynType.GetFields().First(f => Equals(f.ConstantValue, arg.Value))
: Enum.ToObject(targetType, arg.Value);
if (targetType == typeof(IFieldSymbol))
return enumRoslynType.GetFields().First(f => Equals(f.ConstantValue, arg.Value));

if (targetType.IsConstructedGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
targetType = Nullable.GetUnderlyingType(targetType)!;
}

return Enum.ToObject(targetType, arg.Value);
}
}
154 changes: 69 additions & 85 deletions src/Riok.Mapperly/Configuration/MapperConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,101 +1,85 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Configuration;

public class MapperConfiguration
{
private readonly MappingConfiguration _defaultConfiguration;
private readonly AttributeDataAccessor _dataAccessor;
/// <summary>
/// Strategy on how to match mapping property names.
/// </summary>
public PropertyNameMappingStrategy? PropertyNameMappingStrategy { get; set; }

public MapperConfiguration(SymbolAccessor symbolAccessor, ISymbol mapperSymbol)
{
_dataAccessor = new AttributeDataAccessor(symbolAccessor);
Mapper = _dataAccessor.AccessSingle<MapperAttribute>(mapperSymbol);
_defaultConfiguration = new MappingConfiguration(
new EnumMappingConfiguration(
Mapper.EnumMappingStrategy,
Mapper.EnumMappingIgnoreCase,
null,
Array.Empty<IFieldSymbol>(),
Array.Empty<IFieldSymbol>(),
Array.Empty<EnumValueMappingConfiguration>()
),
new PropertiesMappingConfiguration(
Array.Empty<string>(),
Array.Empty<string>(),
Array.Empty<PropertyMappingConfiguration>(),
Mapper.IgnoreObsoleteMembersStrategy
),
Array.Empty<DerivedTypeMappingConfiguration>()
);
}
/// <summary>
/// The default enum mapping strategy.
/// Can be overwritten on specific enums via mapping method configurations.
/// </summary>
public EnumMappingStrategy? EnumMappingStrategy { get; set; }

public MapperAttribute Mapper { get; }
/// <summary>
/// Whether the case should be ignored for enum mappings.
/// </summary>
public bool? EnumMappingIgnoreCase { get; set; }

public MappingConfiguration BuildFor(MappingConfigurationReference reference)
{
if (reference.Method == null)
return _defaultConfiguration;
/// <summary>
/// Specifies the behaviour in the case when the mapper tries to return <c>null</c> in a mapping method with a non-nullable return type.
/// If set to <c>true</c> an <see cref="ArgumentNullException"/> is thrown.
/// If set to <c>false</c> the mapper tries to return a default value.
/// For a <see cref="string"/> this is <see cref="string.Empty"/>,
/// for value types <c>default</c>
/// and for reference types <c>new()</c> if a parameterless constructor exists or else an <see cref="ArgumentNullException"/> is thrown.
/// </summary>
public bool? ThrowOnMappingNullMismatch { get; set; }

var enumConfig = BuildEnumConfig(reference);
var propertiesConfig = BuildPropertiesConfig(reference.Method);
var derivedTypesConfig = BuildDerivedTypeConfigs(reference.Method);
return new MappingConfiguration(enumConfig, propertiesConfig, derivedTypesConfig);
}
/// <summary>
/// Specifies the behaviour in the case when the mapper tries to set a non-nullable property to a <c>null</c> value.
/// If set to <c>true</c> an <see cref="ArgumentNullException"/> is thrown.
/// If set to <c>false</c> the property assignment is ignored.
/// This is ignored for required init properties and <see cref="IQueryable{T}"/> projection mappings.
/// </summary>
public bool? ThrowOnPropertyMappingNullMismatch { get; set; }

private IReadOnlyCollection<DerivedTypeMappingConfiguration> BuildDerivedTypeConfigs(IMethodSymbol method)
{
return _dataAccessor
.Access<MapDerivedTypeAttribute, DerivedTypeMappingConfiguration>(method)
.Concat(_dataAccessor.Access<MapDerivedTypeAttribute<object, object>, DerivedTypeMappingConfiguration>(method))
.ToList();
}
/// <summary>
/// Specifies whether <c>null</c> values are assigned to the target.
/// If <c>true</c> (default), the source is <c>null</c>, and the target does allow <c>null</c> values,
/// <c>null</c> is assigned.
/// If <c>false</c>, <c>null</c> values are never assigned to the target property.
/// This is ignored for required init properties and <see cref="IQueryable{T}"/> projection mappings.
/// </summary>
public bool? AllowNullPropertyAssignment { get; set; }

private PropertiesMappingConfiguration BuildPropertiesConfig(IMethodSymbol method)
{
var ignoredSourceProperties = _dataAccessor
.Access<MapperIgnoreSourceAttribute>(method)
.Select(x => x.Source)
.WhereNotNull()
.ToList();
var ignoredTargetProperties = _dataAccessor
.Access<MapperIgnoreTargetAttribute>(method)
.Select(x => x.Target)
.WhereNotNull()
.ToList();
var explicitMappings = _dataAccessor.Access<MapPropertyAttribute, PropertyMappingConfiguration>(method).ToList();
var ignoreObsolete = _dataAccessor.Access<MapperIgnoreObsoleteMembersAttribute>(method).FirstOrDefault() is not { } methodIgnore
? _defaultConfiguration.Properties.IgnoreObsoleteMembersStrategy
: methodIgnore.IgnoreObsoleteStrategy;
/// <summary>
/// Whether to always deep copy objects.
/// Eg. when the type <c>Person[]</c> should be mapped to the same type <c>Person[]</c>,
/// when <c>false</c>, the same array is reused.
/// when <c>true</c>, the array and each person is cloned.
/// </summary>
public bool? UseDeepCloning { get; set; }

return new PropertiesMappingConfiguration(ignoredSourceProperties, ignoredTargetProperties, explicitMappings, ignoreObsolete);
}
/// <summary>
/// Enabled conversions which Mapperly automatically implements.
/// By default all supported type conversions are enabled.
/// <example>
/// Eg. to disable all automatically implemented conversions:<br />
/// <c>EnabledConversions = MappingConversionType.None</c>
/// </example>
/// <example>
/// Eg. to disable <c>ToString()</c> method calls:<br />
/// <c>EnabledConversions = MappingConversionType.All &amp; ~MappingConversionType.ToStringMethod</c>
/// </example>
/// </summary>
public MappingConversionType? EnabledConversions { get; set; }

private EnumMappingConfiguration BuildEnumConfig(MappingConfigurationReference configRef)
{
if (configRef.Method == null || !configRef.Source.IsEnum() && !configRef.Target.IsEnum())
return _defaultConfiguration.Enum;
/// <summary>
/// Enables the reference handling feature.
/// Disabled by default for performance reasons.
/// When enabled, an <see cref="IReferenceHandler"/> instance is passed through the mapping methods
/// to keep track of and reuse existing target object instances.
/// </summary>
public bool? UseReferenceHandling { get; set; }

var configData = _dataAccessor.AccessFirstOrDefault<MapEnumAttribute, EnumConfiguration>(configRef.Method);
var explicitMappings = _dataAccessor.Access<MapEnumValueAttribute, EnumValueMappingConfiguration>(configRef.Method).ToList();
var ignoredSources = _dataAccessor
.Access<MapperIgnoreSourceValueAttribute, MapperIgnoreEnumValueConfiguration>(configRef.Method)
.Select(x => x.Value)
.ToList();
var ignoredTargets = _dataAccessor
.Access<MapperIgnoreTargetValueAttribute, MapperIgnoreEnumValueConfiguration>(configRef.Method)
.Select(x => x.Value)
.ToList();
return new EnumMappingConfiguration(
configData?.Strategy ?? _defaultConfiguration.Enum.Strategy,
configData?.IgnoreCase ?? _defaultConfiguration.Enum.IgnoreCase,
configData?.FallbackValue,
ignoredSources,
ignoredTargets,
explicitMappings
);
}
/// <summary>
/// The ignore obsolete attribute strategy. Determines how <see cref="ObsoleteAttribute"/> marked members are mapped.
/// Defaults to <see cref="IgnoreObsoleteMembersStrategy.None"/>.
/// </summary>
public IgnoreObsoleteMembersStrategy? IgnoreObsoleteMembersStrategy { get; set; }
}
104 changes: 104 additions & 0 deletions src/Riok.Mapperly/Configuration/MergedMapperConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Configuration;

public class MergedMapperConfiguration
{
private readonly MappingConfiguration _defaultConfiguration;
private readonly AttributeDataAccessor _dataAccessor;

public MergedMapperConfiguration(SymbolAccessor symbolAccessor, ISymbol mapperSymbol)
{
_dataAccessor = new AttributeDataAccessor(symbolAccessor);
var mapperConfiguration = _dataAccessor.AccessSingle<MapperAttribute, MapperConfiguration>(mapperSymbol);
var defaultMapperConfiguration = _dataAccessor.AccessAssemblyFirstOrDefault<DefaultMapperAttribute, MapperConfiguration>();
Mapper = MapperConfigurationBuilder.Merge(mapperConfiguration, defaultMapperConfiguration);

_defaultConfiguration = new MappingConfiguration(
new EnumMappingConfiguration(
Mapper.EnumMappingStrategy,
Mapper.EnumMappingIgnoreCase,
null,
Array.Empty<IFieldSymbol>(),
Array.Empty<IFieldSymbol>(),
Array.Empty<EnumValueMappingConfiguration>()
),
new PropertiesMappingConfiguration(
Array.Empty<string>(),
Array.Empty<string>(),
Array.Empty<PropertyMappingConfiguration>(),
Mapper.IgnoreObsoleteMembersStrategy
),
Array.Empty<DerivedTypeMappingConfiguration>()
);
}

public MapperAttribute Mapper { get; }

public MappingConfiguration BuildFor(MappingConfigurationReference reference)
{
if (reference.Method == null)
return _defaultConfiguration;

var enumConfig = BuildEnumConfig(reference);
var propertiesConfig = BuildPropertiesConfig(reference.Method);
var derivedTypesConfig = BuildDerivedTypeConfigs(reference.Method);
return new MappingConfiguration(enumConfig, propertiesConfig, derivedTypesConfig);
}

private IReadOnlyCollection<DerivedTypeMappingConfiguration> BuildDerivedTypeConfigs(IMethodSymbol method)
{
return _dataAccessor
.Access<MapDerivedTypeAttribute, DerivedTypeMappingConfiguration>(method)
.Concat(_dataAccessor.Access<MapDerivedTypeAttribute<object, object>, DerivedTypeMappingConfiguration>(method))
.ToList();
}

private PropertiesMappingConfiguration BuildPropertiesConfig(IMethodSymbol method)
{
var ignoredSourceProperties = _dataAccessor
.Access<MapperIgnoreSourceAttribute>(method)
.Select(x => x.Source)
.WhereNotNull()
.ToList();
var ignoredTargetProperties = _dataAccessor
.Access<MapperIgnoreTargetAttribute>(method)
.Select(x => x.Target)
.WhereNotNull()
.ToList();
var explicitMappings = _dataAccessor.Access<MapPropertyAttribute, PropertyMappingConfiguration>(method).ToList();
var ignoreObsolete = _dataAccessor.Access<MapperIgnoreObsoleteMembersAttribute>(method).FirstOrDefault() is not { } methodIgnore
? _defaultConfiguration.Properties.IgnoreObsoleteMembersStrategy
: methodIgnore.IgnoreObsoleteStrategy;

return new PropertiesMappingConfiguration(ignoredSourceProperties, ignoredTargetProperties, explicitMappings, ignoreObsolete);
}

private EnumMappingConfiguration BuildEnumConfig(MappingConfigurationReference configRef)
{
if (configRef.Method == null || !configRef.Source.IsEnum() && !configRef.Target.IsEnum())
return _defaultConfiguration.Enum;

var configData = _dataAccessor.AccessFirstOrDefault<MapEnumAttribute, EnumConfiguration>(configRef.Method);
var explicitMappings = _dataAccessor.Access<MapEnumValueAttribute, EnumValueMappingConfiguration>(configRef.Method).ToList();
var ignoredSources = _dataAccessor
.Access<MapperIgnoreSourceValueAttribute, MapperIgnoreEnumValueConfiguration>(configRef.Method)
.Select(x => x.Value)
.ToList();
var ignoredTargets = _dataAccessor
.Access<MapperIgnoreTargetValueAttribute, MapperIgnoreEnumValueConfiguration>(configRef.Method)
.Select(x => x.Value)
.ToList();
return new EnumMappingConfiguration(
configData?.Strategy ?? _defaultConfiguration.Enum.Strategy,
configData?.IgnoreCase ?? _defaultConfiguration.Enum.IgnoreCase,
configData?.FallbackValue,
ignoredSources,
ignoredTargets,
explicitMappings
);
}
}
Loading

0 comments on commit 099d7a8

Please sign in to comment.