diff --git a/SourceGenerators/Mapper/MintPlayer.Mapper.Attributes/MapAsDictionaryAttribute.cs b/SourceGenerators/Mapper/MintPlayer.Mapper.Attributes/MapAsDictionaryAttribute.cs new file mode 100644 index 00000000..8131ed28 --- /dev/null +++ b/SourceGenerators/Mapper/MintPlayer.Mapper.Attributes/MapAsDictionaryAttribute.cs @@ -0,0 +1,9 @@ +namespace MintPlayer.Mapper.Attributes; + +/// +/// Indicates that this type should be treated as a dictionary when mapping. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] +public sealed class MapAsDictionaryAttribute : Attribute +{ +} diff --git a/SourceGenerators/Mapper/MintPlayer.Mapper/Generators/MapperGenerator.Producer.cs b/SourceGenerators/Mapper/MintPlayer.Mapper/Generators/MapperGenerator.Producer.cs index 6a9fb3ca..598c61c0 100644 --- a/SourceGenerators/Mapper/MintPlayer.Mapper/Generators/MapperGenerator.Producer.cs +++ b/SourceGenerators/Mapper/MintPlayer.Mapper/Generators/MapperGenerator.Producer.cs @@ -112,7 +112,7 @@ protected override void ProduceSource(IndentedTextWriter writer, CancellationTok foreach (var (source, destination) in type.MappedProperties) { - HandleProperty(writer, source, destination, EWriteType.Assignment); + HandleProperty(writer, source, destination, type.TypeToMap.SourceTypeHasIndexer, type.TypeToMap.DestinationTypeHasIndexer, EWriteType.Assignment); } writer.Indent--; @@ -134,7 +134,7 @@ protected override void ProduceSource(IndentedTextWriter writer, CancellationTok foreach (var (source, destination) in type.MappedProperties) { - HandleProperty(writer, source, destination, EWriteType.Initializer); + HandleProperty(writer, source, destination, type.TypeToMap.SourceTypeHasIndexer, type.TypeToMap.DestinationTypeHasIndexer, EWriteType.Initializer); } writer.Indent--; @@ -155,7 +155,7 @@ protected override void ProduceSource(IndentedTextWriter writer, CancellationTok foreach (var (source, destination) in type.MappedProperties) { - HandleProperty(writer, destination, source, EWriteType.Assignment); + HandleProperty(writer, destination, source, type.TypeToMap.DestinationTypeHasIndexer, type.TypeToMap.SourceTypeHasIndexer, EWriteType.Assignment); } writer.Indent--; @@ -178,7 +178,7 @@ protected override void ProduceSource(IndentedTextWriter writer, CancellationTok // TODO: add properties foreach (var (source, destination) in type.MappedProperties) { - HandleProperty(writer, destination, source, EWriteType.Initializer); + HandleProperty(writer, destination, source, type.TypeToMap.DestinationTypeHasIndexer, type.TypeToMap.SourceTypeHasIndexer, EWriteType.Initializer); } writer.Indent--; @@ -218,21 +218,21 @@ protected override void ProduceSource(IndentedTextWriter writer, CancellationTok } // Helper methods for type checks - private static bool IsArrayType(string type) - => type.EndsWith("[]"); + private static bool IsArrayType(string? type) + => type is not null && type.EndsWith("[]"); - private static bool IsListType(string type) - => type.StartsWith("global::System.Collections.Generic.List<") || type.StartsWith("List<"); + private static bool IsListType(string? type) + => type is not null && (type.StartsWith("global::System.Collections.Generic.List<") || type.StartsWith("List<")); - private static bool IsCollectionType(string type) - => type.StartsWith("global::System.Collections.Generic.ICollection<") || type.StartsWith("ICollection<"); + private static bool IsCollectionType(string? type) + => type is not null && (type.StartsWith("global::System.Collections.Generic.ICollection<") || type.StartsWith("ICollection<")); - private static bool IsNullableType(string type) - => type.EndsWith("?") || type.StartsWith("System.Nullable<"); + private static bool IsNullableType(string? type) + => type is not null && (type.EndsWith("?") || type.StartsWith("System.Nullable<")); - private static bool IsPrimitiveOrString(string type) + private static bool IsPrimitiveOrString(string? type) { - switch (type.WithoutGlobal()) + switch (type?.WithoutGlobal()) { case "string": case "System.String": @@ -292,15 +292,27 @@ private static string GetElementType(string collectionType) return collectionType; } - private static void HandleProperty(IndentedTextWriter writer, PropertyDeclaration source, PropertyDeclaration destination, EWriteType writeType) + private static void HandleProperty(IndentedTextWriter writer, PropertyDeclaration? source, PropertyDeclaration? destination, bool sourceWithIndexer, bool destinationWithIndexer, EWriteType writeType) { - var prefix = writeType switch + var destProp = destination is { } ? destination.Alias ?? destination.PropertyName : null; + var sourceProp = source is { } ? source.Alias ?? source.PropertyName : null; + + destProp ??= sourceProp; + sourceProp ??= destProp; + + var sourceValueAccessor = (sourceWithIndexer, writeType) switch { - EWriteType.Initializer => $"{source.PropertyName} = ", - EWriteType.Assignment => $"output.{source.PropertyName} = ", + (true, EWriteType.Initializer) => $"[{destProp}] = ", + (true, EWriteType.Assignment) => $"output[{destProp}] = ", + (false, EWriteType.Initializer) => $"{destProp} = ", + (false, EWriteType.Assignment) => $"output.{destProp} = ", _ => throw new NotImplementedException(), }; + var destinationValueAccessor = destinationWithIndexer + ? $""""input["{destProp}"]"""" + : $"input.{destProp}"; + var suffix = writeType switch { EWriteType.Initializer => ",", @@ -309,66 +321,69 @@ private static void HandleProperty(IndentedTextWriter writer, PropertyDeclaratio }; // Handle primitive types - if (source.IsPrimitive && destination.IsPrimitive) + if ((source is { IsPrimitive: true } && destination is { IsPrimitive: true }) || + (source is { HasStringIndexer: true } && destination is { IsPrimitive: true }) || + (source is { IsPrimitive: true } && destination is { HasStringIndexer: true })) { if (source.PropertyType != destination.PropertyType) - writer.WriteLine($"{prefix}ConvertProperty<{destination.PropertyType}, {source.PropertyType}>(input.{destination.PropertyName}){suffix}"); + writer.WriteLine($"{sourceValueAccessor}ConvertProperty<{destination.PropertyType}, {source.PropertyType}>({destinationValueAccessor}){suffix}"); else if (source.StateName != null && destination.StateName != null) - writer.WriteLine($"""{prefix}ConvertProperty<{destination.PropertyType}, {source.PropertyType}>(input.{destination.PropertyName}, {source.StateName}, {destination.StateName}){suffix}"""); + writer.WriteLine($"""{sourceValueAccessor}ConvertProperty<{destination.PropertyType}, {source.PropertyType}>({destinationValueAccessor}, {source.StateName}, {destination.StateName}){suffix}"""); else - writer.WriteLine($"{prefix}input.{destination.PropertyName}{suffix}"); + writer.WriteLine($"{sourceValueAccessor}{destinationValueAccessor}{suffix}"); } // Handle arrays - else if (IsArrayType(source.PropertyType) && IsArrayType(destination.PropertyType)) + else if (source is not null && IsArrayType(source.PropertyType) && IsArrayType(destination?.PropertyType)) { var elementType = GetElementType(source.PropertyType); if (IsPrimitiveOrString(elementType)) { - writer.WriteLine($"{prefix}input.{destination.PropertyName} == null ? null : input.{destination.PropertyName}.ToArray(){suffix}"); + writer.WriteLine($"{sourceValueAccessor}{destinationValueAccessor} == null ? null : {destinationValueAccessor}.ToArray(){suffix}"); } else { var shortName = elementType.WithoutGlobal().Split('.').Last(); - writer.WriteLine($"{prefix}input.{destination.PropertyName} == null ? null : input.{destination.PropertyName}.Select(x => x.MapTo{shortName}()).ToArray(){suffix}"); + writer.WriteLine($"{sourceValueAccessor}{destinationValueAccessor} == null ? null : {destinationValueAccessor}.Select(x => x.MapTo{shortName}()).ToArray(){suffix}"); } } // Handle List - else if (IsListType(source.PropertyType) && IsListType(destination.PropertyType)) + else if (source is not null && IsListType(source.PropertyType) && IsListType(destination?.PropertyType)) { var elementType = GetElementType(source.PropertyType); if (IsPrimitiveOrString(elementType)) { - writer.WriteLine($"{prefix}input.{destination.PropertyName} == null ? null : input.{destination.PropertyName}.ToList(){suffix}"); + writer.WriteLine($"{sourceValueAccessor}{destinationValueAccessor} == null ? null : {destinationValueAccessor}.ToList(){suffix}"); } else { var shortName = elementType.WithoutGlobal().Split('.').Last(); - writer.WriteLine($"{prefix}input.{destination.PropertyName} == null ? null : input.{destination.PropertyName}.Select(x => x.MapTo{shortName}()).ToList(){suffix}"); + writer.WriteLine($"{sourceValueAccessor}{destinationValueAccessor} == null ? null : {destinationValueAccessor}.Select(x => x.MapTo{shortName}()).ToList(){suffix}"); } } // Handle ICollection - else if (IsCollectionType(source.PropertyType) && IsCollectionType(destination.PropertyType)) + else if (source is not null && IsCollectionType(source.PropertyType) && IsCollectionType(destination?.PropertyType)) { var elementType = GetElementType(source.PropertyType); if (IsPrimitiveOrString(elementType)) { - writer.WriteLine($"{prefix}input.{destination.PropertyName} == null ? null : input.{destination.PropertyName}.ToList(){suffix}"); + writer.WriteLine($"{sourceValueAccessor}{destinationValueAccessor} == null ? null : {destinationValueAccessor}.ToList(){suffix}"); } else { var shortName = elementType.WithoutGlobal().Split('.').Last(); - writer.WriteLine($"{prefix}input.{destination.PropertyName} == null ? null : input.{destination.PropertyName}.Select(x => x.MapTo{shortName}()).ToList(){suffix}"); + writer.WriteLine($"{sourceValueAccessor}{destinationValueAccessor} == null ? null : {destinationValueAccessor}.Select(x => x.MapTo{shortName}()).ToList(){suffix}"); } } // Handle nullable reference types - else if (IsNullableType(source.PropertyType) && IsNullableType(destination.PropertyType)) + else if (source is not null && IsNullableType(source.PropertyType) && IsNullableType(destination?.PropertyType)) { - writer.WriteLine($"{prefix}input.{destination.PropertyName} == null ? null : input.{destination.PropertyName}.MapTo{source.PropertyTypeName}(){suffix}"); + writer.WriteLine($"{sourceValueAccessor}{destinationValueAccessor} == null ? null : {destinationValueAccessor}.MapTo{source.PropertyTypeName}(){suffix}"); } // Handle complex types else { - writer.WriteLine($"{prefix}input.{destination.PropertyName}.MapTo{source.PropertyTypeName}(){suffix}"); + var type = source?.PropertyTypeName ?? destination?.PropertyTypeName ?? throw new InvalidOperationException("Both source and destination are null"); + writer.WriteLine($"{sourceValueAccessor}{destinationValueAccessor}.MapTo{type}(){suffix}"); } } } diff --git a/SourceGenerators/Mapper/MintPlayer.Mapper/Generators/MapperGenerator.cs b/SourceGenerators/Mapper/MintPlayer.Mapper/Generators/MapperGenerator.cs index b0046455..6c14dbce 100644 --- a/SourceGenerators/Mapper/MintPlayer.Mapper/Generators/MapperGenerator.cs +++ b/SourceGenerators/Mapper/MintPlayer.Mapper/Generators/MapperGenerator.cs @@ -1,6 +1,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using MintPlayer.Mapper.Models; using MintPlayer.SourceGenerators.Tools; using MintPlayer.SourceGenerators.Tools.Extensions; @@ -31,7 +32,7 @@ public override void Initialize(IncrementalGeneratorInitializationContext contex //var destTypeMethodName = destAttr?.ConstructorArguments.ElementAtOrDefault(1); var mappingMethodName = destType1.Name.EnsureStartsWith("MapTo"); var declaredMethodName = sourceType.Name.EnsureStartsWith("MapTo"); - return new Models.TypeToMap + var res = new Models.TypeToMap { DestinationNamespace = sourceType.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)), DeclaredType = sourceType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), @@ -44,10 +45,13 @@ public override void Initialize(IncrementalGeneratorInitializationContext contex AppliedOn = Models.EAppliedOn.Assembly, HasError = false, Location = attr1.ApplicationSyntaxReference?.GetSyntax(ct)?.GetLocation() ?? Location.None, + SourceTypeHasIndexer = sourceType.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == "MintPlayer.Mapper.Attributes.MapAsDictionaryAttribute"), + DestinationTypeHasIndexer = destType1.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == "MintPlayer.Mapper.Attributes.MapAsDictionaryAttribute"), DeclaredProperties = ProcessProperties(sourceType).ToArray(), MappingProperties = ProcessProperties(destType1).ToArray(), }; + return res; } else { @@ -74,7 +78,7 @@ public override void Initialize(IncrementalGeneratorInitializationContext contex var destAttr = destType2.GetAttributes().FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == "MintPlayer.Mapper.Attributes.GenerateMapperAttribute"); var destTypeMethodName = destAttr?.ConstructorArguments.ElementAtOrDefault(1); var mappingMethodName = destTypeMethodName is null ? destType2.Name.EnsureStartsWith("MapTo") : CreateMethodName((TypedConstant)destTypeMethodName, destType2); - return new Models.TypeToMap + var res = new Models.TypeToMap { DestinationNamespace = typeSymbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)), DeclaredType = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), @@ -87,10 +91,13 @@ public override void Initialize(IncrementalGeneratorInitializationContext contex AppliedOn = Models.EAppliedOn.Class, HasError = false, Location = attr2.ApplicationSyntaxReference?.GetSyntax(ct)?.GetLocation() ?? Location.None, + SourceTypeHasIndexer = typeSymbol.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == "MintPlayer.Mapper.Attributes.MapAsDictionaryAttribute"), + DestinationTypeHasIndexer = destType2.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == "MintPlayer.Mapper.Attributes.MapAsDictionaryAttribute"), DeclaredProperties = ProcessProperties(typeSymbol).ToArray(), MappingProperties = ProcessProperties(destType2).ToArray(), }; + return res; } else { @@ -155,9 +162,26 @@ public override void Initialize(IncrementalGeneratorInitializationContext contex .Select(static (i, ct) => new Models.TypeWithMappedProperties { TypeToMap = i, - MappedProperties = i.DeclaredProperties - .Select(dp => (Source: dp, Destination: i.MappingProperties.FirstOrDefault(mp => mp.Alias == dp.Alias))) - .Where(p => p.Source is { IsStatic: false } && p.Destination is { IsStatic: false }) + //MappedProperties = i.DeclaredProperties + // .Select(dp => (Source: dp, Destination: i.MappingProperties.FirstOrDefault(mp => mp.Alias == dp.Alias))) + // .Where(p => p.Source is { IsStatic: false } && p.Destination is { IsStatic: false }) + + MappedProperties = (i.SourceTypeHasIndexer, i.DestinationTypeHasIndexer) switch + { + (true, true) => [], + (true, false) => i.MappingProperties + .Where(mp => !mp.IsStatic) + .Select(mp => (Source: (PropertyDeclaration?)null, Destination: mp.AsNullable())) + .Where(p => p.Destination is { IsStatic: false }), + (false, true) => i.DeclaredProperties + .Where(dp => !dp.IsStatic) + .Select(dp => (Source: dp.AsNullable(), Destination: (PropertyDeclaration?)null)) + .Where(p => p.Source is { IsStatic: false }), + (false, false) => i.DeclaredProperties + .Where(dp => !dp.IsStatic) + .Select(dp => (Source: dp.AsNullable(), Destination: (PropertyDeclaration?)i.MappingProperties.FirstOrDefault(mp => mp.Alias == dp.Alias))) + .Where(p => p.Source is { IsStatic: false } && p.Destination is { IsStatic: false }), + } }) .WithComparer() .Collect(); @@ -277,6 +301,8 @@ private static string CreateMethodName(TypedConstant preferred, INamedTypeSymbol //IsAbstract = p.IsAbstract, //IsOverride = p.IsOverride, IsPrimitive = p.Type.IsValueType || p.Type.SpecialType == SpecialType.System_String, + HasStringIndexer = p.Type is INamedTypeSymbol namedType2 && namedType2.GetMembers().OfType().Any(pi => pi.IsIndexer && pi.Parameters.Length == 1 && pi.Parameters[0].Type.SpecialType == SpecialType.System_String), + //ShouldMapAsDictionary = p.Type.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == "MintPlayer.Mapper.Attributes.MapAsDictionaryAttribute"), }); } diff --git a/SourceGenerators/Mapper/MintPlayer.Mapper/Models/PropertyDeclaration.cs b/SourceGenerators/Mapper/MintPlayer.Mapper/Models/PropertyDeclaration.cs index bc37b746..14a89963 100644 --- a/SourceGenerators/Mapper/MintPlayer.Mapper/Models/PropertyDeclaration.cs +++ b/SourceGenerators/Mapper/MintPlayer.Mapper/Models/PropertyDeclaration.cs @@ -12,6 +12,8 @@ public partial class PropertyDeclaration public int? StateName { get; set; } public bool IsStatic { get; set; } public bool IsPrimitive { get; set; } + public bool HasStringIndexer { get; set; } + //public bool ShouldMapAsDictionary { get; set; } public override string ToString() => $"{PropertyName} ({PropertyType})"; } diff --git a/SourceGenerators/Mapper/MintPlayer.Mapper/Models/TypeToMap.cs b/SourceGenerators/Mapper/MintPlayer.Mapper/Models/TypeToMap.cs index 7c27be72..e24eefc3 100644 --- a/SourceGenerators/Mapper/MintPlayer.Mapper/Models/TypeToMap.cs +++ b/SourceGenerators/Mapper/MintPlayer.Mapper/Models/TypeToMap.cs @@ -37,6 +37,16 @@ public partial class TypeToMap /// public bool AreBothDecorated { get; set; } + /// + /// Indicates that the source type has an indexer + /// + public bool SourceTypeHasIndexer { get; set; } + + /// + /// Indicates that the destination type has an indexer + /// + public bool DestinationTypeHasIndexer { get; set; } + public EAppliedOn AppliedOn { get; set; } public bool HasError { get; set; } diff --git a/SourceGenerators/Mapper/MintPlayer.Mapper/Models/TypeWithMappedProperties.cs b/SourceGenerators/Mapper/MintPlayer.Mapper/Models/TypeWithMappedProperties.cs index 6cb9a5c1..7cffeab9 100644 --- a/SourceGenerators/Mapper/MintPlayer.Mapper/Models/TypeWithMappedProperties.cs +++ b/SourceGenerators/Mapper/MintPlayer.Mapper/Models/TypeWithMappedProperties.cs @@ -6,5 +6,5 @@ namespace MintPlayer.Mapper.Models; public partial class TypeWithMappedProperties { public TypeToMap TypeToMap { get; set; } = null!; - public IEnumerable<(PropertyDeclaration Source, PropertyDeclaration Destination)> MappedProperties { get; set; } = []; + public IEnumerable<(PropertyDeclaration? Source, PropertyDeclaration? Destination)> MappedProperties { get; set; } = []; } \ No newline at end of file diff --git a/SourceGenerators/MintPlayer.SourceGenerators.Tools/Extensions/EnumerableExtensions.cs b/SourceGenerators/MintPlayer.SourceGenerators.Tools/Extensions/EnumerableExtensions.cs index 79075ad2..315dd20f 100644 --- a/SourceGenerators/MintPlayer.SourceGenerators.Tools/Extensions/EnumerableExtensions.cs +++ b/SourceGenerators/MintPlayer.SourceGenerators.Tools/Extensions/EnumerableExtensions.cs @@ -10,4 +10,29 @@ public static IEnumerable NotNull(this IEnumerable source) where T : c { return source.Where(item => item is not null).Cast(); } + + /// + /// Converts a sequence of value types to a sequence of nullable value types of the same underlying type. + /// + /// This method enables LINQ queries and other operations that require nullable value types when + /// working with sequences of non-nullable value types. The order and number of elements in the returned sequence + /// are the same as in the source sequence. + /// The value type of the elements in the source sequence. + /// The sequence of value type elements to convert. Cannot be null. + /// An IEnumerable containing the elements of the source sequence, each cast to a nullable value type. + public static IEnumerable AsNullable(this IEnumerable source) where T : struct + { + return source.Cast(); + } + + /// + /// Converts a reference type value to its nullable equivalent. + /// + /// The reference type to convert to a nullable type. + /// The reference type value to convert. Can be null. + /// A nullable value of type T that represents the original value, or null if the source is null. + public static T? AsNullable(this T source) where T : class + { + return (T?)source; + } } diff --git a/SourceGenerators/TestProjects/MapperDebugging/Program.cs b/SourceGenerators/TestProjects/MapperDebugging/Program.cs index d9e54ea8..16bc8fcf 100644 --- a/SourceGenerators/TestProjects/MapperDebugging/Program.cs +++ b/SourceGenerators/TestProjects/MapperDebugging/Program.cs @@ -128,17 +128,22 @@ public class PersonDto [GenerateMapper(typeof(AddressDto))] public class Address { + [MapperAlias("Straatnaam")] public string? Street { get; set; } + [MapperAlias("Stad")] public string? City { get; set; } } +[MapAsDictionary] public class AddressDto { - [MapperAlias(nameof(Address.Street))] - public string? Straatnaam { get; set; } + private readonly Dictionary data = new(); - [MapperAlias(nameof(Address.City))] - public string? Stad { get; set; } + public object? this[string key] + { + get => data.TryGetValue(key, out object? value) ? value : null; + set => data[key] = value; + } } public class ContactInfo