Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 45 additions & 8 deletions src/PatternKit.Generators/IdentityMap/IdentityMapGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -91,21 +92,57 @@ private static string GenerateSource(INamedTypeSymbol type, INamedTypeSymbol ent
sb.AppendLine();
}

var containingTypes = GetContainingTypes(type);
var indentLevel = 0;
foreach (var containingType in containingTypes)
{
AppendTypeDeclaration(sb, containingType, indentLevel);
sb.AppendLine();
sb.AppendLine(new string(' ', indentLevel * 4) + "{");
indentLevel++;
}

AppendTypeDeclaration(sb, type, indentLevel);
sb.AppendLine();
var indent = new string(' ', indentLevel * 4);
sb.AppendLine(indent + "{");
var memberIndent = indent + " ";
var bodyIndent = memberIndent + " ";
sb.Append(memberIndent).Append("public static global::PatternKit.Application.IdentityMap.IdentityMap<")
.Append(entityName).Append(", ").Append(keyName).Append("> ").Append(factoryName).AppendLine("()");
sb.Append(bodyIndent).Append("=> global::PatternKit.Application.IdentityMap.IdentityMap<")
.Append(entityName).Append(", ").Append(keyName).Append(">.Create(").Append(selectorName).AppendLine(").Build();");
sb.AppendLine(indent + "}");
for (var i = containingTypes.Length - 1; i >= 0; i--)
{
sb.AppendLine(new string(' ', i * 4) + "}");
}

return sb.ToString();
}

private static INamedTypeSymbol[] GetContainingTypes(INamedTypeSymbol type)
{
var containingTypes = new Stack<INamedTypeSymbol>();
for (var current = type.ContainingType; current is not null; current = current.ContainingType)
{
containingTypes.Push(current);
}

return containingTypes.ToArray();
}

private static void AppendTypeDeclaration(StringBuilder sb, INamedTypeSymbol type, int indentLevel)
{
sb.Append(new string(' ', indentLevel * 4));
sb.Append(GetAccessibility(type.DeclaredAccessibility)).Append(' ');
if (type.IsStatic)
sb.Append("static ");
else if (type.IsAbstract && type.TypeKind == TypeKind.Class)
sb.Append("abstract ");
else if (type.IsSealed && type.TypeKind == TypeKind.Class)
sb.Append("sealed ");
sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name).AppendLine();
sb.AppendLine("{");
sb.Append(" public static global::PatternKit.Application.IdentityMap.IdentityMap<")
.Append(entityName).Append(", ").Append(keyName).Append("> ").Append(factoryName).AppendLine("()");
sb.Append(" => global::PatternKit.Application.IdentityMap.IdentityMap<")
.Append(entityName).Append(", ").Append(keyName).Append(">.Create(").Append(selectorName).AppendLine(").Build();");
sb.AppendLine("}");
return sb.ToString();
sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name);
}

private static bool IsKeySelector(IMethodSymbol method, INamedTypeSymbol entityType, INamedTypeSymbol keyType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,26 +142,62 @@ private static string GenerateSource(
sb.AppendLine();
}

var containingTypes = GetContainingTypes(type);
var indentLevel = 0;
foreach (var containingType in containingTypes)
{
AppendTypeDeclaration(sb, containingType, indentLevel);
sb.AppendLine();
sb.AppendLine(new string(' ', indentLevel * 4) + "{");
indentLevel++;
}

AppendTypeDeclaration(sb, type, indentLevel);
sb.AppendLine();
var indent = new string(' ', indentLevel * 4);
sb.AppendLine(indent + "{");
var memberIndent = indent + " ";
var bodyIndent = memberIndent + " ";
sb.Append(memberIndent).Append("public static global::PatternKit.Application.ServiceLayer.ServiceLayerOperation<")
.Append(requestName).Append(", ").Append(responseName).Append("> ").Append(factoryName).AppendLine("()");
sb.AppendLine(memberIndent + "{");
sb.Append(bodyIndent).Append("var builder = global::PatternKit.Application.ServiceLayer.ServiceLayerOperation<")
.Append(requestName).Append(", ").Append(responseName).Append(">.Create(\"").Append(Escape(operationName)).AppendLine("\");");
foreach (var rule in rules)
sb.Append(bodyIndent).Append("builder.Require(\"").Append(Escape(rule.Code)).Append("\", \"").Append(Escape(rule.Message)).Append("\", ").Append(rule.Method.Name).AppendLine(");");
sb.Append(bodyIndent).Append("return builder.Handle(").Append(handlerName).AppendLine(").Build();");
sb.AppendLine(memberIndent + "}");
sb.AppendLine(indent + "}");
for (var i = containingTypes.Length - 1; i >= 0; i--)
{
sb.AppendLine(new string(' ', i * 4) + "}");
}

return sb.ToString();
}

