Skip to content

Commit

Permalink
feat: Add option to specify the string format of a property (#921)
Browse files Browse the repository at this point in the history
  • Loading branch information
latonz authored Nov 21, 2023
1 parent 1f6f1f5 commit 236a46f
Show file tree
Hide file tree
Showing 33 changed files with 516 additions and 196 deletions.
18 changes: 17 additions & 1 deletion docs/docs/configuration/mapper.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ the `RequiredMappingStrategy` can be used.
// highlight-start
[MapperRequiredMapping(RequiredMappingStrategy.Source)]
// highlight-end
public partial CarDto MapMake(Car make);
public partial CarDto MapCar(Car car);
}
```

Expand All @@ -227,6 +227,22 @@ the `RequiredMappingStrategy` can be used.

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

### String format

The string format passed to `ToString` calls when converting to a string can be customized
by using the `StringFormat` property of the `MapPropertyAttribute`.

```csharp
[Mapper]
public partial class CarMapper
{
// highlight-start
[MapProperty(nameof(Car.Price), nameof(CarDto.Price), StringFormat = "C")]
// highlight-end
public partial CarDto MapCar(Car car);
}
```

## Default Mapper configuration

The `MapperDefaultsAttribute` allows to set default configurations applied to all mappers on the assembly level.
Expand Down
5 changes: 5 additions & 0 deletions src/Riok.Mapperly.Abstractions/MapPropertyAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ public MapPropertyAttribute(string[] source, string[] target)
/// </summary>
public IReadOnlyCollection<string> Target { get; }

/// <summary>
/// Gets or sets the format of the <c>ToString</c> conversion.
/// </summary>
public string? StringFormat { get; set; }

/// <summary>
/// Gets the full name of the target property path.
/// </summary>
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 @@ -55,6 +55,8 @@ Riok.Mapperly.Abstractions.MapPropertyAttribute.Source.get -> System.Collections
Riok.Mapperly.Abstractions.MapPropertyAttribute.SourceFullName.get -> string!
Riok.Mapperly.Abstractions.MapPropertyAttribute.Target.get -> System.Collections.Generic.IReadOnlyCollection<string!>!
Riok.Mapperly.Abstractions.MapPropertyAttribute.TargetFullName.get -> string!
Riok.Mapperly.Abstractions.MapPropertyAttribute.StringFormat.get -> string?
Riok.Mapperly.Abstractions.MapPropertyAttribute.StringFormat.set -> void
Riok.Mapperly.Abstractions.ObjectFactoryAttribute
Riok.Mapperly.Abstractions.ObjectFactoryAttribute.ObjectFactoryAttribute() -> void
Riok.Mapperly.Abstractions.PropertyNameMappingStrategy
Expand Down
1 change: 1 addition & 0 deletions src/Riok.Mapperly/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ RMG051 | Mapper | Warning | Invalid ignore source member found, nested ignor
RMG052 | Mapper | Warning | Invalid ignore target member found, nested ignores are not supported
RMG053 | Mapper | Error | The flag MemberVisibility.Accessible cannot be disabled, this feature requires .NET 8.0 or greater
RMG054 | Mapper | Error | Mapper class containing 'static partial' method must not have any instance methods
RMG055 | Mapper | Error | The source type does not implement IFormattable, string format cannot be applied

### Removed Rules
Rule ID | Category | Severity | Notes
Expand Down
4 changes: 2 additions & 2 deletions src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ private PropertiesMappingConfiguration BuildPropertiesConfig(IMethodSymbol metho
.Select(x => x.Target)
.WhereNotNull()
.ToList();
var explicitMappings = _dataAccessor.Access<MapPropertyAttribute, PropertyMappingConfiguration>(method).ToList();
var propertyConfigurations = _dataAccessor.Access<MapPropertyAttribute, PropertyMappingConfiguration>(method).ToList();
var ignoreObsolete = _dataAccessor.Access<MapperIgnoreObsoleteMembersAttribute>(method).FirstOrDefault() is not { } methodIgnore
? _defaultConfiguration.Properties.IgnoreObsoleteMembersStrategy
: methodIgnore.IgnoreObsoleteStrategy;
Expand All @@ -83,7 +83,7 @@ private PropertiesMappingConfiguration BuildPropertiesConfig(IMethodSymbol metho
return new PropertiesMappingConfiguration(
ignoredSourceProperties,
ignoredTargetProperties,
explicitMappings,
propertyConfigurations,
ignoreObsolete,
requiredMapping
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
using Riok.Mapperly.Descriptors;

namespace Riok.Mapperly.Configuration;

public record PropertyMappingConfiguration(StringMemberPath Source, StringMemberPath Target);
public record PropertyMappingConfiguration(StringMemberPath Source, StringMemberPath Target)
{
public string? StringFormat { get; set; }

public TypeMappingConfiguration ToTypeMappingConfiguration() => new(StringFormat);
}
7 changes: 3 additions & 4 deletions src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ private void ExtractUserMappings()
firstNonStaticUserMapping = userMapping.Method;
}

_mappings.Add(userMapping);
_mappings.Add(userMapping, TypeMappingConfiguration.Default);
}

if (_mapperDescriptor.Static && firstNonStaticUserMapping is not null)
Expand All @@ -152,8 +152,7 @@ private void EnqueueUserMappings(ObjectFactoryCollection objectFactories)
_builderContext,
objectFactories,
userMapping.Method,
userMapping.SourceType,
userMapping.TargetType
new TypeMappingKey(userMapping.SourceType, userMapping.TargetType)
);

_mappings.EnqueueToBuildBody(userMapping, ctx);
Expand All @@ -164,7 +163,7 @@ private void ExtractExternalMappings()
{
foreach (var externalMapping in ExternalMappingsExtractor.ExtractExternalMappings(_builderContext, _mapperDescriptor.Symbol))
{
_mappings.Add(externalMapping);
_mappings.Add(externalMapping, TypeMappingConfiguration.Default);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Descriptors.Mappings.ExistingTarget;
using Riok.Mapperly.Descriptors.Mappings.UserMappings;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Descriptors;

Expand All @@ -16,16 +15,11 @@ public class InlineExpressionMappingBuilderContext : MappingBuilderContext
private readonly MappingCollection _inlineExpressionMappings;
private readonly MappingBuilderContext _parentContext;

public InlineExpressionMappingBuilderContext(MappingBuilderContext ctx, ITypeSymbol sourceType, ITypeSymbol targetType)
: this(ctx, (ctx.FindMapping(sourceType, targetType) as IUserMapping)?.Method, sourceType, targetType) { }
public InlineExpressionMappingBuilderContext(MappingBuilderContext ctx, TypeMappingKey mappingKey)
: this(ctx, (ctx.FindMapping(mappingKey) as IUserMapping)?.Method, mappingKey) { }

private InlineExpressionMappingBuilderContext(
MappingBuilderContext ctx,
IMethodSymbol? userSymbol,
ITypeSymbol source,
ITypeSymbol target
)
: base(ctx, userSymbol, source, target, false)
private InlineExpressionMappingBuilderContext(MappingBuilderContext ctx, IMethodSymbol? userSymbol, TypeMappingKey mappingKey)
: base(ctx, userSymbol, mappingKey, false)
{
_parentContext = ctx;
_inlineExpressionMappings = new MappingCollection();
Expand All @@ -34,11 +28,10 @@ ITypeSymbol target
private InlineExpressionMappingBuilderContext(
InlineExpressionMappingBuilderContext ctx,
IMethodSymbol? userSymbol,
ITypeSymbol source,
ITypeSymbol target,
TypeMappingKey mappingKey,
bool clearDerivedTypes
)
: base(ctx, userSymbol, source, target, clearDerivedTypes)
: base(ctx, userSymbol, mappingKey, clearDerivedTypes)
{
_parentContext = ctx;
_inlineExpressionMappings = ctx._inlineExpressionMappings;
Expand All @@ -55,25 +48,24 @@ conversionType is not MappingConversionType.EnumToString and not MappingConversi
&& base.IsConversionEnabled(conversionType);

/// <summary>
/// Tries to find an existing mapping for the provided types.
/// Tries to find an existing mapping for the provided types + config.
/// The nullable annotation of reference types is ignored and always set to non-nullable.
/// Only inline expression mappings and user implemented mappings are considered.
/// </summary>
/// <param name="sourceType">The source type.</param>
/// <param name="targetType">The target type.</param>
/// <param name="mappingKey">The mapping key.</param>
/// <returns>The <see cref="INewInstanceMapping"/> if a mapping was found or <c>null</c> if none was found.</returns>
public override INewInstanceMapping? FindMapping(ITypeSymbol sourceType, ITypeSymbol targetType)
public override INewInstanceMapping? FindMapping(TypeMappingKey mappingKey)
{
if (_inlineExpressionMappings.Find(sourceType, targetType) is { } mapping)
if (_inlineExpressionMappings.Find(mappingKey) is { } mapping)
return mapping;

// User implemented mappings are also taken into account.
// This works as long as the user implemented methods
// follow the expression tree limitations:
// https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/expression-trees/#limitations
if (_parentContext.FindMapping(sourceType, targetType) is UserImplementedMethodMapping userMapping)
if (_parentContext.FindMapping(mappingKey) is UserImplementedMethodMapping userMapping)
{
_inlineExpressionMappings.Add(userMapping);
_inlineExpressionMappings.Add(userMapping, mappingKey.Configuration);
return userMapping;
}

Expand All @@ -88,33 +80,29 @@ conversionType is not MappingConversionType.EnumToString and not MappingConversi
/// This ensures, the configuration of the user defined method is reused.
/// <seealso cref="MappingBuilderContext.FindOrBuildMapping"/>
/// </summary>
/// <param name="sourceType">The source type.</param>
/// <param name="targetType">The target type.</param>
/// <param name="mappingKey">The mapping key.</param>
/// <param name="options">The options, <see cref="MappingBuildingOptions.MarkAsReusable"/> is ignored.</param>
/// <returns></returns>
public override INewInstanceMapping? FindOrBuildMapping(
ITypeSymbol sourceType,
ITypeSymbol targetType,
TypeMappingKey mappingKey,
MappingBuildingOptions options = MappingBuildingOptions.Default
)
{
sourceType = sourceType.UpgradeNullable();
targetType = targetType.UpgradeNullable();
var mapping = FindMapping(sourceType, targetType);
var mapping = FindMapping(mappingKey);
if (mapping != null)
return mapping;

var userSymbol = options.HasFlag(MappingBuildingOptions.KeepUserSymbol) ? UserSymbol : null;

userSymbol ??= (MappingBuilder.Find(sourceType, targetType) as IUserMapping)?.Method;
userSymbol ??= (MappingBuilder.Find(mappingKey) as IUserMapping)?.Method;

// unset MarkAsReusable and KeepUserSymbol as they have special handling for inline mappings
options &= ~(MappingBuildingOptions.MarkAsReusable | MappingBuildingOptions.KeepUserSymbol);

mapping = BuildMapping(userSymbol, sourceType, targetType, options);
mapping = BuildMapping(userSymbol, mappingKey, options);
if (mapping != null)
{
_inlineExpressionMappings.Add(mapping);
_inlineExpressionMappings.Add(mapping, mappingKey.Configuration);
}

return mapping;
Expand All @@ -123,26 +111,22 @@ conversionType is not MappingConversionType.EnumToString and not MappingConversi
/// <summary>
/// Existing target instance mappings are not supported.
/// </summary>
/// <param name="sourceType">The source type, ignored.</param>
/// <param name="targetType">The target type, ignored.</param>
/// <param name="mappingKey">The mapping key, ignored.</param>
/// <param name="options">The options to build a new mapping, ignored.</param>
/// <returns><c>null</c></returns>
public override IExistingTargetMapping? FindOrBuildExistingTargetMapping(
ITypeSymbol sourceType,
ITypeSymbol targetType,
TypeMappingKey mappingKey,
MappingBuildingOptions options = MappingBuildingOptions.Default
) => null;

/// <summary>
/// Existing target instance mappings are not supported.
/// </summary>
/// <param name="sourceType">The source type, ignored.</param>
/// <param name="targetType">The target type, ignored.</param>
/// <param name="mappingKey">The mapping key, ignored.</param>
/// <param name="options">The options to build a new mapping, ignored.</param>
/// <returns><c>null</c></returns>
public override IExistingTargetMapping? BuildExistingTargetMapping(
ITypeSymbol sourceType,
ITypeSymbol targetType,
TypeMappingKey mappingKey,
MappingBuildingOptions options = MappingBuildingOptions.Default
) => null;

Expand All @@ -151,15 +135,15 @@ protected override NullFallbackValue GetNullFallbackValue(ITypeSymbol targetType

protected override MappingBuilderContext ContextForMapping(
IMethodSymbol? userSymbol,
ITypeSymbol sourceType,
ITypeSymbol targetType,
TypeMappingKey mappingKey,
MappingBuildingOptions options
) =>
new InlineExpressionMappingBuilderContext(
)
{
return new InlineExpressionMappingBuilderContext(
this,
userSymbol,
sourceType,
targetType,
mappingKey,
options.HasFlag(MappingBuildingOptions.ClearDerivedTypes)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,22 +132,22 @@ IReadOnlyCollection<PropertyMappingConfiguration> memberConfigs
return;
}

BuildInitMemberMapping(ctx, targetMember, sourceMemberPath);
BuildInitMemberMapping(ctx, targetMember, sourceMemberPath, memberConfig);
}

private static void BuildInitMemberMapping(
INewInstanceBuilderContext<IMapping> ctx,
IMappableMember targetMember,
MemberPath sourcePath
MemberPath sourcePath,
PropertyMappingConfiguration? memberConfig = null
)
{
var targetPath = new MemberPath(new[] { targetMember });
if (!ObjectMemberMappingBodyBuilder.ValidateMappingSpecification(ctx, sourcePath, targetPath, true))
return;

var delegateMapping =
ctx.BuilderContext.FindMapping(sourcePath.MemberType, targetMember.Type)
?? ctx.BuilderContext.FindOrBuildMapping(sourcePath.MemberType.NonNullable(), targetMember.Type.NonNullable());
var mappingKey = new TypeMappingKey(sourcePath.MemberType, targetMember.Type, memberConfig?.ToTypeMappingConfiguration());
var delegateMapping = ctx.BuilderContext.FindOrBuildLooseNullableMapping(mappingKey);

if (delegateMapping == null)
{
Expand Down Expand Up @@ -254,7 +254,7 @@ private static bool TryBuildConstructorMapping(
var skippedOptionalParam = false;
foreach (var parameter in ctor.Parameters)
{
if (!TryFindConstructorParameterSourcePath(ctx, parameter, out var sourcePath))
if (!TryFindConstructorParameterSourcePath(ctx, parameter, out var sourcePath, out var memberConfig))
{
// expressions do not allow skipping of optional parameters
if (!parameter.IsOptional || ctx.BuilderContext.IsExpression)
Expand All @@ -266,9 +266,8 @@ private static bool TryBuildConstructorMapping(

// nullability is handled inside the member mapping
var paramType = parameter.Type.WithNullableAnnotation(parameter.NullableAnnotation);
var delegateMapping =
ctx.BuilderContext.FindMapping(sourcePath.MemberType, paramType)
?? ctx.BuilderContext.FindOrBuildMapping(sourcePath.Member.Type.NonNullable(), paramType.NonNullable());
var typeMapping = new TypeMappingKey(sourcePath.MemberType, paramType, memberConfig?.ToTypeMappingConfiguration());
var delegateMapping = ctx.BuilderContext.FindOrBuildLooseNullableMapping(typeMapping);

if (delegateMapping == null)
{
Expand Down Expand Up @@ -311,10 +310,12 @@ private static bool TryBuildConstructorMapping(
private static bool TryFindConstructorParameterSourcePath(
INewInstanceBuilderContext<IMapping> ctx,
IParameterSymbol parameter,
[NotNullWhen(true)] out MemberPath? sourcePath
[NotNullWhen(true)] out MemberPath? sourcePath,
out PropertyMappingConfiguration? memberConfig
)
{
sourcePath = null;
memberConfig = null;

if (!ctx.MemberConfigsByRootTargetName.TryGetValue(parameter.Name, out var memberConfigs))
{
Expand All @@ -338,7 +339,7 @@ out sourcePath
);
}

var memberConfig = memberConfigs.First();
memberConfig = memberConfigs.First();
if (memberConfig.Target.Path.Count > 1)
{
ctx.BuilderContext.ReportDiagnostic(
Expand Down
Loading

0 comments on commit 236a46f

Please sign in to comment.