Add parameter to ITypeConverter.
This commit is contained in:
parent
2ef7ded450
commit
13c5279e15
|
@ -26,7 +26,7 @@ namespace MapTo
|
|||
Create($"{ErrorId}031", property.Locations.FirstOrDefault(), "Type Mismatch", $"Cannot create a map for '{property.ToDisplayString()}' property because source and destination types are not implicitly convertible. Consider using '{MapTypeConverterAttributeSource.FullyQualifiedName}' to provide a type converter or ignore the property using '{IgnorePropertyAttributeSource.FullyQualifiedName}'.");
|
||||
|
||||
internal static Diagnostic InvalidTypeConverterGenericTypesError(IPropertySymbol property, IPropertySymbol sourceProperty) =>
|
||||
Create($"{ErrorId}032", property.Locations.FirstOrDefault(), "Type Mismatch", $"Cannot map '{property.ToDisplayString()}' property because the annotated converter does not implement '{RootNamespace}.{TypeConverterSource.InterfaceName}<{sourceProperty.Type.ToDisplayString()}, {property.Type.ToDisplayString()}>'.");
|
||||
Create($"{ErrorId}032", property.Locations.FirstOrDefault(), "Type Mismatch", $"Cannot map '{property.ToDisplayString()}' property because the annotated converter does not implement '{RootNamespace}.{ITypeConverterSource.InterfaceName}<{sourceProperty.Type.ToDisplayString()}, {property.Type.ToDisplayString()}>'.");
|
||||
|
||||
internal static Diagnostic ConfigurationParseError(string error) =>
|
||||
Create($"{ErrorId}040", Location.None, "Incorrect Configuration", error);
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
namespace MapTo.Extensions
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MapTo.Extensions
|
||||
{
|
||||
internal static class StringExtensions
|
||||
{
|
||||
public static string ToCamelCase(this string value) => string.IsNullOrWhiteSpace(value) ? value : $"{char.ToLower(value[0])}{value.Substring(1)}";
|
||||
|
||||
public static string ToSourceCodeString(this object? value) => value switch
|
||||
{
|
||||
null => "null",
|
||||
string strValue => $"\"{strValue}\"",
|
||||
char charValue => $"'{charValue}'",
|
||||
_ => value.ToString()
|
||||
};
|
||||
}
|
||||
}
|
|
@ -24,7 +24,7 @@ namespace MapTo
|
|||
var compilation = context.Compilation
|
||||
.AddSource(ref context, MapFromAttributeSource.Generate(options))
|
||||
.AddSource(ref context, IgnorePropertyAttributeSource.Generate(options))
|
||||
.AddSource(ref context, TypeConverterSource.Generate(options))
|
||||
.AddSource(ref context, ITypeConverterSource.Generate(options))
|
||||
.AddSource(ref context, MapTypeConverterAttributeSource.Generate(options));
|
||||
|
||||
if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateClasses.Any())
|
||||
|
|
|
@ -24,8 +24,8 @@ namespace MapTo
|
|||
MapTypeConverterAttributeTypeSymbol = compilation.GetTypeByMetadataName(MapTypeConverterAttributeSource.FullyQualifiedName)
|
||||
?? throw new TypeLoadException($"Unable to find '{MapTypeConverterAttributeSource.FullyQualifiedName}' type.");
|
||||
|
||||
TypeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataName(TypeConverterSource.FullyQualifiedName)
|
||||
?? throw new TypeLoadException($"Unable to find '{TypeConverterSource.FullyQualifiedName}' type.");
|
||||
TypeConverterInterfaceTypeSymbol = compilation.GetTypeByMetadataName(ITypeConverterSource.FullyQualifiedName)
|
||||
?? throw new TypeLoadException($"Unable to find '{ITypeConverterSource.FullyQualifiedName}' type.");
|
||||
}
|
||||
|
||||
public INamedTypeSymbol MapTypeConverterAttributeTypeSymbol { get; }
|
||||
|
@ -114,9 +114,17 @@ namespace MapTo
|
|||
}
|
||||
|
||||
string? converterFullyQualifiedName = null;
|
||||
var converterParameters = new List<string>();
|
||||
if (!SymbolEqualityComparer.Default.Equals(property.Type, sourceProperty.Type) && !context.Compilation.HasImplicitConversion(sourceProperty.Type, property.Type))
|
||||
{
|
||||
var converterTypeSymbol = property.GetAttribute(context.MapTypeConverterAttributeTypeSymbol)?.ConstructorArguments.First().Value as INamedTypeSymbol;
|
||||
var typeConverterAttribute = property.GetAttribute(context.MapTypeConverterAttributeTypeSymbol);
|
||||
if (typeConverterAttribute is null)
|
||||
{
|
||||
context.ReportDiagnostic(DiagnosticProvider.NoMatchingPropertyTypeFoundError(property));
|
||||
continue;
|
||||
}
|
||||
|
||||
var converterTypeSymbol = typeConverterAttribute.ConstructorArguments.First().Value as INamedTypeSymbol;
|
||||
if (converterTypeSymbol is null)
|
||||
{
|
||||
context.ReportDiagnostic(DiagnosticProvider.NoMatchingPropertyTypeFoundError(property));
|
||||
|
@ -136,9 +144,15 @@ namespace MapTo
|
|||
}
|
||||
|
||||
converterFullyQualifiedName = converterTypeSymbol.ToDisplayString();
|
||||
|
||||
var converterParameter = typeConverterAttribute.ConstructorArguments.Skip(1).FirstOrDefault();
|
||||
if (!converterParameter.IsNull )
|
||||
{
|
||||
converterParameters.AddRange(converterParameter.Values.Where(v => v.Value is not null).Select(v => v.Value!.ToSourceCodeString()));
|
||||
}
|
||||
}
|
||||
|
||||
mappedProperties.Add(new MappedProperty(property.Name, converterFullyQualifiedName));
|
||||
mappedProperties.Add(new MappedProperty(property.Name, converterFullyQualifiedName, converterParameters.ToImmutableArray()));
|
||||
}
|
||||
|
||||
return mappedProperties.ToImmutableArray();
|
||||
|
|
|
@ -16,7 +16,7 @@ namespace MapTo
|
|||
);
|
||||
}
|
||||
|
||||
internal record MappedProperty(string Name, string? TypeConverter);
|
||||
internal record MappedProperty(string Name, string? TypeConverter, ImmutableArray<string> TypeConverterParameters);
|
||||
|
||||
internal record MappingModel (
|
||||
SourceGenerationOptions Options,
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
using Microsoft.CodeAnalysis;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using static MapTo.Sources.Constants;
|
||||
|
||||
namespace MapTo.Sources
|
||||
{
|
||||
internal class TypeConverterSource
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
internal static class ITypeConverterSource
|
||||
{
|
||||
internal const string InterfaceName = "ITypeConverter";
|
||||
internal const string FullyQualifiedName = RootNamespace + "." + InterfaceName + "`2";
|
||||
|
@ -34,14 +36,15 @@ namespace MapTo.Sources
|
|||
{
|
||||
builder
|
||||
.WriteLine("/// <summary>")
|
||||
.WriteLine("/// Converts the value of <paramref name=\"value\"/> object to <typeparamref name=\"TDestination\"/>.")
|
||||
.WriteLine("/// Converts the value of <paramref name=\"source\"/> object to <typeparamref name=\"TDestination\"/>.")
|
||||
.WriteLine("/// </summary>")
|
||||
.WriteLine("/// <param name=\"value\">The object to convert.</param>")
|
||||
.WriteLine("/// <param name=\"source\">The <see cref=\"TSource\"/> to convert.</param>")
|
||||
.WriteLine($"/// <param name=\"converterParameters\">The parameter list passed to the <see cref=\"{MapTypeConverterAttributeSource.AttributeClassName}\"/></param>")
|
||||
.WriteLine("/// <returns><typeparamref name=\"TDestination\"/> object.</returns>");
|
||||
}
|
||||
|
||||
builder
|
||||
.WriteLine("TDestination Convert(TSource source);")
|
||||
.WriteLine("TDestination Convert(TSource source, object[] converterParameters);")
|
||||
.WriteClosingBracket()
|
||||
.WriteClosingBracket();
|
||||
|
|
@ -66,13 +66,17 @@ namespace MapTo.Sources
|
|||
|
||||
foreach (var property in model.MappedProperties)
|
||||
{
|
||||
if (property.TypeConverter is not null)
|
||||
if (property.TypeConverter is null)
|
||||
{
|
||||
builder.WriteLine($"{property.Name} = new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.Name});");
|
||||
builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.Name};");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.WriteLine($"{property.Name} = {sourceClassParameterName}.{property.Name};");
|
||||
var parameters = property.TypeConverterParameters.IsEmpty
|
||||
? "null"
|
||||
: $"new object[] {{ {string.Join(", ", property.TypeConverterParameters)} }}";
|
||||
|
||||
builder.WriteLine($"{property.Name} = new {property.TypeConverter}().Convert({sourceClassParameterName}.{property.Name}, {parameters});");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ namespace MapTo.Sources
|
|||
internal const string AttributeClassName = AttributeName + "Attribute";
|
||||
internal const string FullyQualifiedName = RootNamespace + "." + AttributeClassName;
|
||||
internal const string ConverterPropertyName = "Converter";
|
||||
internal const string ConverterParametersPropertyName = "ConverterParameters";
|
||||
|
||||
internal static SourceCode Generate(SourceGenerationOptions options)
|
||||
{
|
||||
|
@ -37,13 +38,16 @@ namespace MapTo.Sources
|
|||
builder
|
||||
.WriteLine("/// <summary>")
|
||||
.WriteLine($"/// Initializes a new instance of <see cref=\"{AttributeClassName}\"/>.")
|
||||
.WriteLine("/// </summary>");
|
||||
.WriteLine("/// </summary>")
|
||||
.WriteLine($"/// <param name=\"converter\">The <see cref=\"{ITypeConverterSource.InterfaceName}{{TSource,TDestination}}\" /> to be used to convert the source type.</param>")
|
||||
.WriteLine("/// <param name=\"converterParameters\">The parameter list to pass to the <paramref name=\"converter\"/> during the type conversion.</param>");
|
||||
}
|
||||
|
||||
builder
|
||||
.WriteLine($"public {AttributeClassName}(Type converter)")
|
||||
.WriteLine($"public {AttributeClassName}(Type converter, object[] converterParameters = null)")
|
||||
.WriteOpeningBracket()
|
||||
.WriteLine($"{ConverterPropertyName} = converter;")
|
||||
.WriteLine($"{ConverterParametersPropertyName} = converterParameters;")
|
||||
.WriteClosingBracket()
|
||||
.WriteLine();
|
||||
|
||||
|
@ -51,12 +55,24 @@ namespace MapTo.Sources
|
|||
{
|
||||
builder
|
||||
.WriteLine("/// <summary>")
|
||||
.WriteLine($"/// Gets or sets the <see cref=\"{TypeConverterSource.InterfaceName}{{TSource,TDestination}}\" /> to be used to convert the source type.")
|
||||
.WriteLine($"/// Gets or sets the <see cref=\"{ITypeConverterSource.InterfaceName}{{TSource,TDestination}}\" /> to be used to convert the source type.")
|
||||
.WriteLine("/// </summary>");
|
||||
}
|
||||
|
||||
builder
|
||||
.WriteLine($"public Type {ConverterPropertyName} {{ get; }}")
|
||||
.WriteLine();
|
||||
|
||||
if (options.GenerateXmlDocument)
|
||||
{
|
||||
builder
|
||||
.WriteLine("/// <summary>")
|
||||
.WriteLine($"/// Gets the parameter list to pass to the <see cref=\"{ConverterPropertyName}\"/> during the type conversion.")
|
||||
.WriteLine("/// </summary>");
|
||||
}
|
||||
|
||||
builder
|
||||
.WriteLine($"public object[] {ConverterParametersPropertyName} {{ get; }}")
|
||||
.WriteClosingBracket()
|
||||
.WriteClosingBracket();
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using MapTo.Extensions;
|
||||
|
@ -10,7 +9,6 @@ using MapTo.Tests.Infrastructure;
|
|||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using static MapTo.Extensions.GeneratorExecutionContextExtensions;
|
||||
|
@ -65,7 +63,7 @@ namespace MapTo
|
|||
builder.AppendLine("// Test source code.");
|
||||
builder.AppendLine("//");
|
||||
builder.AppendLine();
|
||||
|
||||
|
||||
if (options.UseMapToNamespace)
|
||||
{
|
||||
builder.AppendFormat("using {0};", Constants.RootNamespace).AppendLine();
|
||||
|
@ -91,7 +89,7 @@ namespace MapTo
|
|||
|
||||
builder
|
||||
.PadLeft(Indent1)
|
||||
.AppendLine(options.UseMapToNamespace ? "[MapFrom(typeof(Baz))]": "[MapTo.MapFrom(typeof(Baz))]")
|
||||
.AppendLine(options.UseMapToNamespace ? "[MapFrom(typeof(Baz))]" : "[MapTo.MapFrom(typeof(Baz))]")
|
||||
.PadLeft(Indent1).Append("public partial class Foo")
|
||||
.AppendOpeningBracket(Indent1);
|
||||
|
||||
|
@ -182,10 +180,10 @@ namespace MapTo
|
|||
.PadLeft(Indent2).AppendLine("public string Prop4 { get; set; }");
|
||||
},
|
||||
SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public int Prop4 { get; set; }")));
|
||||
|
||||
|
||||
// Act
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
|
||||
|
||||
|
||||
// Assert
|
||||
var expectedError = DiagnosticProvider.NoMatchingPropertyTypeFoundError(GetSourcePropertySymbol("Prop4", compilation));
|
||||
|
||||
|
@ -221,7 +219,7 @@ namespace MapTo
|
|||
|
||||
// Act
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
|
||||
|
||||
|
||||
// Assert
|
||||
diagnostics.ShouldBeSuccessful();
|
||||
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult);
|
||||
|
@ -332,7 +330,7 @@ namespace Test
|
|||
// Assert
|
||||
var fooType = compilation.GetTypeByMetadataName("Test.Foo");
|
||||
fooType.ShouldNotBeNull();
|
||||
|
||||
|
||||
var bazType = compilation.GetTypeByMetadataName("Test.Baz");
|
||||
bazType.ShouldNotBeNull();
|
||||
|
||||
|
@ -384,7 +382,7 @@ namespace Test
|
|||
{
|
||||
// Arrange
|
||||
const string source = "";
|
||||
var expectedTypes = new[] { IgnorePropertyAttributeSource.AttributeName, MapFromAttributeSource.AttributeName, TypeConverterSource.InterfaceName };
|
||||
var expectedTypes = new[] { IgnorePropertyAttributeSource.AttributeName, MapFromAttributeSource.AttributeName, ITypeConverterSource.InterfaceName };
|
||||
|
||||
// Act
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
|
||||
|
@ -490,7 +488,7 @@ namespace Test
|
|||
diagnostics.ShouldBeSuccessful();
|
||||
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim());
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void VerifyTypeConverterInterface()
|
||||
{
|
||||
|
@ -503,7 +501,7 @@ namespace MapTo
|
|||
{{
|
||||
public interface ITypeConverter<in TSource, out TDestination>
|
||||
{{
|
||||
TDestination Convert(TSource source);
|
||||
TDestination Convert(TSource source, object[] converterParameters);
|
||||
}}
|
||||
}}
|
||||
".Trim();
|
||||
|
@ -513,9 +511,9 @@ namespace MapTo
|
|||
|
||||
// Assert
|
||||
diagnostics.ShouldBeSuccessful();
|
||||
compilation.SyntaxTrees.ShouldContainSource(TypeConverterSource.InterfaceName, expectedInterface);
|
||||
compilation.SyntaxTrees.ShouldContainSource(ITypeConverterSource.InterfaceName, expectedInterface);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void VerifyMapTypeConverterAttribute()
|
||||
{
|
||||
|
@ -530,12 +528,15 @@ namespace MapTo
|
|||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
|
||||
public sealed class MapTypeConverterAttribute : Attribute
|
||||
{{
|
||||
public MapTypeConverterAttribute(Type converter)
|
||||
public MapTypeConverterAttribute(Type converter, object[] converterParameters = null)
|
||||
{{
|
||||
Converter = converter;
|
||||
ConverterParameters = converterParameters;
|
||||
}}
|
||||
|
||||
public Type Converter {{ get; }}
|
||||
|
||||
public object[] ConverterParameters {{ get; }}
|
||||
}}
|
||||
}}
|
||||
".Trim();
|
||||
|
@ -547,7 +548,7 @@ namespace MapTo
|
|||
diagnostics.ShouldBeSuccessful();
|
||||
compilation.SyntaxTrees.ShouldContainSource(MapTypeConverterAttributeSource.AttributeName, expectedInterface);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void When_FoundMatchingPropertyNameWithDifferentImplicitlyConvertibleType_Should_GenerateTheProperty()
|
||||
{
|
||||
|
@ -582,7 +583,7 @@ namespace MapTo
|
|||
diagnostics.ShouldBeSuccessful();
|
||||
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void When_FoundMatchingPropertyNameWithIncorrectConverterType_ShouldReportError()
|
||||
{
|
||||
|
@ -606,19 +607,19 @@ namespace Test
|
|||
|
||||
public class Prop4Converter: ITypeConverter<string, int>
|
||||
{
|
||||
public int Convert(string source) => int.Parse(source);
|
||||
public int Convert(string source, object[] converterParameters) => int.Parse(source);
|
||||
}
|
||||
}
|
||||
";
|
||||
|
||||
// Act
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
|
||||
|
||||
|
||||
// Assert
|
||||
var expectedError = DiagnosticProvider.InvalidTypeConverterGenericTypesError(GetSourcePropertySymbol("Prop4", compilation), GetSourcePropertySymbol("Prop4", compilation, "Baz"));
|
||||
diagnostics.ShouldBeUnsuccessful(expectedError);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterToAssignProperties()
|
||||
{
|
||||
|
@ -640,16 +641,52 @@ namespace Test
|
|||
|
||||
public class Prop4Converter: ITypeConverter<string, long>
|
||||
{
|
||||
public long Convert(string source) => long.Parse(source);
|
||||
public long Convert(string source, object[] converterParameters) => long.Parse(source);
|
||||
}
|
||||
}
|
||||
";
|
||||
|
||||
const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4);";
|
||||
const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, null);";
|
||||
|
||||
// Act
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
|
||||
|
||||
|
||||
// Assert
|
||||
diagnostics.ShouldBeSuccessful();
|
||||
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedSyntax);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void When_FoundMatchingPropertyNameWithConverterType_ShouldUseTheConverterAndItsParametersToAssignProperties()
|
||||
{
|
||||
// Arrange
|
||||
var source = GetSourceText(new SourceGeneratorOptions(
|
||||
true,
|
||||
PropertyBuilder: builder =>
|
||||
{
|
||||
builder
|
||||
.PadLeft(Indent2).AppendLine("[MapTypeConverter(typeof(Prop4Converter), new object[]{\"G\", 'C', 10})]")
|
||||
.PadLeft(Indent2).AppendLine("public string Prop4 { get; set; }");
|
||||
},
|
||||
SourcePropertyBuilder: builder => builder.PadLeft(Indent2).AppendLine("public long Prop4 { get; set; }")));
|
||||
|
||||
source += @"
|
||||
namespace Test
|
||||
{
|
||||
using MapTo;
|
||||
|
||||
public class Prop4Converter: ITypeConverter<long, string>
|
||||
{
|
||||
public string Convert(long source, object[] converterParameters) => source.ToString(converterParameters[0] as string);
|
||||
}
|
||||
}
|
||||
";
|
||||
|
||||
const string expectedSyntax = "Prop4 = new Test.Prop4Converter().Convert(baz.Prop4, new object[] { \"G\", 'C', 10 });";
|
||||
|
||||
// Act
|
||||
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source, analyzerConfigOptions: DefaultAnalyzerOptions);
|
||||
|
||||
// Assert
|
||||
diagnostics.ShouldBeSuccessful();
|
||||
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedSyntax);
|
||||
|
@ -665,12 +702,12 @@ namespace Test
|
|||
.OfType<PropertyDeclarationSyntax>()
|
||||
.Single(p => p.Identifier.ValueText == targetPropertyName);
|
||||
}
|
||||
|
||||
|
||||
private static IPropertySymbol GetSourcePropertySymbol(string propertyName, Compilation compilation, string targetClass = "Foo")
|
||||
{
|
||||
var syntaxTree = compilation.SyntaxTrees.First();
|
||||
var propSyntax = GetPropertyDeclarationSyntax(syntaxTree, propertyName, targetClass);
|
||||
|
||||
|
||||
var semanticModel = compilation.GetSemanticModel(syntaxTree);
|
||||
return semanticModel.GetDeclaredSymbol(propSyntax);
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ namespace TestConsoleApp.ViewModels
|
|||
private class LastNameConverter : ITypeConverter<long, string>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Convert(long source) => $"{source} :: With Type Converter";
|
||||
public string Convert(long source, object[] converterParameters) => $"{source} :: With Type Converter";
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue