This commit is contained in:
Mohammadreza Taikandi 2020-12-21 16:34:53 +00:00
parent 3ff1e90fb9
commit 2a671a9dd4
17 changed files with 396 additions and 352 deletions

72
.editorconfig Normal file
View File

@ -0,0 +1,72 @@
[*.{appxmanifest,asax,ascx,aspx,axaml,build,cg,cginc,compute,cs,cshtml,dtd,fs,fsi,fsscript,fsx,hlsl,hlsli,hlslinc,master,ml,mli,nuspec,paml,razor,resw,resx,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}]
indent_style = space
indent_size = 4
tab_width = 4
[*]
# Microsoft .NET properties
csharp_new_line_before_members_in_object_initializers = false
csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
csharp_space_after_cast = false
csharp_style_var_elsewhere = true:suggestion
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none
dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:none
dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
dotnet_style_qualification_for_event = false:suggestion
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
# ReSharper properties
resharper_autodetect_indent_settings = true
resharper_blank_lines_after_control_transfer_statements = 1
resharper_blank_lines_after_multiline_statements = 1
resharper_blank_lines_around_block_case_section = 1
resharper_blank_lines_around_multiline_case_section = 1
resharper_blank_lines_around_single_line_auto_property = 1
resharper_blank_lines_around_single_line_local_method = 1
resharper_blank_lines_around_single_line_property = 1
resharper_braces_for_for = required
resharper_braces_for_foreach = required
resharper_braces_for_ifelse = required
resharper_braces_for_while = required
resharper_csharp_blank_lines_around_single_line_invocable = 1
resharper_csharp_empty_block_style = together_same_line
resharper_csharp_keep_blank_lines_in_code = 1
resharper_csharp_keep_blank_lines_in_declarations = 1
resharper_csharp_max_line_length = 180
resharper_csharp_wrap_lines = false
resharper_local_function_body = expression_body
resharper_method_or_operator_body = expression_body
resharper_place_accessorholder_attribute_on_same_line = false
resharper_place_field_attribute_on_same_line = false
resharper_space_after_cast = false
resharper_space_within_single_line_array_initializer_braces = true
resharper_use_indent_from_vs = false
resharper_xmldoc_indent_text = ZeroIndent
# ReSharper inspection severities
resharper_arguments_style_literal_highlighting = none
resharper_arguments_style_named_expression_highlighting = none
resharper_arguments_style_other_highlighting = none
resharper_arrange_redundant_parentheses_highlighting = hint
resharper_arrange_this_qualifier_highlighting = hint
resharper_arrange_type_member_modifiers_highlighting = hint
resharper_arrange_type_modifiers_highlighting = hint
resharper_built_in_type_reference_style_for_member_access_highlighting = hint
resharper_built_in_type_reference_style_highlighting = hint
resharper_class_never_instantiated_global_highlighting = none
resharper_redundant_base_qualifier_highlighting = warning
resharper_suggest_var_or_type_built_in_types_highlighting = hint
resharper_suggest_var_or_type_elsewhere_highlighting = hint
resharper_suggest_var_or_type_simple_types_highlighting = hint
resharper_web_config_module_not_resolved_highlighting = warning
resharper_web_config_type_not_resolved_highlighting = warning
resharper_web_config_wrong_module_highlighting = warning

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Mohammadreza Taikandi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -12,7 +12,5 @@ namespace System.Runtime.CompilerServices
/// This class should not be used by developers in source code.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit
{
}
internal static class IsExternalInit { }
}

View File

@ -6,16 +6,16 @@ namespace MapTo
{
private const string UsageCategory = "Usage";
internal static Diagnostic SymbolNotFound(Location location, string syntaxName) =>
Diagnostic.Create(CreateDescriptor("MT0001", "Symbol not found.", $"Unable to find any symbols for {syntaxName}"), location);
internal static Diagnostic SymbolNotFound(Location location, string syntaxName) =>
Create("MT0001", "Symbol not found.", $"Unable to find any symbols for {syntaxName}", location);
internal static Diagnostic MapFromAttributeNotFound(Location location) =>
Diagnostic.Create(CreateDescriptor("MT0002", "Attribute Not Available", $"Unable to find {SourceBuilder.MapFromAttributeName} type."), location);
internal static Diagnostic MapFromAttributeNotFound(Location location) =>
Create("MT0002", "Attribute Not Available", $"Unable to find {SourceBuilder.MapFromAttributeName} type.", location);
internal static Diagnostic ClassMappingsGenerated(Location location, string typeName) =>
Diagnostic.Create(CreateDescriptor("MT1001", "Mapped Type", $"Generated mappings for {typeName}", DiagnosticSeverity.Info), location);
Create("MT1001", "Mapped Type", $"Generated mappings for {typeName}", location, DiagnosticSeverity.Info);
private static DiagnosticDescriptor CreateDescriptor(string id, string title, string message, DiagnosticSeverity severity = DiagnosticSeverity.Error) =>
new(id, title, message, UsageCategory, severity, true);
private static Diagnostic Create(string id, string title, string message, Location location, DiagnosticSeverity severity = DiagnosticSeverity.Error) =>
Diagnostic.Create(new DiagnosticDescriptor(id, title, message, UsageCategory, severity, true), location);
}
}

View File

@ -23,35 +23,11 @@ namespace MapTo.Extensions
return type.GetBaseTypesAndThis().SelectMany(n => n.GetMembers());
}
public static IEnumerable<T> GetAllMembersOfType<T>(this ITypeSymbol type) where T : ISymbol
{
return type.GetAllMembers().OfType<T>();
}
public static IEnumerable<T> GetAllMembersOfType<T>(this ITypeSymbol type) where T : ISymbol => type.GetAllMembers().OfType<T>();
public static CompilationUnitSyntax GetCompilationUnit(this SyntaxNode syntaxNode)
{
return syntaxNode.Ancestors().OfType<CompilationUnitSyntax>().Single();
}
public static CompilationUnitSyntax GetCompilationUnit(this SyntaxNode syntaxNode) => syntaxNode.Ancestors().OfType<CompilationUnitSyntax>().Single();
public static string GetClassName(this ClassDeclarationSyntax classSyntax)
{
return classSyntax.Identifier.Text;
}
public static string GetClassModifier(this ClassDeclarationSyntax classSyntax)
{
return classSyntax.Modifiers.ToFullString().Trim();
}
public static bool HaveAttribute(this ClassDeclarationSyntax classSyntax, string attributeName)
{
return classSyntax.AttributeLists.Count > 0 &&
classSyntax.AttributeLists.SelectMany(al => al.Attributes
.Where(a =>
(a.Name as IdentifierNameSyntax)?.Identifier.Text == attributeName ||
((a.Name as QualifiedNameSyntax)?.Right as IdentifierNameSyntax)?.Identifier.ValueText == attributeName))
.Any();
}
public static string GetClassName(this ClassDeclarationSyntax classSyntax) => classSyntax.Identifier.Text;
public static AttributeSyntax? GetAttribute(this ClassDeclarationSyntax classSyntax, string attributeName)
{
@ -62,21 +38,11 @@ namespace MapTo.Extensions
((a.Name as QualifiedNameSyntax)?.Right as IdentifierNameSyntax)?.Identifier.ValueText == attributeName);
}
public static string? GetNamespace(this CompilationUnitSyntax root)
{
return root.ChildNodes()
public static string? GetNamespace(this CompilationUnitSyntax root) =>
root.ChildNodes()
.OfType<NamespaceDeclarationSyntax>()
.FirstOrDefault()
?.Name
.ToString();
}
public static List<string> GetUsings(this CompilationUnitSyntax root)
{
return root.ChildNodes()
.OfType<UsingDirectiveSyntax>()
.Select(n => n.Name.ToString())
.ToList();
}
}
}

View File

