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

enum is defined check and fallback values #491

Merged
merged 1 commit into from
Jun 12, 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
31 changes: 25 additions & 6 deletions docs/docs/02-configuration/04-enum.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ Apply the `MapEnumAttribute` and pass the strategy to be used for this enum.
It is also possible to set the strategy for the entire mapper via the `MapperAttribute`.
Available strategies:

| Name | Description |
| ------- | ---------------------------------------------- |
| ByValue | Matches enum entries by their values (default) |
| ByName | Matches enum entries by their exact names |
| Name | Description |
| ------------------- | --------------------------------------------------------------------------- |
| ByValue | Matches enum entries by their values (default) |
| ByValueCheckDefined | Matches enum entries by their values, checks if the target value is defined |
| ByName | Matches enum entries by their exact names |

The `IgnoreCase` property allows to opt in for case insensitive mappings (defaults to `false`).

Expand All @@ -33,7 +34,7 @@ public partial class CarMapper
```

</TabItem>
<TabItem value="enum" label="Enum (Mapping Method Level)">
<TabItem value="enum" label="Enum (mapping method level)">

Applied to the specific enum mapped by this method.
Attribute is only valid on mapping method with enums as parameters.
Expand Down Expand Up @@ -61,14 +62,32 @@ Attribute is only valid on mapping methods with enums as parameters.
[Mapper]
public partial class CarMapper
{
[MapEnum(EnumMappingStrategy.ByName, IgnoreCase = true)]
[MapEnum(EnumMappingStrategy.ByName)]
// highlight-start
[MapEnumValue(CarFeature.AWD, CarFeatureDto.AllWheelDrive)]
// highlight-end
public partial CarFeatureDto MapFeature(CarFeature feature);
}
```

## Fallback value

To map to a fallback value instead of throwing when encountering an unknown value,
the `FallbackValue` property on the `MapEnum` attribute can be used.

`FallbackValue` is supported by `ByName` and `ByValueCheckDefined`.

```csharp
[Mapper]
public partial class CarMapper
{
// highlight-start
[MapEnum(EnumMappingStrategy.ByName, FallbackValue = CarFeatureDto.Unknown)]
// highlight-end
public partial CarFeatureDto MapFeature(CarFeature feature);
}
```

### Strict enum mappings

To enforce strict enum mappings
Expand Down
6 changes: 6 additions & 0 deletions docs/docs/98-contributing/05-common-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,9 @@ To support a new roslyn version via multi targeting follow these steps (see also
6. Adjust the .NET version in the `global.json` file as needed.
7. If generated code changes based on the new Roslyn version,
introduce a new `roslynVersionName` in `Riok.Mapperly.IntegrationTests.BaseMapperTest.GetRoslynVersion()` and generate the new snapshots.

## Mapping syntax

Mapperly Mappings use Roslyn syntax trees.
[RoslynQuoter](https://roslynquoter.azurewebsites.net/) and [SharpLab](https://sharplab.io/)
are fantastic tools to understand and work with Roslyn syntax trees.
6 changes: 6 additions & 0 deletions src/Riok.Mapperly.Abstractions/EnumMappingStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,10 @@ public enum EnumMappingStrategy
/// Matches enum members by their names.
/// </summary>
ByName,

/// <summary>
/// Matches enum members by their values.
/// Checks if the value is defined in the enum.
/// </summary>
ByValueCheckDefined,
}
5 changes: 5 additions & 0 deletions src/Riok.Mapperly.Abstractions/MapEnumAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,9 @@ public MapEnumAttribute(EnumMappingStrategy strategy)
/// Whether the case should be ignored during mappings.
/// </summary>
public bool IgnoreCase { get; set; }

/// <summary>
/// The fallback value if an enum cannot be mapped, used instead of throwing.
/// </summary>
public object? FallbackValue { get; set; }
}
3 changes: 3 additions & 0 deletions src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,6 @@ Riok.Mapperly.Abstractions.MapDerivedTypeAttribute
Riok.Mapperly.Abstractions.MapDerivedTypeAttribute.MapDerivedTypeAttribute(System.Type! sourceType, System.Type! targetType) -> void
Riok.Mapperly.Abstractions.MapDerivedTypeAttribute.SourceType.get -> System.Type!
Riok.Mapperly.Abstractions.MapDerivedTypeAttribute.TargetType.get -> System.Type!
Riok.Mapperly.Abstractions.EnumMappingStrategy.ByValueCheckDefined = 2 -> Riok.Mapperly.Abstractions.EnumMappingStrategy
Riok.Mapperly.Abstractions.MapEnumAttribute.FallbackValue.get -> object?
Riok.Mapperly.Abstractions.MapEnumAttribute.FallbackValue.set -> void
2 changes: 2 additions & 0 deletions src/Riok.Mapperly/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,5 @@ RMG038 | Mapper | Info | An enum member could not be found on the target
RMG039 | Mapper | Error | Enum source value is specified multiple times, a source enum value may only be specified once
RMG040 | Mapper | Error | A target enum member value does not match the target enum type
RMG041 | Mapper | Error | A source enum member value does not match the source enum type
RMG042 | Mapper | Error | The type of the enum fallback value does not match the target enum type
RMG043 | Mapper | Warning | Enum fallback values are only supported for the ByName and ByValueCheckDefined strategies, but not for the ByValue strategy
26 changes: 15 additions & 11 deletions src/Riok.Mapperly/Configuration/AttributeDataAccessor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Reflection;
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Descriptors;
using Riok.Mapperly.Helpers;

