diff --git a/src/Atc.SourceGenerators/Generators/Internal/MappingTypeAnalyzer.cs b/src/Atc.SourceGenerators/Generators/Internal/MappingTypeAnalyzer.cs index 6400c94..ebc181b 100644 --- a/src/Atc.SourceGenerators/Generators/Internal/MappingTypeAnalyzer.cs +++ b/src/Atc.SourceGenerators/Generators/Internal/MappingTypeAnalyzer.cs @@ -71,7 +71,8 @@ internal static bool IsBuiltInTypeConversion( return IsStringToFromSystemTypeConversion(sourceTypeName, targetTypeName) || IsStringToFromUriConversion(sourceTypeName, targetTypeName) || - IsStringToFromEnumConversion(sourceType, targetType, sourceTypeName, targetTypeName); + IsStringToFromEnumConversion(sourceType, targetType, sourceTypeName, targetTypeName) || + IsDateTimeOffsetToFromDateTimeConversion(sourceTypeName, targetTypeName); } private static bool IsStringToFromSystemTypeConversion( @@ -126,6 +127,17 @@ private static bool IsStringToFromUriConversion( (normalizedSource == "System.Uri" && normalizedTarget == "string"); } + private static bool IsDateTimeOffsetToFromDateTimeConversion( + string sourceTypeName, + string targetTypeName) + { + var normalizedSource = sourceTypeName.TrimEnd('?'); + var normalizedTarget = targetTypeName.TrimEnd('?'); + + return (normalizedSource == "System.DateTimeOffset" && normalizedTarget == "System.DateTime") || + (normalizedSource == "System.DateTime" && normalizedTarget == "System.DateTimeOffset"); + } + private static bool IsStringToFromEnumConversion( ITypeSymbol sourceType, ITypeSymbol targetType, diff --git a/src/Atc.SourceGenerators/Generators/MappingConfigurationGenerator.cs b/src/Atc.SourceGenerators/Generators/MappingConfigurationGenerator.cs index 545a113..a47f447 100644 --- a/src/Atc.SourceGenerators/Generators/MappingConfigurationGenerator.cs +++ b/src/Atc.SourceGenerators/Generators/MappingConfigurationGenerator.cs @@ -2518,12 +2518,50 @@ private static string GenerateBuiltInTypeConversion( return $"bool.Parse({sourcePropertyAccess})"; } + // DateTimeOffset <-> DateTime conversions + if (normalizedSource == "System.DateTimeOffset" && normalizedTarget == "System.DateTime") + { + var sourceIsNullableValue = MappingTypeAnalyzer.IsNullableValueType(prop.SourceProperty.Type); + var targetIsNullableValue = MappingTypeAnalyzer.IsNullableValueType(prop.TargetProperty.Type); + + if (sourceIsNullableValue && targetIsNullableValue) + { + return $"{sourcePropertyAccess}?.DateTime"; + } + + if (sourceIsNullableValue) + { + return $"{sourcePropertyAccess}!.Value.DateTime"; + } + + return $"{sourcePropertyAccess}.DateTime"; + } + + if (normalizedSource == "System.DateTime" && normalizedTarget == "System.DateTimeOffset") + { + var sourceIsNullableValue = MappingTypeAnalyzer.IsNullableValueType(prop.SourceProperty.Type); + var targetIsNullableValue = MappingTypeAnalyzer.IsNullableValueType(prop.TargetProperty.Type); + + if (sourceIsNullableValue && targetIsNullableValue) + { + var varName = $"__{char.ToLowerInvariant(prop.SourceProperty.Name[0])}{prop.SourceProperty.Name.Substring(1)}"; + return $"{sourcePropertyAccess} is {{ }} {varName} ? new global::System.DateTimeOffset({varName}) : null"; + } + + if (sourceIsNullableValue) + { + return $"new global::System.DateTimeOffset({sourcePropertyAccess}!.Value)"; + } + + return $"new global::System.DateTimeOffset({sourcePropertyAccess})"; + } + // string <-> Uri conversions if (normalizedSource == "string" && normalizedTarget == "System.Uri") { if (sourceIsNullableRef || targetIsNullableRef) { - return $"{sourcePropertyAccess} is not null ? new global::System.Uri({sourcePropertyAccess}) : null"; + return $"!string.IsNullOrEmpty({sourcePropertyAccess}) ? new global::System.Uri({sourcePropertyAccess}) : null"; } return $"new global::System.Uri({sourcePropertyAccess})"; diff --git a/src/Atc.SourceGenerators/Generators/ObjectMappingGenerator.cs b/src/Atc.SourceGenerators/Generators/ObjectMappingGenerator.cs index 6f0d8d4..3908a8f 100644 --- a/src/Atc.SourceGenerators/Generators/ObjectMappingGenerator.cs +++ b/src/Atc.SourceGenerators/Generators/ObjectMappingGenerator.cs @@ -760,6 +760,15 @@ private static bool IsBuiltInTypeConversion( return true; } + // DateTimeOffset <-> DateTime + var normalizedSource = sourceTypeName.TrimEnd('?'); + var normalizedTarget = targetTypeName.TrimEnd('?'); + if ((normalizedSource == "System.DateTimeOffset" && normalizedTarget == "System.DateTime") || + (normalizedSource == "System.DateTime" && normalizedTarget == "System.DateTimeOffset")) + { + return true; + } + return false; } @@ -1917,6 +1926,18 @@ private static string GenerateBuiltInTypeConversion( return $"bool.Parse({sourcePropertyAccess})"; } + // DateTimeOffset -> DateTime + if (sourceTypeName == "System.DateTimeOffset" && targetTypeName == "System.DateTime") + { + return $"{sourcePropertyAccess}.DateTime"; + } + + // DateTime -> DateTimeOffset + if (sourceTypeName == "System.DateTime" && targetTypeName == "System.DateTimeOffset") + { + return $"new global::System.DateTimeOffset({sourcePropertyAccess})"; + } + // Fallback (should not reach here if IsBuiltInTypeConversion is correct) return sourcePropertyAccess; } diff --git a/test/Atc.SourceGenerators.Tests/Generators/MappingConfiguration/MappingConfigurationGeneratorBuiltInConversionTests.cs b/test/Atc.SourceGenerators.Tests/Generators/MappingConfiguration/MappingConfigurationGeneratorBuiltInConversionTests.cs index 6ce32b7..3f3f028 100644 --- a/test/Atc.SourceGenerators.Tests/Generators/MappingConfiguration/MappingConfigurationGeneratorBuiltInConversionTests.cs +++ b/test/Atc.SourceGenerators.Tests/Generators/MappingConfiguration/MappingConfigurationGeneratorBuiltInConversionTests.cs @@ -119,7 +119,7 @@ public static partial class ItemMappings var (diagnostics, output) = GetGeneratedOutput(source); Assert.Empty(diagnostics); - Assert.Contains("is not null ? new global::System.Uri(source.Link) : null", output, StringComparison.Ordinal); + Assert.Contains("!string.IsNullOrEmpty(source.Link) ? new global::System.Uri(source.Link) : null", output, StringComparison.Ordinal); } [Fact] @@ -644,4 +644,164 @@ public static partial class ItemMappings Assert.Contains("Enum.TryParse(source.Status, out var __parsed)", output, StringComparison.Ordinal); Assert.Contains("(double)source.Value", output, StringComparison.Ordinal); } + + [Fact] + public void Config_Should_Map_DateTimeOffset_To_DateTime() + { + const string source = """ + using System; + using Atc.SourceGenerators.Annotations; + + namespace SourceNs + { + public class Item + { + public DateTimeOffset Timestamp { get; set; } + } + } + + namespace TargetNs + { + public class Item + { + public DateTime Timestamp { get; set; } + } + } + + namespace Mappings + { + [MappingConfiguration] + public static partial class ItemMappings + { + public static partial TargetNs.Item MapToItem(this SourceNs.Item source); + } + } + """; + + var (diagnostics, output) = GetGeneratedOutput(source); + + Assert.Empty(diagnostics); + Assert.Contains("source.Timestamp.DateTime", output, StringComparison.Ordinal); + } + + [Fact] + public void Config_Should_Map_DateTime_To_DateTimeOffset() + { + const string source = """ + using System; + using Atc.SourceGenerators.Annotations; + + namespace SourceNs + { + public class Item + { + public DateTime Timestamp { get; set; } + } + } + + namespace TargetNs + { + public class Item + { + public DateTimeOffset Timestamp { get; set; } + } + } + + namespace Mappings + { + [MappingConfiguration] + public static partial class ItemMappings + { + public static partial TargetNs.Item MapToItem(this SourceNs.Item source); + } + } + """; + + var (diagnostics, output) = GetGeneratedOutput(source); + + Assert.Empty(diagnostics); + Assert.Contains("new global::System.DateTimeOffset(source.Timestamp)", output, StringComparison.Ordinal); + } + + [Fact] + public void Config_Should_Map_Nullable_DateTimeOffset_To_Nullable_DateTime() + { + const string source = """ + using System; + using Atc.SourceGenerators.Annotations; + + #nullable enable + + namespace SourceNs + { + public class Item + { + public DateTimeOffset? Timestamp { get; set; } + } + } + + namespace TargetNs + { + public class Item + { + public DateTime? Timestamp { get; set; } + } + } + + namespace Mappings + { + [MappingConfiguration] + public static partial class ItemMappings + { + public static partial TargetNs.Item MapToItem(this SourceNs.Item source); + } + } + """; + + var (diagnostics, output) = GetGeneratedOutput(source); + + Assert.Empty(diagnostics); + Assert.Contains("source.Timestamp?.DateTime", output, StringComparison.Ordinal); + } + + [Fact] + public void Config_Should_Map_Nullable_DateTime_To_Nullable_DateTimeOffset() + { + const string source = """ + using System; + using Atc.SourceGenerators.Annotations; + + #nullable enable + + namespace SourceNs + { + public class Item + { + public DateTime? Timestamp { get; set; } + } + } + + namespace TargetNs + { + public class Item + { + public DateTimeOffset? Timestamp { get; set; } + } + } + + namespace Mappings + { + [MappingConfiguration] + public static partial class ItemMappings + { + public static partial TargetNs.Item MapToItem(this SourceNs.Item source); + } + } + """; + + var (diagnostics, output) = GetGeneratedOutput(source); + + Assert.Empty(diagnostics); + Assert.Contains("is { } __timestamp ? new global::System.DateTimeOffset(__timestamp) : null", output, StringComparison.Ordinal); + } } \ No newline at end of file