Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add option to specify the string format of a property #921

Merged
merged 1 commit into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
/// </summary>
public IReadOnlyCollection<string> Target { get; }

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

Check warning on line 49 in src/Riok.Mapperly.Abstractions/MapPropertyAttribute.cs

View check run for this annotation

Codecov / codecov/patch

src/Riok.Mapperly.Abstractions/MapPropertyAttribute.cs#L49

Added line #L49 was not covered by tests

/// <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