Skip to content

Commit

Permalink
feat: datetime to dateonly and timeonly mapping (#253)
Browse files Browse the repository at this point in the history
  • Loading branch information
hoyau authored Jan 25, 2023
1 parent 8fa4116 commit c40108d
Show file tree
Hide file tree
Showing 23 changed files with 236 additions and 20 deletions.
2 changes: 1 addition & 1 deletion docs/docs/02-configuration/02-static-mappers.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public static partial class CarMapper
{
public static partial CarDto CarToCarDto(this Car car);

private static DateOnly DateTimeToDateOnly(DateTime dt) => DateOnly.FromDateTime(dt);
private static int TimeSpanToHours(TimeSpan t) => t.Hours;
}
```

Expand Down
4 changes: 2 additions & 2 deletions docs/docs/02-configuration/06-user-implemented-methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ public partial class CarMapper
{
public partial CarDto CarToCarDto(Car car);

private DateOnly DateTimeToDateOnly(DateTime dt) => DateOnly.FromDateTime(dt);
private int TimeSpanToHours(TimeSpan t) => t.Hours;
}
```

Whenever Mapperly needs a mapping from `DateTime` to `DateOnly` inside the `CarMapper` implementation, it will use the provided implementation.
Whenever Mapperly needs a mapping from `TimeSpan` to `int` inside the `CarMapper` implementation, it will use the provided implementation.
30 changes: 16 additions & 14 deletions docs/docs/02-configuration/11-conversions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@

Mapperly implements several types of automatic conversions (in order of priority):

| Name | Description | Conditions |
| ----------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| Direct assignment | Directly assigns the source object to the target | Source type is assignable to the target type and `UseDeepCloning` is `false` |
| Dictionary | Maps a source dictionary to an enumerable target | Source type is an `IDictionary<,>` or an `IReadOnlyDictionary<,>` |
| Enumerable | Maps an enumerable source to an enumerable target | Source type is an `IEnumerable<>` |
| Implicit cast | Implicit cast operator | An implicit cast operator is defined to cast from the source type to the target type |
| Parse method | Uses a static `Parse` method on the source type | Source type is a `string` and target has a static method with the following signature: `TTarget Parse(string)`. |
| Constructor | Uses a constructor on the target type with the source as single parameter | Target type has a visible constructor with a single parameter of the source type. |
| String to enum | Maps a string to an enum member name | Source type is a `string` and the target type is an enum |
| Enum to string | Maps an enum member name to a string | Source type is an enum and the target type is a `string` |
| Enum to enum | Maps an enum to another enum either by value or by member name | Source and target types are enums |
| Explicit cast | Explicit cast operator | An explicit cast operator is defined to cast from the source type to the target type |
| ToString | `ToString` method of an object | Target type is a `string` |
| New instance | Create a new instance of the target type and map all properties | The target type has a visible constructor or an object factory exists for the target type |
| Name | Description | Conditions |
| -------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| Direct assignment | Directly assigns the source object to the target | Source type is assignable to the target type and `UseDeepCloning` is `false` |
| Dictionary | Maps a source dictionary to an enumerable target | Source type is an `IDictionary<,>` or an `IReadOnlyDictionary<,>` |
| Enumerable | Maps an enumerable source to an enumerable target | Source type is an `IEnumerable<>` |
| Implicit cast | Implicit cast operator | An implicit cast operator is defined to cast from the source type to the target type |
| Parse method | Uses a static `Parse` method on the source type | Source type is a `string` and target has a static method with the following signature: `TTarget Parse(string)`. |
| Constructor | Uses a constructor on the target type with the source as single parameter | Target type has a visible constructor with a single parameter of the source type. |
| String to enum | Maps a string to an enum member name | Source type is a `string` and the target type is an enum |
| Enum to string | Maps an enum member name to a string | Source type is an enum and the target type is a `string` |
| Enum to enum | Maps an enum to another enum either by value or by member name | Source and target types are enums |
| DateTime to DateOnly | Maps a `DateTime` to a `DateOnly` | Source type is a `DateTime` and target type is a `DateOnly` |
| DateTime to TimeOnly | Maps a `DateTime` to a `TimeOnly` | Source type is a `DateTime` and target type is a `TimeOnly` |
| Explicit cast | Explicit cast operator | An explicit cast operator is defined to cast from the source type to the target type |
| ToString | `ToString` method of an object | Target type is a `string` |
| New instance | Create a new instance of the target type and map all properties | The target type has a visible constructor or an object factory exists for the target type |

## Disable all automatic conversions

Expand Down
14 changes: 14 additions & 0 deletions src/Riok.Mapperly.Abstractions/MappingConversionType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,20 @@ public enum MappingConversionType
/// </summary>
EnumToEnum = 1 << 7,

/// <summary>
/// If the source is a <see cref="DateTime"/>
/// and the target is a DateOnly
/// uses the `FromDateTime` method on the target type with the source as single parameter.
/// </summary>
DateTimeToDateOnly = 1 << 8,

/// <summary>
/// If the source is a <see cref="DateTime"/>
/// and the target is a TimeOnly
/// uses the `FromDateTime` method on the target type with the source as single parameter.
/// </summary>
DateTimeToTimeOnly = 1 << 9,

/// <summary>
/// Enables all supported conversions.
/// </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 @@ -50,6 +50,8 @@ Riok.Mapperly.Abstractions.PropertyNameMappingStrategy.CaseSensitive = 0 -> Riok
Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.All = -1 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.Constructor = 1 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.DateTimeToDateOnly = 256 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.DateTimeToTimeOnly = 512 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.EnumToEnum = 128 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.EnumToString = 64 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.ExplicitCast = 4 -> Riok.Mapperly.Abstractions.MappingConversionType
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.Mappings;

namespace Riok.Mapperly.Descriptors.MappingBuilders;

public static class DateTimeToDateOnlyMappingBuilder
{
private const string FromDateTimeMethodName = "FromDateTime";

public static StaticMethodMapping? TryBuildMapping(MappingBuilderContext ctx)
{
if (!ctx.IsConversionEnabled(MappingConversionType.DateTimeToDateOnly) || ctx.Types.DateOnly == null)
return null;

if (ctx.Source.SpecialType != SpecialType.System_DateTime)
return null;

if (ctx.Target is not INamedTypeSymbol namedSymbol || !SymbolEqualityComparer.Default.Equals(namedSymbol, ctx.Types.DateOnly))
return null;

var fromDateTimeMethod = ResolveFromDateTimeMethod(ctx);
if (fromDateTimeMethod is null)
return null;

return new StaticMethodMapping(fromDateTimeMethod);
}

private static IMethodSymbol? ResolveFromDateTimeMethod(MappingBuilderContext ctx)
{
return ctx.Types.DateOnly?
.GetMembers(FromDateTimeMethodName)
.OfType<IMethodSymbol>()
.FirstOrDefault(m => m.IsStatic);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.Mappings;

namespace Riok.Mapperly.Descriptors.MappingBuilders;

public static class DateTimeToTimeOnlyMappingBuilder
{
private const string FromDateTimeMethodName = "FromDateTime";

public static StaticMethodMapping? TryBuildMapping(MappingBuilderContext ctx)
{
if (!ctx.IsConversionEnabled(MappingConversionType.DateTimeToTimeOnly) || ctx.Types.TimeOnly == null)
return null;

if (ctx.Source.SpecialType != SpecialType.System_DateTime)
return null;

if (ctx.Target is not INamedTypeSymbol namedSymbol || !SymbolEqualityComparer.Default.Equals(namedSymbol, ctx.Types.TimeOnly))
return null;

var fromDateTimeMethod = ResolveFromDateTimeMethod(ctx);
if (fromDateTimeMethod is null)
return null;

return new StaticMethodMapping(fromDateTimeMethod);
}

private static IMethodSymbol? ResolveFromDateTimeMethod(MappingBuilderContext ctx)
{
return ctx.Types.TimeOnly?
.GetMembers(FromDateTimeMethodName)
.OfType<IMethodSymbol>()
.FirstOrDefault(m => m.IsStatic);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public class MappingBuilder
StringToEnumMappingBuilder.TryBuildMapping,
EnumToStringMappingBuilder.TryBuildMapping,
EnumMappingBuilder.TryBuildMapping,
DateTimeToDateOnlyMappingBuilder.TryBuildMapping,
DateTimeToTimeOnlyMappingBuilder.TryBuildMapping,
ExplicitCastMappingBuilder.TryBuildMapping,
ToStringMappingBuilder.TryBuildMapping,
NewInstanceObjectPropertyMappingBuilder.TryBuildMapping,
Expand Down
8 changes: 8 additions & 0 deletions src/Riok.Mapperly/Descriptors/WellKnownTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public class WellKnownTypes
private INamedTypeSymbol? _keyValuePair;
private INamedTypeSymbol? _dictionary;

private INamedTypeSymbol? _dateOnly;
private INamedTypeSymbol? _timeOnly;

internal WellKnownTypes(Compilation compilation)
{
_compilation = compilation;
Expand All @@ -52,8 +55,13 @@ internal WellKnownTypes(Compilation compilation)
public INamedTypeSymbol IReadOnlyList => _iReadOnlyList ??= GetTypeSymbol(typeof(IReadOnlyList<>));
public INamedTypeSymbol KeyValuePair => _keyValuePair ??= GetTypeSymbol(typeof(KeyValuePair<,>));
public INamedTypeSymbol Dictionary => _dictionary ??= GetTypeSymbol(typeof(Dictionary<,>));
public INamedTypeSymbol? DateOnly => _dateOnly ??= GetTypeSymbol("System.DateOnly");
public INamedTypeSymbol? TimeOnly => _timeOnly ??= GetTypeSymbol("System.TimeOnly");

private INamedTypeSymbol GetTypeSymbol(Type type)
=> _compilation.GetTypeByMetadataName(type.FullName ?? throw new InvalidOperationException("Could not get name of type " + type))
?? throw new InvalidOperationException("Could not get type " + type.FullName);

private INamedTypeSymbol? GetTypeSymbol(string typeFullName)
=> _compilation.GetTypeByMetadataName(typeFullName);
}
16 changes: 16 additions & 0 deletions test/Riok.Mapperly.IntegrationTests/BaseMapperTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
using Riok.Mapperly.IntegrationTests.Dto;
#if !NET6_0_OR_GREATER
using Riok.Mapperly.IntegrationTests.Helpers;
#endif
using Riok.Mapperly.IntegrationTests.Models;
using VerifyTests;
using VerifyXunit;
Expand All @@ -11,6 +15,16 @@ public abstract class BaseMapperTest
{
static BaseMapperTest()
{
#if !NET6_0_OR_GREATER
VerifierSettings.AddExtraSettings(settings =>
{
settings.Converters.Add(new PortableDateOnlyConverter());
settings.Converters.Add(new PortableTimeOnlyConverter());
});
#endif

VerifierSettings.DontScrubDateTimes();

Verifier.DerivePathInfo((file, _, type, method)
=> new PathInfo(Path.Combine(Path.GetDirectoryName(file)!, "_snapshots"), type.Name, method.Name));
}
Expand Down Expand Up @@ -40,6 +54,8 @@ protected TestObject NewTestObj()
SubObject = new InheritanceSubObject { BaseIntValue = 1, SubIntValue = 2, },
EnumRawValue = TestEnum.Value20,
EnumStringValue = TestEnum.Value30,
DateTimeValueTargetDateOnly = new DateTime(2020, 1, 3, 15, 10, 5, DateTimeKind.Utc),
DateTimeValueTargetTimeOnly = new DateTime(2020, 1, 3, 15, 10, 5, DateTimeKind.Utc),
IgnoredStringValue = "ignored",
RenamedStringValue = "fooBar2",
StringNullableTargetNotNullable = "fooBar3",
Expand Down
5 changes: 5 additions & 0 deletions test/Riok.Mapperly.IntegrationTests/Dto/TestObjectDto.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using Riok.Mapperly.IntegrationTests.Models;

namespace Riok.Mapperly.IntegrationTests.Dto
Expand Down Expand Up @@ -64,5 +65,9 @@ public TestObjectDto(int ctorValue, int unknownValue = 10, int ctorValue2 = 100)

public string? IgnoredStringValue { get; set; }
public int IgnoredIntValue { get; set; }

public DateOnly DateTimeValueTargetDateOnly { get; set; }

public TimeOnly DateTimeValueTargetTimeOnly { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#if !NET6_0_OR_GREATER
using System;
using System.Globalization;
using VerifyTests;

namespace Riok.Mapperly.IntegrationTests.Helpers
{
/// <summary>
/// Portable <see cref="DateOnly"/> converter for VerifyTests to ensure consistent json output across different net versions.
/// Is necessary to handle net framework tests which include <see cref="DateOnly"/> per nuget package.
/// </summary>
public class PortableDateOnlyConverter : WriteOnlyJsonConverter<DateOnly>
{
public override void Write(VerifyJsonWriter writer, DateOnly value) =>
// copied format from https://github.com/VerifyTests/Verify/blob/19.7.1/src/Verify/Serialization/VerifierSettings.cs#L58
writer.WriteRawValue(value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#if !NET6_0_OR_GREATER
using System;
using System.Globalization;
using VerifyTests;


namespace Riok.Mapperly.IntegrationTests.Helpers
{
/// <summary>
/// Portable <see cref="TimeOnly"/> converter for VerifyTests to ensure consistent json output across different net versions.
/// Is necessary to handle net framework tests which include <see cref="TimeOnly"/> per nuget package.
/// </summary>
public class PortableTimeOnlyConverter : WriteOnlyJsonConverter<TimeOnly>
{
public override void Write(VerifyJsonWriter writer, TimeOnly value) =>
// copied format from https://github.com/VerifyTests/Verify/blob/19.7.1/src/Verify/Serialization/VerifierSettings.cs#L65
writer.WriteRawValue(value.ToString("h:mm tt", CultureInfo.InvariantCulture));
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public static TestObjectDto MapToDto(TestObject src)
// disable obsolete warning, as the obsolete attribute should still be tested.
#pragma warning disable CS0618
[MapperIgnore(nameof(TestObject.IgnoredStringValue))]
[MapperIgnore(nameof(TestObjectDto.DateTimeValueTargetDateOnly))]
[MapperIgnore(nameof(TestObjectDto.DateTimeValueTargetTimeOnly))]
#pragma warning restore CS0618
[MapperIgnoreTarget(nameof(TestObject.IgnoredIntValue))]
[MapperIgnoreSource(nameof(TestObjectDto.IgnoredIntValue))]
Expand Down
2 changes: 2 additions & 0 deletions test/Riok.Mapperly.IntegrationTests/Mapper/TestMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public TestObjectDto MapToDto(TestObject src)
// disable obsolete warning, as the obsolete attribute should still be tested.
#pragma warning disable CS0618
[MapperIgnore(nameof(TestObject.IgnoredStringValue))]
[MapperIgnore(nameof(TestObjectDto.DateTimeValueTargetDateOnly))]
[MapperIgnore(nameof(TestObjectDto.DateTimeValueTargetTimeOnly))]
#pragma warning restore CS0618
[MapperIgnoreTarget(nameof(TestObject.IgnoredIntValue))]
[MapperIgnoreSource(nameof(TestObjectDto.IgnoredIntValue))]
Expand Down
5 changes: 5 additions & 0 deletions test/Riok.Mapperly.IntegrationTests/Models/TestObject.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;

namespace Riok.Mapperly.IntegrationTests.Models
Expand Down Expand Up @@ -63,5 +64,9 @@ public TestObject(int ctorValue, int unknownValue = 10, int ctorValue2 = 100)
public string? IgnoredStringValue { get; set; }

public int IgnoredIntValue { get; set; }

public DateTime DateTimeValueTargetDateOnly { get; set; }

public DateTime DateTimeValueTargetTimeOnly { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,8 @@
<PackageReference Include="Riok.Mapperly" Version="$(MapperlyNugetPackageVersion)" />
</ItemGroup>

<ItemGroup Condition="'$(MapperlyIntegrationTestsTargetFramework)' == 'net48'">
<PackageReference Include="Portable.System.DateTimeOnly" Version="7.0.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,7 @@
SubObject: {
SubIntValue: 2,
BaseIntValue: 1
}
},
DateTimeValueTargetDateOnly: 2020-01-03,
DateTimeValueTargetTimeOnly: 3:10 PM
}
Loading

0 comments on commit c40108d

Please sign in to comment.