Add MapProperty attribute. Makes it possible to map to a different property.
This commit is contained in:
parent
f7a755332e
commit
acd98e72ff
|
@ -25,7 +25,8 @@ namespace MapTo
|
|||
.AddSource(ref context, MapFromAttributeSource.Generate(options))
|
||||
.AddSource(ref context, IgnorePropertyAttributeSource.Generate(options))
|
||||
.AddSource(ref context, ITypeConverterSource.Generate(options))
|
||||
.AddSource(ref context, MapTypeConverterAttributeSource.Generate(options));
|
||||
.AddSource(ref context, MapTypeConverterAttributeSource.Generate(options))
|
||||
.AddSource(ref context, MapPropertyAttributeSource.Generate(options));
|
||||
|
||||
if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateClasses.Any())
|
||||
{
|
||||
|
|
|
@ -24,19 +24,24 @@ namespace MapTo
|
|||
|
||||
TypeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataName(ITypeConverterSource.FullyQualifiedName)
|
||||
?? throw new TypeLoadException($"Unable to find '{ITypeConverterSource.FullyQualifiedName}' type.");
|
||||
|
||||
MapPropertyAttributeTypeSymbol = compilation.GetTypeByMetadataName(MapPropertyAttributeSource.FullyQualifiedName)
|
||||
?? throw new TypeLoadException($"Unable to find '{MapPropertyAttributeSource.FullyQualifiedName}' type.");
|
||||
}
|
||||
|
||||
private Compilation Compilation { get; }
|
||||
|
||||
public INamedTypeSymbol MapTypeConverterAttributeTypeSymbol { get; }
|
||||
|
||||
public INamedTypeSymbol TypeConverterInterfaceTypeSymbol { get; }
|
||||
|
||||
public MappingModel? Model { get; private set; }
|
||||
|
||||
public ImmutableArray<Diagnostic> Diagnostics { get; private set; }
|
||||
|
||||
public INamedTypeSymbol IgnorePropertyAttributeTypeSymbol { get; }
|
||||
|
||||
public INamedTypeSymbol MapTypeConverterAttributeTypeSymbol { get; }
|
||||
|
||||
public INamedTypeSymbol TypeConverterInterfaceTypeSymbol { get; }
|
||||
|
||||
public INamedTypeSymbol MapPropertyAttributeTypeSymbol { get; }
|
||||
|
||||
internal static MappingContext Create(Compilation compilation, ClassDeclarationSyntax classSyntax, SourceGenerationOptions sourceGenerationOptions)
|
||||
{
|
||||
|
@ -102,7 +107,7 @@ namespace MapTo
|
|||
|
||||
foreach (var property in classProperties)
|
||||
{
|
||||
var sourceProperty = sourceProperties.FindProperty(property);
|
||||
var sourceProperty = FindSourceProperty(context, sourceProperties, property);
|
||||
if (sourceProperty is null)
|
||||
{
|
||||
continue;
|
||||
|
@ -138,12 +143,23 @@ namespace MapTo
|
|||
converterParameters.AddRange(GetTypeConverterParameters(typeConverterAttribute));
|
||||
}
|
||||
|
||||
mappedProperties.Add(new MappedProperty(property.Name, converterFullyQualifiedName, converterParameters.ToImmutableArray()));
|
||||
mappedProperties.Add(new MappedProperty(property.Name, converterFullyQualifiedName, converterParameters.ToImmutableArray(), sourceProperty.Name));
|
||||
}
|
||||
|
||||
return mappedProperties.ToImmutableArray();
|
||||
}
|
||||
|
||||
private static IPropertySymbol? FindSourceProperty(MappingContext context, IEnumerable<IPropertySymbol> sourceProperties, IPropertySymbol property)
|
||||
{
|
||||
var propertyName = property
|
||||
.GetAttribute(context.MapPropertyAttributeTypeSymbol)
|
||||
?.NamedArguments
|
||||
.SingleOrDefault(a => a.Key == MapPropertyAttributeSource.SourcePropertyNamePropertyName)
|
||||
.Value.Value as string ?? property.Name;
|
||||
|
||||
return sourceProperties.SingleOrDefault(p => p.Name == propertyName);
|
||||
}
|
||||
|
||||
private static INamedTypeSymbol? GetTypeConverterBaseInterface(MappingContext context, ITypeSymbol converterTypeSymbol, IPropertySymbol property, IPropertySymbol sourceProperty)
|
||||
{
|
||||
return converterTypeSymbol.AllInterfaces
|
||||
|
|
|
@ -7,7 +7,11 @@ namespace MapTo
|
|||
{
|
||||
internal record SourceCode(string Text, string HintName);
|
||||
|
||||
internal record MappedProperty(string Name, string? TypeConverter, ImmutableArray<string> TypeConverterParameters);
|
||||
internal record MappedProperty(
|
||||
string Name,
|
||||
string? TypeConverter,
|
||||
ImmutableArray<string> TypeConverterParameters,
|
||||
string SourcePropertyName);
|
||||
|
||||
internal record MappingModel (
|
||||
SourceGenerationOptions Options,
|
||||
|
|
|
@ -70,7 +70,7 @@ namespace MapTo.Sources
|
|||
{
|
||||
if (property.TypeConverter is null)
|
||||
{
|
||||
builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.Name};");
|
||||
builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.SourcePropertyName};");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
using static MapTo.Sources.Constants;
|
||||
|
||||
namespace MapTo.Sources
|
||||
{
|
||||
internal static class MapPropertyAttributeSource
|
||||
{
|
||||
internal const string AttributeName = "MapProperty";
|
||||
internal const string AttributeClassName = AttributeName + "Attribute";
|
||||
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
|
||||
internal const string SourcePropertyNamePropertyName = "SourcePropertyName";
|
||||
|
||||
internal static SourceCode Generate(SourceGenerationOptions options)
|
||||
{
|
||||
using var builder = new SourceBuilder()
|
||||
.WriteLine(GeneratedFilesHeader)
|
||||
.WriteNullableContextOptionIf(options.SupportNullableReferenceTypes)
|
||||
.WriteLine()
|
||||
.WriteLine("using System;")
|
||||
.WriteLine()
|
||||
.WriteLine($"namespace {RootNamespace}")
|
||||
.WriteOpeningBracket();
|
||||
|
||||
if (options.GenerateXmlDocument)
|
||||
{
|
||||
builder
|
||||
.WriteLine("/// <summary>")
|
||||
.WriteLine("/// Specifies the mapping behavior of annotated property.")
|
||||
.WriteLine("/// </summary>")
|
||||
.WriteLine("/// <remarks>")
|
||||
.WriteLine($"/// {AttributeClassName} has a number of uses:")
|
||||
.WriteLine("/// <list type=\"bullet\">")
|
||||
.WriteLine("/// <item><description>By default properties with same name will get mapped. This attribute allows the names to be different.</description></item>")
|
||||
.WriteLine("/// <item><description>Indicates that a property should be mapped when member serialization is set to opt-in.</description></item>")
|
||||
.WriteLine("/// </list>")
|
||||
.WriteLine("/// </remarks>");
|
||||
}
|
||||
|
||||
builder
|
||||
.WriteLine("[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]")
|
||||
.WriteLine($"public sealed class {AttributeClassName} : Attribute")
|
||||
.WriteOpeningBracket();
|
||||
|
||||
if (options.GenerateXmlDocument)
|
||||
{
|
||||
builder
|
||||
.WriteLine("/// <summary>")
|
||||
.WriteLine("/// Gets or sets the property name of the object to mapping from.")
|
||||
.WriteLine("/// </summary>");
|
||||
}
|
||||
|
||||
builder
|
||||
.WriteLine($"public string{options.NullableReferenceSyntax} {SourcePropertyNamePropertyName} {{ get; set; }}")
|
||||
.WriteClosingBracket() // class
|
||||
.WriteClosingBracket(); // namespace
|
||||
|
||||
|
||||
return new(builder.ToString(), $"{AttributeClassName}.g.cs");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -384,7 +384,13 @@ namespace Test
|
|||
{
|
||||
// Arrange
|
||||
const string source = "";
|
||||
var expectedTypes = new[] { IgnorePropertyAttributeSource.AttributeName, MapFromAttributeSource.AttributeName, ITypeConverterSource.InterfaceName };
|
||||
var expectedTypes = new[]
|
||||
{
|
||||
IgnorePropertyAttributeSource.AttributeName,
|
||||
MapFromAttributeSource.AttributeName,
|
||||
ITypeConverterSource.InterfaceName,
|
||||
MapPropertyAttributeSource.AttributeName
|
||||
};
|
||||
|
||||
// Act
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
|
||||
|
@ -542,7 +548,7 @@ namespace MapTo
|
|||
diagnostics.ShouldBeSuccessful();
|
||||
compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void VerifyMapTypeConverterAttribute()
|
||||
{
|
||||
|
@ -578,7 +584,7 @@ namespace MapTo
|
|||
diagnostics.ShouldBeSuccessful();
|
||||
compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void VerifyMapTypeConverterAttributeWithNullableOptionOn()
|
||||
{
|
||||
|
@ -759,6 +765,73 @@ namespace Test
|
|||
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedSyntax);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(NullableContextOptions.Disable)]
|
||||
[InlineData(NullableContextOptions.Enable)]
|
||||
public void VerifyMapPropertyAttribute(NullableContextOptions nullableContextOptions)
|
||||
{
|
||||
// Arrange
|
||||
const string source = "";
|
||||
var nullableSyntax = nullableContextOptions == NullableContextOptions.Enable ? "?" : string.Empty;
|
||||
var expectedInterface = $@"
|
||||
{Constants.GeneratedFilesHeader}
|
||||
{(nullableContextOptions == NullableContextOptions.Enable ? $"#nullable enable{Environment.NewLine}": string.Empty)}
|
||||
using System;
|
||||
|
||||
namespace MapTo
|
||||
{{
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
|
||||
public sealed class MapPropertyAttribute : Attribute
|
||||
{{
|
||||
public string{nullableSyntax} SourcePropertyName {{ get; set; }}
|
||||
}}
|
||||
}}
|
||||
".Trim();
|
||||
|
||||
// Act
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions, nullableContextOptions: nullableContextOptions);
|
||||
|
||||
// Assert
|
||||
diagnostics.ShouldBeSuccessful();
|
||||
compilation.SyntaxTrees.ShouldContainSource(MapPropertyAttributeSource.AttributeName, expectedInterface);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_MapPropertyFound_Should_UseItToMapToSourceProperty()
|
||||
{
|
||||
// Arrange
|
||||
var source = GetSourceText(new SourceGeneratorOptions(
|
||||
true,
|
||||
PropertyBuilder: builder =>
|
||||
{
|
||||
builder
|
||||
.PadLeft(Indent2).AppendLine("[MapProperty(SourcePropertyName = nameof(Baz.Prop3))]")
|
||||
.PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }");
|
||||
},
|
||||
SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }")));
|
||||
|
||||
var expectedResult = @"
|
||||
partial class Foo
|
||||
{
|
||||
public Foo(Test.Models.Baz baz)
|
||||
{
|
||||
if (baz == null) throw new ArgumentNullException(nameof(baz));
|
||||
|
||||
Prop1 = baz.Prop1;
|
||||
Prop2 = baz.Prop2;
|
||||
Prop3 = baz.Prop3;
|
||||
Prop4 = baz.Prop3;
|
||||
}
|
||||
".Trim();
|
||||
|
||||
// Act
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
|
||||
|
||||
// Assert
|
||||
diagnostics.ShouldBeSuccessful();
|
||||
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult);
|
||||
}
|
||||
|
||||
private static PropertyDeclarationSyntax GetPropertyDeclarationSyntax(SyntaxTree syntaxTree, string targetPropertyName, string targetClass = "Foo")
|
||||
{
|
||||
return syntaxTree.GetRoot()
|
||||
|
|
Loading…
Reference in New Issue