@ -15,10 +15,7 @@ namespace MapTo.Extensions
return builder;
}
internal static StringBuilder AppendOpeningBracket(this StringBuilder builder, int indent = 0)
{
return builder.AppendLine().PadLeft(indent).AppendFormat("{{{0}", Environment.NewLine);
}
internal static StringBuilder AppendOpeningBracket(this StringBuilder builder, int indent = 0) => builder.AppendLine().PadLeft(indent).AppendFormat("{{{0}", Environment.NewLine);
internal static StringBuilder AppendClosingBracket(this StringBuilder builder, int indent = 0, bool padNewLine = true)
{

View File

@ -2,9 +2,6 @@
{
internal static class StringExtensions
{
public static string ToCamelCase(this string value)
{
return string.IsNullOrWhiteSpace(value) ? value : $"{char.ToLower(value[0])}{value.Substring(1)}";
}
public static string ToCamelCase(this string value) => string.IsNullOrWhiteSpace(value) ? value : $"{char.ToLower(value[0])}{value.Substring(1)}";
}
}

View File

@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using MapTo.Extensions;
using MapTo.Models;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@ -19,57 +18,36 @@ namespace MapTo
/// <inheritdoc />
public void Execute(GeneratorExecutionContext context)
{
context.AddMapToAttribute();
AddMapFromAttribute(context);
if (!(context.SyntaxReceiver is MapToSyntaxReceiver receiver) || !receiver.CandidateClasses.Any())
if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateClasses.Any())
{
return;
AddGeneratedMappingsClasses(context, receiver.CandidateClasses);
}
}
foreach (var classDeclarationSyntax in receiver.CandidateClasses)
private static void AddGeneratedMappingsClasses(GeneratorExecutionContext context, IEnumerable<ClassDeclarationSyntax> candidateClasses)
{
foreach (var classDeclarationSyntax in candidateClasses)
{
var (model, diagnostic) = GetModel(context.Compilation, classDeclarationSyntax);
var (model, diagnostic) = MapModel.Create(context.Compilation, classDeclarationSyntax);
if (model is null)
{
context.ReportDiagnostic(diagnostic!);
continue;
}
var (source, hintName) = SourceBuilder.GenerateSource(model);
context.AddSource(hintName, source);
context.ReportDiagnostic(Diagnostics.ClassMappingsGenerated(classDeclarationSyntax.GetLocation(), model.ClassName));
context.ReportDiagnostic(Diagnostics.ClassMappingsGenerated(classDeclarationSyntax.GetLocation(), model.ClassName));
}
}
private static (MapModel? model, Diagnostic? diagnostic) GetModel(Compilation compilation, ClassDeclarationSyntax classSyntax)
private static void AddMapFromAttribute(GeneratorExecutionContext context)
{
var root = classSyntax.GetCompilationUnit();
var classSemanticModel = compilation.GetSemanticModel(classSyntax.SyntaxTree);
if (!(classSemanticModel.GetDeclaredSymbol(classSyntax) is INamedTypeSymbol classSymbol))
{
return (default, Diagnostics.SymbolNotFound(classSyntax.GetLocation(), classSyntax.Identifier.ValueText));
}
var sourceTypeSymbol = GetSourceTypeSymbol(classSyntax, classSemanticModel);
if (sourceTypeSymbol is null)
{
return (default, Diagnostics.SymbolNotFound(classSyntax.GetLocation(), classSyntax.Identifier.ValueText));
}
return (MapModel.Create(root, classSyntax, classSymbol, sourceTypeSymbol), default);
}
private static ITypeSymbol? GetSourceTypeSymbol(ClassDeclarationSyntax classSyntax, SemanticModel model)
{
var sourceTypeExpressionSyntax = classSyntax
.GetAttribute(SourceBuilder.MapFromAttributeName)
?.DescendantNodes()
.OfType<TypeOfExpressionSyntax>()
.SingleOrDefault();
return sourceTypeExpressionSyntax is not null ? model.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type : null;
var (source, hintName) = SourceBuilder.GenerateMapFromAttribute();
context.AddSource(hintName, source);
}
}
}

View File