namespace Riok.Mapperly.Configuration;

Expand All @@ -19,8 +20,8 @@ public AttributeDataAccessor(WellKnownTypes types)
public T AccessSingle<T>(ISymbol symbol)
where T : Attribute => Access<T, T>(symbol).Single();

public T? AccessFirstOrDefault<T>(ISymbol symbol)
where T : Attribute => Access<T, T>(symbol).FirstOrDefault();
public TData? AccessFirstOrDefault<TAttribute, TData>(ISymbol symbol)
where TAttribute : Attribute => Access<TAttribute, TData>(symbol).FirstOrDefault();

public IEnumerable<TAttribute> Access<TAttribute>(ISymbol symbol)
where TAttribute : Attribute => Access<TAttribute, TAttribute>(symbol);
Expand All @@ -29,6 +30,7 @@ public IEnumerable<TAttribute> Access<TAttribute>(ISymbol symbol)
/// Reads the 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 an accessible ctor with the parameters 0 to n-1 to be of type <see cref="ITypeSymbol"/>.
/// <see cref="TData"/> needs to have exactly the same constructors as <see cref="TAttribute"/> with additional type arguments.
/// </summary>
/// <param name="symbol">The symbol on which the attributes should be read.</param>
/// <typeparam name="TAttribute">The type of the attribute.</typeparam>
Expand All @@ -39,6 +41,7 @@ public IEnumerable<TData> Access<TAttribute, TData>(ISymbol symbol)
where TAttribute : Attribute
{
var attrType = typeof(TAttribute);
var dataType = typeof(TData);
var attrSymbol = _types.Get($"{attrType.Namespace}.{attrType.Name}");

var attrDatas = symbol
Expand All @@ -50,11 +53,11 @@ public IEnumerable<TData> Access<TAttribute, TData>(ISymbol symbol)
var typeArguments = attrData.AttributeClass?.TypeArguments ?? Enumerable.Empty<ITypeSymbol>();
var ctorArguments = attrData.ConstructorArguments.Select(BuildArgumentValue);
var newInstanceArguments = typeArguments.Concat(ctorArguments).ToArray();
var attr = (TData)Activator.CreateInstance(typeof(TData), newInstanceArguments);
var attr = (TData)Activator.CreateInstance(dataType, newInstanceArguments);

foreach (var namedArgument in attrData.NamedArguments)
{
var prop = attrType.GetProperty(namedArgument.Key);
var prop = dataType.GetProperty(namedArgument.Key);
if (prop == null)
throw new InvalidOperationException($"Could not get property {namedArgument.Key} of attribute {attrType.FullName}");

Expand Down Expand Up @@ -89,24 +92,25 @@ arg.Type as IArrayTypeSymbol

var values = arg.Values.Select(BuildArgumentValue).ToArray();

// if we can't get the element type then it's not available to reflection (only accessible by Roslyn) so use the TypedConstant
// if this is the case, a roslyn typed configuration class should be used which accepts the typed constants.
var elementType = GetReflectionType(arrayTypeSymbol.ElementType) ?? typeof(TypedConstant);
var elementType = GetReflectionType(arrayTypeSymbol.ElementType);
if (elementType == null)
throw new InvalidOperationException("Non reflection array configurations are not supported");

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

private static object? GetEnumValue(TypedConstant arg)
{
if (arg.Value == null)
return null;

var enumType = GetReflectionType(arg.Type ?? throw new InvalidOperationException("Type is null"));

// if we can't get the enum type then it's not available to reflection (only accessible by Roslyn) so return the TypedConstant
// if this is the case, a roslyn typed configuration class should be used which accepts the typed constants.
if (enumType == null)
return arg;

return arg.Value == null ? null : Enum.ToObject(enumType, arg.Value);
return enumType == null ? arg.Type.GetFields().First(f => Equals(f.ConstantValue, arg.Value)) : Enum.ToObject(enumType, arg.Value);
}

private static Type? GetReflectionType(ITypeSymbol type)
Expand Down
2 changes: 2 additions & 0 deletions src/Riok.Mapperly/Configuration/EnumMappingConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;

namespace Riok.Mapperly.Configuration;

public record EnumMappingConfiguration(
EnumMappingStrategy Strategy,
bool IgnoreCase,
IFieldSymbol? FallbackValue,
IReadOnlyCollection<EnumValueMappingConfiguration> ExplicitMappings
);
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ namespace Riok.Mapperly.Configuration;
/// </summary>
/// <param name="Source">The source constant of the enum value mapping.</param>
/// <param name="Target">The target constant of the enum value mapping.</param>
public record EnumValueMappingConfiguration(TypedConstant Source, TypedConstant Target);
public record EnumValueMappingConfiguration(IFieldSymbol Source, IFieldSymbol Target);
31 changes: 31 additions & 0 deletions src/Riok.Mapperly/Configuration/MapEnumAttributeData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;

namespace Riok.Mapperly.Configuration;

/// <summary>
/// Represents the <see cref="MapEnumAttribute"/>
/// with enum values as typed constants.
/// </summary>
public class MapEnumAttributeData
{
public MapEnumAttributeData(EnumMappingStrategy strategy)
{
Strategy = strategy;
}

/// <summary>
/// The strategy to be used to map enums.
/// </summary>
public EnumMappingStrategy Strategy { get; }

/// <summary>
/// Whether the case should be ignored during mappings.
/// </summary>
public bool? IgnoreCase { get; set; }

/// <summary>
/// The fallback value if an enum cannot be mapped, used instead of throwing.
/// </summary>
public IFieldSymbol? FallbackValue { get; set; }
}
8 changes: 5 additions & 3 deletions src/Riok.Mapperly/Configuration/MapperConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public MapperConfiguration(WellKnownTypes wellKnownTypes, ISymbol mapperSymbol)
new EnumMappingConfiguration(
Mapper.EnumMappingStrategy,
Mapper.EnumMappingIgnoreCase,
null,
Array.Empty<EnumValueMappingConfiguration>()
),
new PropertiesMappingConfiguration(Array.Empty<string>(), Array.Empty<string>(), Array.Empty<MapPropertyAttribute>()),
Expand Down Expand Up @@ -62,11 +63,12 @@ private PropertiesMappingConfiguration BuildPropertiesConfig(IMethodSymbol metho

private EnumMappingConfiguration BuildEnumConfig(IMethodSymbol method)
{
var config = _dataAccessor.AccessFirstOrDefault<MapEnumAttribute>(method);
var configData = _dataAccessor.AccessFirstOrDefault<MapEnumAttribute, MapEnumAttributeData>(method);
var explicitMappings = _dataAccessor.Access<MapEnumValueAttribute, EnumValueMappingConfiguration>(method).ToList();
return new EnumMappingConfiguration(
config?.Strategy ?? _defaultConfiguration.Enum.Strategy,
config?.IgnoreCase ?? _defaultConfiguration.Enum.IgnoreCase,
configData?.Strategy ?? _defaultConfiguration.Enum.Strategy,
configData?.IgnoreCase ?? _defaultConfiguration.Enum.IgnoreCase,
configData?.FallbackValue,
explicitMappings
);
}
Expand Down
Loading