Skip to content

Commit

Permalink
feat: intorduce MapValueAttribute to map constant values and method p…
Browse files Browse the repository at this point in the history
…rovided values
  • Loading branch information
latonz committed Jun 19, 2024
1 parent b65e2a8 commit 94d29fc
Show file tree
Hide file tree
Showing 57 changed files with 1,796 additions and 53 deletions.
2 changes: 1 addition & 1 deletion docs/docs/configuration/before-after-map.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 4
sidebar_position: 5
description: Run custom logic before or after the generated mapping.
---

Expand Down
48 changes: 48 additions & 0 deletions docs/docs/configuration/constant-generated-values.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
sidebar_position: 4
description: Map constant and generated values
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Constant and generated values

## Constant values

To map a constant value to a member or a constructor parameter `MapValue` can be used.
Make sure the value exactly matches the target type.

<Tabs>
<TabItem value="declaration" label="Declaration" default>
```csharp [MapValue(nameof(CarDto.SourceSystem), "C1")] partial CarDto
Map(Car car); ```
</TabItem>
<TabItem value="generated" label="Generated code" default>
```csharp target.SourceSystem = "C1"; ```
</TabItem>
</Tabs>

## Method generated values

To map a method generated value to a member or a constructor parameter `MapValue` can be used.
Make sure the return type exactly matches the target type.

<Tabs>
<TabItem value="declaration" label="Declaration" default>
```csharp
[MapValue(nameof(CarDto.SourceSystem), nameof(GetSourceSystem))]
partial CarDto Map(Car car);

string GetSourceSystem() => "C1";
```
</TabItem>
<TabItem value="generated" label="Generated code" default>
```csharp
target.SourceSystem = GetSourceSystem();
```
</TabItem>

</Tabs>

This also works for constructor parameters.
2 changes: 1 addition & 1 deletion docs/docs/configuration/conversions.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 14
sidebar_position: 15
description: A list of conversions supported by Mapperly
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/ctor-mappings.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 6
sidebar_position: 7
description: Constructor mappings
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/derived-type-mapping.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 9
sidebar_position: 10
description: Map derived types and interfaces
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/enum.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 3
sidebar_position: 4
description: Map enums
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/existing-target.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 8
sidebar_position: 9
description: Map to an existing target object
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/flattening.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 2
sidebar_position: 3
description: Flatten properties and fields
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/generated-source.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 16
sidebar_position: 17
description: How to inspect and check in the generated source into a version control system (VCS, GIT, ...)
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/generic-mapping.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 10
sidebar_position: 11
description: Create a generic mapping method
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/object-factories.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 7
sidebar_position: 8
description: Construct and resolve objects using object factories
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/private-member-mapping.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 12
sidebar_position: 13
description: Private member mapping
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/queryable-projections.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 13
sidebar_position: 14
description: Use queryable projections to map queryable objects and optimize ORM performance
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/reference-handling.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 11
sidebar_position: 12
description: Use reference handling to handle reference loops
---

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/user-implemented-methods.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 5
sidebar_position: 6
description: Manually implement mappings
---

Expand Down
73 changes: 73 additions & 0 deletions src/Riok.Mapperly.Abstractions/MapValueAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.Diagnostics;

namespace Riok.Mapperly.Abstractions;