@ -2,11 +2,11 @@
<PropertyGroup>
<AssemblyName>MapTo</AssemblyName>
<Description>Generates mapping code between two types using Roslyn code generator.</Description>
<Description>An object to object mapping generator using using Roslyn code generator.</Description>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IncludeSymbols>true</IncludeSymbols>
<NoWarn>NU5128</NoWarn>
<PackageId>MapTo</PackageId>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://github.com/mrtaikandi/mapto</PackageProjectUrl>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageVersion>$(Version)</PackageVersion>
@ -16,7 +16,7 @@
<RootNamespace>MapTo</RootNamespace>
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<LangVersion>9</LangVersion>
</PropertyGroup>
<ItemGroup>
@ -32,6 +32,7 @@
</ItemGroup>
<ItemGroup>
<None Include="..\LICENSE" Pack="true" PackagePath="" Visible="false" />
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using MapTo.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@ -43,9 +44,23 @@ namespace MapTo.Models
public IEnumerable<IPropertySymbol> SourceTypeProperties { get; }
internal static MapModel Create(CompilationUnitSyntax root, ClassDeclarationSyntax classSyntax, INamedTypeSymbol classSymbol, ITypeSymbol sourceTypeSymbol)
internal static (MapModel? model, Diagnostic? diagnostic) Create(Compilation compilation, ClassDeclarationSyntax classSyntax)
{
return new(
var root = classSyntax.GetCompilationUnit();
var classSemanticModel = compilation.GetSemanticModel(classSyntax.SyntaxTree);
if (!(classSemanticModel.GetDeclaredSymbol(classSyntax) is INamedTypeSymbol classSymbol))
{
return (default, Diagnostics.SymbolNotFound(classSyntax.GetLocation(), classSyntax.Identifier.ValueText));
}
var sourceTypeSymbol = GetSourceTypeSymbol(classSyntax, classSemanticModel);
if (sourceTypeSymbol is null)
{
return (default, Diagnostics.SymbolNotFound(classSyntax.GetLocation(), classSyntax.Identifier.ValueText));
}
var model = new MapModel(
root.GetNamespace(),
classSyntax.Modifiers,
classSyntax.GetClassName(),
@ -54,6 +69,19 @@ namespace MapTo.Models
sourceTypeSymbol.Name,
sourceTypeSymbol.ToString(),
sourceTypeSymbol.GetAllMembersOfType<IPropertySymbol>());
return (model, default);
}
private static ITypeSymbol? GetSourceTypeSymbol(ClassDeclarationSyntax classSyntax, SemanticModel model)
{
var sourceTypeExpressionSyntax = classSyntax
.GetAttribute(SourceBuilder.MapFromAttributeName)
?.DescendantNodes()
.OfType<TypeOfExpressionSyntax>()
.SingleOrDefault();
return sourceTypeExpressionSyntax is not null ? model.GetTypeInfo(sourceTypeExpressionSyntax.Type).Type : null;
}
}
}

View File

@ -16,7 +16,7 @@ namespace MapTo
private const int Indent2 = Indent1 * 2; // " ";
private const int Indent3 = Indent1 * 3; // " ";
internal static void AddMapToAttribute(this GeneratorExecutionContext context)
internal static (string source, string hintName) GenerateMapFromAttribute()
{
const string source = @"
using System;
@ -36,7 +36,7 @@ namespace MapTo
}
";
context.AddSource("MapFromAttribute.g.cs", source);
return (source, $"{MapFromAttributeName}Attribute.g.cs");
}
internal static (string source, string hintName) GenerateSource(MapModel model)
@ -70,13 +70,13 @@ namespace MapTo
.PadLeft(Indent1)
.AppendFormat("{0} static partial class {1}Extensions", model.ClassModifiers.FirstOrDefault().ToFullString().Trim(), model.SourceClassName)
.AppendOpeningBracket(Indent1)
// Extension class body
.GenerateSourceTypeExtensionMethod(model)
// End extensions class declaration
.AppendClosingBracket(Indent1)
// End namespace declaration
.AppendClosingBracket();
@ -150,11 +150,9 @@ namespace MapTo
.AppendFormat("return {0} == null ? null : new {1}({0});", sourceClassParameterName, model.ClassName)
.AppendClosingBracket(Indent2);
}
private static StringBuilder AppendFileHeader(this StringBuilder builder)
{
return builder
private static StringBuilder AppendFileHeader(this StringBuilder builder) =>
builder
.AppendLine("// <auto-generated />");
}
}
}

View File

@ -11,17 +11,6 @@ namespace MapToTests
{
internal static class CSharpGenerator
{
internal static string GetGeneratedOutput(this ITestOutputHelper outputHelper, string source)
{
var (compilation, diagnostics) = GetOutputCompilation(source);
diagnostics.ShouldBeSuccessful();
var generatedOutput = compilation.SyntaxTrees.Last().ToString();
outputHelper.WriteLine(generatedOutput);
return generatedOutput;
}
internal static void ShouldBeSuccessful(this ImmutableArray<Diagnostic> diagnostics)
{
Assert.False(diagnostics.Any(d => d.Severity >= DiagnosticSeverity.Warning), $"Failed: {Environment.NewLine}{string.Join($"{Environment.NewLine}- ", diagnostics.Select(c => c.GetMessage()))}");
@ -47,7 +36,7 @@ namespace MapToTests
ISourceGenerator generator = new MapToGenerator();
var driver = CSharpGeneratorDriver.Create(generator);
driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var generateDiagnostics);
return (outputCompilation, generateDiagnostics);
}
}