private static INamedTypeSymbol[] GetContainingTypes(INamedTypeSymbol type)
{
var containingTypes = new Stack<INamedTypeSymbol>();
for (var current = type.ContainingType; current is not null; current = current.ContainingType)
{
containingTypes.Push(current);
}

return containingTypes.ToArray();
}

private static void AppendTypeDeclaration(StringBuilder sb, INamedTypeSymbol type, int indentLevel)
{
sb.Append(new string(' ', indentLevel * 4));
sb.Append(GetAccessibility(type.DeclaredAccessibility)).Append(' ');
if (type.IsStatic)
sb.Append("static ");
else if (type.IsAbstract && type.TypeKind == TypeKind.Class)
sb.Append("abstract ");
else if (type.IsSealed && type.TypeKind == TypeKind.Class)
sb.Append("sealed ");
sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name).AppendLine();
sb.AppendLine("{");
sb.Append(" public static global::PatternKit.Application.ServiceLayer.ServiceLayerOperation<")
.Append(requestName).Append(", ").Append(responseName).Append("> ").Append(factoryName).AppendLine("()");
sb.AppendLine(" {");
sb.Append(" var builder = global::PatternKit.Application.ServiceLayer.ServiceLayerOperation<")
.Append(requestName).Append(", ").Append(responseName).Append(">.Create(\"").Append(Escape(operationName)).AppendLine("\");");
foreach (var rule in rules)
sb.Append(" builder.Require(\"").Append(Escape(rule.Code)).Append("\", \"").Append(Escape(rule.Message)).Append("\", ").Append(rule.Method.Name).AppendLine(");");
sb.Append(" return builder.Handle(").Append(handlerName).AppendLine(").Build();");
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name);
}

private static bool IsHandler(IMethodSymbol method, INamedTypeSymbol requestType, INamedTypeSymbol responseType)
Expand Down
122 changes: 111 additions & 11 deletions test/PatternKit.Generators.Tests/IdentityMapGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,28 +29,50 @@ public static partial class OrderIdentityMap
ScenarioExpect.Empty(result.Diagnostics);
ScenarioExpect.Contains("CreateMap()", ScenarioExpect.Single(result.GeneratedSources));
ScenarioExpect.Contains("IdentityMap<global::Demo.Order, string>.Create(SelectKey).Build()", result.GeneratedSources[0]);
ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics));
})
.AssertPassed();