/// <summary>
/// Specifies a constant value mapping.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
[Conditional("MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME")]
public sealed class MapValueAttribute : Attribute
{
private const string PropertyAccessSeparatorStr = ".";
private const char PropertyAccessSeparator = '.';

/// <summary>
/// Maps a constant value to a target member.
/// </summary>
/// <param name="target">The target member path.</param>
/// <param name="value">The value to assign to the <paramref name="target"/>, needs to be of the same type as the <paramref name="target"/>.</param>
public MapValueAttribute(string target, object? value)
: this(target.Split(PropertyAccessSeparator), value) { }

/// <summary>
/// Maps a constant value to a target member.
/// </summary>
/// <param name="target">The target member path.</param>
/// <param name="value">The value to assign to the <paramref name="target"/>, needs to be of the same type as the <paramref name="target"/>.</param>
public MapValueAttribute(string[] target, object? value)
{
Target = target;
Value = value;
}

/// <summary>
/// Maps a method generated value to a target member.
/// Requires the usage of the <see cref="Use"/> property.
/// </summary>
/// <param name="target">The target member path.</param>
public MapValueAttribute(string target)
: this(target, null) { }

/// <summary>
/// Maps a method generated value to a target member.
/// Requires the usage of the <see cref="Use"/> property.
/// </summary>
/// <param name="target">The target member path, the usage of nameof is encouraged.</param>
public MapValueAttribute(string[] target)
: this(target, null) { }

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

/// <summary>
/// Gets the full name of the target property path.
/// </summary>
public string TargetFullName => string.Join(PropertyAccessSeparatorStr, Target);

/// <summary>
/// Gets the value to be assigned to <see cref="Target"/>.
/// </summary>
public object? Value { get; }

/// <summary>
/// Gets or sets the method name of the method which generates the value to be assigned to <see cref="Target"/>.
/// Either this property or <see cref="Value"/> needs to be set.
/// The return type of the referenced method must exactly match the type of <see cref="Target"/>
/// and needs to be parameterless.
/// The usage of nameof is encouraged.
/// </summary>
public string? Use { get; set; }
}
10 changes: 10 additions & 0 deletions src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,13 @@ Riok.Mapperly.Abstractions.MapPropertyFromSourceAttribute.FormatProvider.get ->
Riok.Mapperly.Abstractions.MapPropertyFromSourceAttribute.FormatProvider.set -> void
Riok.Mapperly.Abstractions.MapPropertyFromSourceAttribute.Use.get -> string?
Riok.Mapperly.Abstractions.MapPropertyFromSourceAttribute.Use.set -> void
Riok.Mapperly.Abstractions.MapValueAttribute
Riok.Mapperly.Abstractions.MapValueAttribute.Use.get -> string?
Riok.Mapperly.Abstractions.MapValueAttribute.Use.set -> void
Riok.Mapperly.Abstractions.MapValueAttribute.MapValueAttribute(string! target) -> void
Riok.Mapperly.Abstractions.MapValueAttribute.MapValueAttribute(string! target, object? value) -> void
Riok.Mapperly.Abstractions.MapValueAttribute.MapValueAttribute(string![]! target) -> void
Riok.Mapperly.Abstractions.MapValueAttribute.MapValueAttribute(string![]! target, object? value) -> void
Riok.Mapperly.Abstractions.MapValueAttribute.Target.get -> System.Collections.Generic.IReadOnlyCollection<string!>!
Riok.Mapperly.Abstractions.MapValueAttribute.TargetFullName.get -> string!
Riok.Mapperly.Abstractions.MapValueAttribute.Value.get -> object?
6 changes: 6 additions & 0 deletions src/Riok.Mapperly/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@ RMG073 | Mapper | Warning | The target type of the referenced mapping does n
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
RMG074 | Mapper | Error | Multiple mappings are configured for the same target member
RMG075 | Mapper | Error | Invalid usage of the MapValueAttribute
RMG076 | Mapper | Warning | Cannot assign null to non-nullable member
RMG077 | Mapper | Error | Cannot assign constant value because the type of the value does not match the type of the target
RMG078 | Mapper | Error | Cannot assign method return type because the type of the value does not match the type of the target
RMG079 | Mapper | Error | The referenced method could not be found or has an unsupported signature
RMG080 | Mapper | Error | The MapValueAttribute does not support types and arrays

### Removed Rules