View File

@ -8,7 +8,13 @@ namespace MapToTests
{
public class Tests
{
public Tests(ITestOutputHelper output)
{
_output = output;
}
private readonly ITestOutputHelper _output;
private const string ExpectedAttribute = @"
using System;
@ -26,225 +32,6 @@ namespace MapTo
}
}
";
public Tests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void VerifyMapToAttribute()
{
// Arrange
const string source = "";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContain(c => c.ToString() == ExpectedAttribute);
}
[Fact]
public void When_NoMapToAttributeFound_Should_GenerateOnlyTheAttribute()
{
// Arrange
const string source = "";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContain(s => s.ToString() == ExpectedAttribute);
compilation.SyntaxTrees.Select(s => s.ToString()).Where(s => s != string.Empty && s != ExpectedAttribute).ShouldBeEmpty();
}
[Fact]
public void When_MapToAttributeFound_Should_GenerateTheClass()
{
// Arrange
const string source = @"
using MapTo;
namespace Test
{
[MapFrom(typeof(Baz))]
public partial class Foo
{
}
public class Baz
{
public int Prop1 { get; set; }
public int Prop2 { get; }
public int Prop3 { get; set; }
}
}
";
const string expectedResult = @"
// <auto-generated />
using System;
namespace Test
{
public partial class Foo
{
public Foo(Test.Baz baz)
{
if (baz == null) throw new ArgumentNullException(nameof(baz));
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim());
}
[Fact]
public void When_MapToAttributeWithNamespaceFound_Should_GenerateTheClass()
{
// Arrange
const string source = @"
namespace Test
{
[MapTo.MapFrom(typeof(Baz))]
public partial class Foo
{
}
public class Baz
{
}
}
";
const string expectedResult = @"
// <auto-generated />
using System;
namespace Test
{
public partial class Foo
{
public Foo(Test.Baz baz)
{
if (baz == null) throw new ArgumentNullException(nameof(baz));
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim());
}
[Fact]
public void When_SourceTypeHasMatchingProperties_Should_CreateConstructorAndAssignSrcToDest()
{
// Arrange
var source = GetSourceText();
const string expectedResult = @"
public 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;
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim());
}
[Fact]
public void When_SourceTypeHasMatchingProperties_Should_CreateFromStaticMethod()
{
// Arrange
var source = GetSourceText();
const string expectedResult = @"
public static Foo From(Test.Models.Baz baz)
{
return baz == null ? null : new Foo(baz);
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim());
}
[Fact]
public void When_SourceTypeHasDifferentNamespace_Should_AddToUsings()
{
// Arrange
var source = GetSourceText(sourceClassNamespace: "Bazaar");
const string expectedResult = @"
// <auto-generated />
using System;
using Bazaar;
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim());
}
[Fact]
public void When_SourceTypeHasMatchingProperties_Should_GenerateToExtensionMethodOnSourceType()
{
// Arrange
var source = GetSourceText();
const string expectedResult = @"
public static class BazExtensions
{
public static Foo ToFoo(this Test.Models.Baz baz)
{
return baz == null ? null : new Foo(baz);
}
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim());
}
private static string GetSourceText(bool includeAttributeNamespace = false, string sourceClassNamespace = "Test.Models")
{
@ -253,7 +40,7 @@ using Bazaar;
{(includeAttributeNamespace ? string.Empty : "using MapTo;")}
namespace Test
{{
{(sourceClassNamespace != "Test" && !includeAttributeNamespace ? $"using {sourceClassNamespace};": string.Empty)}
{(sourceClassNamespace != "Test" && !includeAttributeNamespace ? $"using {sourceClassNamespace};" : string.Empty)}
{(includeAttributeNamespace ? "[MapTo.MapFrom(typeof(Baz))]" : "[MapFrom(typeof(Baz))]")}
public partial class Foo
@ -279,5 +66,219 @@ namespace {sourceClassNamespace}
return builder.ToString();
}
[Fact]
public void VerifyMapToAttribute()
{
// Arrange
const string source = "";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContain(c => c.ToString() == ExpectedAttribute);
}
[Fact]
public void When_MapToAttributeFound_Should_GenerateTheClass()
{
// Arrange
const string source = @"
using MapTo;
namespace Test
{
[MapFrom(typeof(Baz))]
public partial class Foo
{
}
public class Baz
{
public int Prop1 { get; set; }
public int Prop2 { get; }
public int Prop3 { get; set; }
}
}
";
const string expectedResult = @"
// <auto-generated />
using System;
namespace Test
{
public partial class Foo
{
public Foo(Test.Baz baz)
{
if (baz == null) throw new ArgumentNullException(nameof(baz));
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim());
}
[Fact]
public void When_MapToAttributeWithNamespaceFound_Should_GenerateTheClass()
{
// Arrange
const string source = @"
namespace Test
{
[MapTo.MapFrom(typeof(Baz))]
public partial class Foo
{
}
public class Baz
{
}
}
";
const string expectedResult = @"
// <auto-generated />
using System;
namespace Test
{
public partial class Foo
{
public Foo(Test.Baz baz)
{
if (baz == null) throw new ArgumentNullException(nameof(baz));
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim());
}
[Fact]
public void When_NoMapToAttributeFound_Should_GenerateOnlyTheAttribute()
{
// Arrange
const string source = "";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.ShouldContain(s => s.ToString() == ExpectedAttribute);
compilation.SyntaxTrees.Select(s => s.ToString()).Where(s => s != string.Empty && s != ExpectedAttribute).ShouldBeEmpty();
}
[Fact]
public void When_SourceTypeHasDifferentNamespace_Should_AddToUsings()
{
// Arrange
var source = GetSourceText(sourceClassNamespace: "Bazaar");
const string expectedResult = @"
// <auto-generated />
using System;
using Bazaar;
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldStartWith(expectedResult.Trim());
}
[Fact]
public void When_SourceTypeHasMatchingProperties_Should_CreateConstructorAndAssignSrcToDest()
{
// Arrange
var source = GetSourceText();
const string expectedResult = @"
public 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;
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim());
}
[Fact]
public void When_SourceTypeHasMatchingProperties_Should_CreateFromStaticMethod()
{
// Arrange
var source = GetSourceText();
const string expectedResult = @"
public static Foo From(Test.Models.Baz baz)
{
return baz == null ? null : new Foo(baz);
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim());
}
[Fact]
public void When_SourceTypeHasMatchingProperties_Should_GenerateToExtensionMethodOnSourceType()
{
// Arrange
var source = GetSourceText();
const string expectedResult = @"
public static partial class BazExtensions
{
public static Foo ToFoo(this Test.Models.Baz baz)
{
return baz == null ? null : new Foo(baz);
}
}
";
// Act
var (compilation, diagnostics) = CSharpGenerator.GetOutputCompilation(source);
// Assert
diagnostics.ShouldBeSuccessful();
compilation.SyntaxTrees.Count().ShouldBe(3);
compilation.SyntaxTrees.Last().ToString().ShouldContain(expectedResult.Trim());
}
}
}

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# MapTo
An object to object mapping generator using using Roslyn code generator.

View File

@ -1,15 +1,13 @@
using System;
using TestConsoleApp.ViewModels;
using TestConsoleApp.ViewModels;
namespace TestConsoleApp
{
class Program
internal class Program
{
static void Main(string[] args)
private static void Main(string[] args)
{
var userViewModel = User.From(new Data.Models.User());
var userViewModel2 = UserViewModel.From(new Data.Models.User());
}
}
}

View File

@ -1,9 +1,10 @@

// using MapTo;
// using MapTo;
using MapTo;
namespace TestConsoleApp.ViewModels
{
[MapTo.MapFrom(typeof(Data.Models.User))]
[MapFrom(typeof(Data.Models.User))]
public partial class User
{
public string FirstName { get; }

View File

@ -3,8 +3,5 @@
namespace TestConsoleApp.ViewModels
{
[MapFrom(typeof(Data.Models.User))]
public partial class UserViewModel
{
}
public partial class UserViewModel { }
}