[Scenario("Generator emits internal struct factory in the global namespace")]
[Scenario("Generator emits identity map defaults and host shapes")]
[Fact]
public Task Generator_Emits_Internal_Struct_Factory_In_The_Global_Namespace()
=> Given("a valid internal struct declaration", () => Compile("""
public Task Generator_Emits_Identity_Map_Defaults_And_Host_Shapes()
=> Given("valid identity map declarations with default names and host shapes", () => Compile("""
using PatternKit.Generators.IdentityMap;
namespace Demo;
public sealed record Order(string Id);

[GenerateIdentityMap(typeof(Order), typeof(string))]
internal abstract partial class AbstractIdentityMap
{
[IdentityMapKeySelector]
private static string SelectKey(Order order) => order.Id;
}

[GenerateIdentityMap(typeof(Order), typeof(string))]
internal partial struct OrderIdentityMap
public sealed partial class SealedIdentityMap
{
[IdentityMapKeySelector]
private static string SelectKey(Order order) => order.Id;
}

[GenerateIdentityMap(typeof(Order), typeof(string))]
internal partial struct StructIdentityMap
{
[IdentityMapKeySelector]
private static string SelectKey(Order order) => order.Id;
}
"""))
.Then("generated source keeps the declaration shape and default factory name", result =>
.Then("generated source keeps declaration shapes and default factory names", result =>
{
ScenarioExpect.Empty(result.Diagnostics);
ScenarioExpect.Contains("internal partial struct OrderIdentityMap", ScenarioExpect.Single(result.GeneratedSources));
ScenarioExpect.Contains("Create()", result.GeneratedSources[0]);
ScenarioExpect.DoesNotContain("namespace", result.GeneratedSources[0]);
ScenarioExpect.Equal(3, result.GeneratedSources.Count);

var combined = string.Join("\n", result.GeneratedSources);
ScenarioExpect.Contains("internal abstract partial class AbstractIdentityMap", combined);
ScenarioExpect.Contains("public sealed partial class SealedIdentityMap", combined);
ScenarioExpect.Contains("internal partial struct StructIdentityMap", combined);
ScenarioExpect.Contains("Create()", combined);
ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics));
})
.AssertPassed();

Expand All @@ -61,6 +83,8 @@ internal partial struct OrderIdentityMap
[InlineData("public static partial class OrderIdentityMap { [IdentityMapKeySelector] private static string SelectKey(Order order) => order.Id; [IdentityMapKeySelector] private static string SelectAlternate(Order order) => order.Id; }", "PKIM002")]
[InlineData("public static partial class OrderIdentityMap { [IdentityMapKeySelector] private static int SelectKey(Order order) => 1; }", "PKIM003")]
[InlineData("public static partial class OrderIdentityMap { [IdentityMapKeySelector] private string SelectKey(Order order) => order.Id; }", "PKIM003")]
[InlineData("public static partial class OrderIdentityMap { [IdentityMapKeySelector] private static string SelectKey() => string.Empty; }", "PKIM003")]
[InlineData("public static partial class OrderIdentityMap { [IdentityMapKeySelector] private static string SelectKey(string order) => order; }", "PKIM003")]
public Task Generator_Reports_Invalid_Identity_Map_Declarations(string declaration, string diagnosticId)
=> Given("an invalid identity map declaration", () => Compile($$"""
using PatternKit.Generators.IdentityMap;
Expand All @@ -72,18 +96,94 @@ public sealed record Order(string Id);
ScenarioExpect.Contains(result.Diagnostics, diagnostic => diagnostic.Id == diagnosticId))
.AssertPassed();

[Scenario("Generator emits nested identity map host wrappers")]
[Fact]
public Task Generator_Emits_Nested_Identity_Map_Host_Wrappers()
=> Given("nested identity map declarations", () => Compile("""
using PatternKit.Generators.IdentityMap;
namespace Demo;
public sealed record Order(string Id);

public partial class IdentityMapContainer
{
private partial class PrivateHost
{
[GenerateIdentityMap(typeof(Order), typeof(string))]
protected partial class ProtectedIdentityMap
{
[IdentityMapKeySelector]
private static string SelectKey(Order order) => order.Id;
}

[GenerateIdentityMap(typeof(Order), typeof(string))]
private protected partial class PrivateProtectedIdentityMap
{
[IdentityMapKeySelector]
private static string SelectKey(Order order) => order.Id;
}

[GenerateIdentityMap(typeof(Order), typeof(string))]
protected internal partial class ProtectedInternalIdentityMap
{
[IdentityMapKeySelector]
private static string SelectKey(Order order) => order.Id;
}
}
}
"""))
.Then("generated sources preserve containing partial type wrappers", result =>
{
ScenarioExpect.Empty(result.Diagnostics);
ScenarioExpect.Equal(3, result.GeneratedSources.Count);

var combined = string.Join("\n", result.GeneratedSources);
ScenarioExpect.Contains("public partial class IdentityMapContainer", combined);
ScenarioExpect.Contains("private partial class PrivateHost", combined);
ScenarioExpect.Contains("protected partial class ProtectedIdentityMap", combined);
ScenarioExpect.Contains("private protected partial class PrivateProtectedIdentityMap", combined);
ScenarioExpect.Contains("protected internal partial class ProtectedInternalIdentityMap", combined);
ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics));
})
.AssertPassed();

[Scenario("Generator skips malformed identity map type arguments")]
[Theory]
[InlineData("null!", "typeof(string)")]
[InlineData("typeof(Order)", "null!")]
public Task Generator_Skips_Malformed_Identity_Map_Type_Arguments(string entityType, string keyType)
=> Given("an identity map declaration with a null type argument", () => Compile($$"""
using PatternKit.Generators.IdentityMap;
public sealed record Order(string Id);
[GenerateIdentityMap({{entityType}}, {{keyType}})]
public static partial class OrderIdentityMap
{
[IdentityMapKeySelector]
private static string SelectKey(Order order) => order.Id;
}
"""))
.Then("no source is generated", result =>
ScenarioExpect.Empty(result.GeneratedSources))
.AssertPassed();

private static GeneratorResult Compile(string source)
{
var compilation = RoslynTestHelpers.CreateCompilation(
source,
"IdentityMapGeneratorTests",
extra: MetadataReference.CreateFromFile(typeof(IdentityMap<,>).Assembly.Location));
_ = RoslynTestHelpers.Run(compilation, new IdentityMapGenerator(), out var run, out _);
_ = RoslynTestHelpers.Run(compilation, new IdentityMapGenerator(), out var run, out var updated);
var result = run.Results.Single();
var emit = updated.Emit(Stream.Null);
return new GeneratorResult(
result.Diagnostics.ToArray(),
result.GeneratedSources.Select(static source => source.SourceText.ToString()).ToArray());
result.GeneratedSources.Select(static source => source.SourceText.ToString()).ToArray(),
emit.Success,
emit.Diagnostics.Select(static diagnostic => diagnostic.ToString()).ToArray());
}

private sealed record GeneratorResult(IReadOnlyList<Diagnostic> Diagnostics, IReadOnlyList<string> GeneratedSources);
private sealed record GeneratorResult(
IReadOnlyList<Diagnostic> Diagnostics,
IReadOnlyList<string> GeneratedSources,
bool EmitSuccess,
IReadOnlyList<string> EmitDiagnostics);
}
Loading
Loading