Expand Down
2 changes: 2 additions & 0 deletions src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ IReadOnlyList<AttributeArgumentSyntax> argumentSyntax
{
return arg.Kind switch
{
_ when (targetType == typeof(AttributeValue?) || targetType == typeof(AttributeValue)) && syntax != null
=> new AttributeValue(arg, syntax.Expression),
_ when arg.IsNull => null,
_ when targetType == typeof(StringMemberPath) => CreateMemberPath(arg, syntax),
TypedConstantKind.Enum => GetEnumValue(arg, targetType),
Expand Down
12 changes: 12 additions & 0 deletions src/Riok.Mapperly/Configuration/AttributeValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Riok.Mapperly.Configuration;

/// <summary>
/// Represents an attribute value provided by the user.
/// Allows access to the intepreted value as well as the source syntax.
/// </summary>
/// <param name="ConstantValue">The interpreted compile-time constant value.</param>
/// <param name="Expression">The syntax as written by the user.</param>
public readonly record struct AttributeValue(TypedConstant ConstantValue, ExpressionSyntax Expression);
9 changes: 8 additions & 1 deletion src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ MapperConfiguration defaultMapperConfiguration
[],
mapper.RequiredMappingStrategy
),
new MembersMappingConfiguration([], [], [], [], mapper.IgnoreObsoleteMembersStrategy, mapper.RequiredMappingStrategy),
new MembersMappingConfiguration([], [], [], [], [], mapper.IgnoreObsoleteMembersStrategy, mapper.RequiredMappingStrategy),
[]
);
}
Expand Down Expand Up @@ -75,6 +75,7 @@ private MembersMappingConfiguration BuildMembersConfig(MappingConfigurationRefer
.Select(x => x.Target)
.WhereNotNull()
.ToList();
var memberValueConfigurations = _dataAccessor.Access<MapValueAttribute, MemberValueMappingConfiguration>(configRef.Method).ToList();
var memberConfigurations = _dataAccessor
.Access<MapPropertyAttribute, MemberMappingConfiguration>(configRef.Method)
.Concat(_dataAccessor.Access<MapPropertyFromSourceAttribute, MemberMappingConfiguration>(configRef.Method))
Expand Down Expand Up @@ -106,6 +107,11 @@ private MembersMappingConfiguration BuildMembersConfig(MappingConfigurationRefer
return MapperConfiguration.Members;
}

foreach (var invalidMemberConfig in memberValueConfigurations.Where(x => !x.IsValid))
{
diagnostics.ReportDiagnostic(DiagnosticDescriptors.InvalidMapValueAttributeUsage, invalidMemberConfig.Location);
}

foreach (var invalidMemberConfig in memberConfigurations.Where(x => !x.IsValid))
{
diagnostics.ReportDiagnostic(DiagnosticDescriptors.InvalidMapPropertyAttributeUsage, invalidMemberConfig.Location);
Expand All @@ -114,6 +120,7 @@ private MembersMappingConfiguration BuildMembersConfig(MappingConfigurationRefer
return new MembersMappingConfiguration(
ignoredSourceMembers,
ignoredTargetMembers,
memberValueConfigurations,
memberConfigurations,
nestedMembersConfigurations,
ignoreObsolete ?? MapperConfiguration.Members.IgnoreObsoleteMembersStrategy,
Expand Down
25 changes: 25 additions & 0 deletions src/Riok.Mapperly/Configuration/MemberValueMappingConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Diagnostics;

namespace Riok.Mapperly.Configuration;

[DebuggerDisplay("{Target}: {DescribeValue()}")]
public record MemberValueMappingConfiguration(StringMemberPath Target, AttributeValue? Value) : HasSyntaxReference
{
/// <summary>
/// Constructor used by <see cref="AttributeDataAccessor"/>.
/// </summary>
public MemberValueMappingConfiguration(StringMemberPath target)
: this(target, null) { }

public string? Use { get; set; }

public bool IsValid => Use != null ^ Value != null;

public string DescribeValue()
{
if (Use != null)
return Use + "()";

return Value?.Expression.ToFullString() ?? string.Empty;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Riok.Mapperly.Configuration;
public record MembersMappingConfiguration(
IReadOnlyCollection<string> IgnoredSources,
IReadOnlyCollection<string> IgnoredTargets,
IReadOnlyCollection<MemberValueMappingConfiguration> ValueMappings,
IReadOnlyCollection<MemberMappingConfiguration> ExplicitMappings,
IReadOnlyCollection<NestedMembersMappingConfiguration> NestedMappings,
IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy,
Expand All @@ -17,7 +18,8 @@ public IEnumerable<string> GetMembersWithExplicitConfigurations(MappingSourceTar
var members = sourceTarget switch
{
MappingSourceTarget.Source => ExplicitMappings.Where(x => x.Source.Path.Count > 0).Select(x => x.Source.Path[0]),
MappingSourceTarget.Target => ExplicitMappings.Select(x => x.Target.Path[0]),
MappingSourceTarget.Target
=> ExplicitMappings.Select(x => x.Target.Path[0]).Concat(ValueMappings.Select(x => x.Target.Path[0])),
_ => throw new ArgumentOutOfRangeException(nameof(sourceTarget), sourceTarget, "Neither source or target"),
};
return members.Distinct();
Expand Down
1 change: 1 addition & 0 deletions src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ MapperConfiguration defaultMapperConfiguration

_builderContext = new SimpleMappingBuilderContext(
compilationContext,
mapperDeclaration,
configurationReader,
_symbolAccessor,
new GenericTypeChecker(_symbolAccessor, _types),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public interface IMembersBuilderContext<out T>

void SetTargetMemberMapped(IMappableMember targetMember);

void ConsumeMemberConfig(MemberMappingInfo members);
void ConsumeMemberConfigs(MemberMappingInfo members);

IEnumerable<IMappableMember> EnumerateUnmappedTargetMembers();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ private static void AddUnmappedSourceMembersDiagnostics(MappingBuilderContext ct

private static void AddUnmappedTargetMembersDiagnostics(MappingBuilderContext ctx, MembersMappingState state)
{
foreach (var targetMember in state.UnmappedTargetMembers)
foreach (var targetMember in state.EnumerateUnmappedTargetMembers())
{
if (targetMember.IsRequired)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ public class MembersContainerBuilderContext<T>(MappingBuilderContext builderCont
/// <param name="memberMapping">The member mapping to be applied if the source member is not null</param>
public void AddNullDelegateMemberAssignmentMapping(IMemberAssignmentMapping memberMapping)
{
if (memberMapping.MemberInfo.SourceMember == null)
{
AddMemberAssignmentMapping(memberMapping);
return;
}

var nullConditionSourcePath = new NonEmptyMemberPath(
memberMapping.MemberInfo.SourceMember.RootType,
memberMapping.MemberInfo.SourceMember.PathWithoutTrailingNonNullable().ToList()
Expand Down
Loading

0 comments on commit 94d29fc

Please sign in to comment.