Skip to content

Commit f76f5e0

Browse files
authored
test(generators): cover identity map and service layer (#442)
1 parent 4596396 commit f76f5e0

4 files changed

Lines changed: 347 additions & 35 deletions

File tree

src/PatternKit.Generators/IdentityMap/IdentityMapGenerator.cs

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Generic;
12
using System.Linq;
23
using System.Text;
34
using Microsoft.CodeAnalysis;
@@ -91,21 +92,57 @@ private static string GenerateSource(INamedTypeSymbol type, INamedTypeSymbol ent
9192
sb.AppendLine();
9293
}
9394

95+
var containingTypes = GetContainingTypes(type);
96+
var indentLevel = 0;
97+
foreach (var containingType in containingTypes)
98+
{
99+
AppendTypeDeclaration(sb, containingType, indentLevel);
100+
sb.AppendLine();
101+
sb.AppendLine(new string(' ', indentLevel * 4) + "{");
102+
indentLevel++;
103+
}
104+
105+
AppendTypeDeclaration(sb, type, indentLevel);
106+
sb.AppendLine();
107+
var indent = new string(' ', indentLevel * 4);
108+
sb.AppendLine(indent + "{");
109+
var memberIndent = indent + " ";
110+
var bodyIndent = memberIndent + " ";
111+
sb.Append(memberIndent).Append("public static global::PatternKit.Application.IdentityMap.IdentityMap<")
112+
.Append(entityName).Append(", ").Append(keyName).Append("> ").Append(factoryName).AppendLine("()");
113+
sb.Append(bodyIndent).Append("=> global::PatternKit.Application.IdentityMap.IdentityMap<")
114+
.Append(entityName).Append(", ").Append(keyName).Append(">.Create(").Append(selectorName).AppendLine(").Build();");
115+
sb.AppendLine(indent + "}");
116+
for (var i = containingTypes.Length - 1; i >= 0; i--)
117+
{
118+
sb.AppendLine(new string(' ', i * 4) + "}");
119+
}
120+
121+
return sb.ToString();
122+
}
123+
124+
private static INamedTypeSymbol[] GetContainingTypes(INamedTypeSymbol type)
125+
{
126+
var containingTypes = new Stack<INamedTypeSymbol>();
127+
for (var current = type.ContainingType; current is not null; current = current.ContainingType)
128+
{
129+
containingTypes.Push(current);
130+
}
131+
132+
return containingTypes.ToArray();
133+
}
134+
135+
private static void AppendTypeDeclaration(StringBuilder sb, INamedTypeSymbol type, int indentLevel)
136+
{
137+
sb.Append(new string(' ', indentLevel * 4));
94138
sb.Append(GetAccessibility(type.DeclaredAccessibility)).Append(' ');
95139
if (type.IsStatic)
96140
sb.Append("static ");
97141
else if (type.IsAbstract && type.TypeKind == TypeKind.Class)
98142
sb.Append("abstract ");
99143
else if (type.IsSealed && type.TypeKind == TypeKind.Class)
100144
sb.Append("sealed ");
101-
sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name).AppendLine();
102-
sb.AppendLine("{");
103-
sb.Append(" public static global::PatternKit.Application.IdentityMap.IdentityMap<")
104-
.Append(entityName).Append(", ").Append(keyName).Append("> ").Append(factoryName).AppendLine("()");
105-
sb.Append(" => global::PatternKit.Application.IdentityMap.IdentityMap<")
106-
.Append(entityName).Append(", ").Append(keyName).Append(">.Create(").Append(selectorName).AppendLine(").Build();");
107-
sb.AppendLine("}");
108-
return sb.ToString();
145+
sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name);
109146
}
110147

111148
private static bool IsKeySelector(IMethodSymbol method, INamedTypeSymbol entityType, INamedTypeSymbol keyType)

src/PatternKit.Generators/ServiceLayer/ServiceLayerOperationGenerator.cs

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -142,26 +142,62 @@ private static string GenerateSource(
142142
sb.AppendLine();
143143
}
144144

145+
var containingTypes = GetContainingTypes(type);
146+
var indentLevel = 0;
147+
foreach (var containingType in containingTypes)
148+
{
149+
AppendTypeDeclaration(sb, containingType, indentLevel);
150+
sb.AppendLine();
151+
sb.AppendLine(new string(' ', indentLevel * 4) + "{");
152+
indentLevel++;
153+
}
154+
155+
AppendTypeDeclaration(sb, type, indentLevel);
156+
sb.AppendLine();
157+
var indent = new string(' ', indentLevel * 4);
158+
sb.AppendLine(indent + "{");
159+
var memberIndent = indent + " ";
160+
var bodyIndent = memberIndent + " ";
161+
sb.Append(memberIndent).Append("public static global::PatternKit.Application.ServiceLayer.ServiceLayerOperation<")
162+
.Append(requestName).Append(", ").Append(responseName).Append("> ").Append(factoryName).AppendLine("()");
163+
sb.AppendLine(memberIndent + "{");
164+
sb.Append(bodyIndent).Append("var builder = global::PatternKit.Application.ServiceLayer.ServiceLayerOperation<")
165+
.Append(requestName).Append(", ").Append(responseName).Append(">.Create(\"").Append(Escape(operationName)).AppendLine("\");");
166+
foreach (var rule in rules)
167+
sb.Append(bodyIndent).Append("builder.Require(\"").Append(Escape(rule.Code)).Append("\", \"").Append(Escape(rule.Message)).Append("\", ").Append(rule.Method.Name).AppendLine(");");
168+
sb.Append(bodyIndent).Append("return builder.Handle(").Append(handlerName).AppendLine(").Build();");
169+
sb.AppendLine(memberIndent + "}");
170+
sb.AppendLine(indent + "}");
171+
for (var i = containingTypes.Length - 1; i >= 0; i--)
172+
{
173+
sb.AppendLine(new string(' ', i * 4) + "}");
174+
}
175+
176+
return sb.ToString();
177+
}
178+
179+
private static INamedTypeSymbol[] GetContainingTypes(INamedTypeSymbol type)
180+
{
181+
var containingTypes = new Stack<INamedTypeSymbol>();
182+
for (var current = type.ContainingType; current is not null; current = current.ContainingType)
183+
{
184+
containingTypes.Push(current);
185+
}
186+
187+
return containingTypes.ToArray();
188+
}
189+
190+
private static void AppendTypeDeclaration(StringBuilder sb, INamedTypeSymbol type, int indentLevel)
191+
{
192+
sb.Append(new string(' ', indentLevel * 4));
145193
sb.Append(GetAccessibility(type.DeclaredAccessibility)).Append(' ');
146194
if (type.IsStatic)
147195
sb.Append("static ");
148196
else if (type.IsAbstract && type.TypeKind == TypeKind.Class)
149197
sb.Append("abstract ");
150198
else if (type.IsSealed && type.TypeKind == TypeKind.Class)
151199
sb.Append("sealed ");
152-
sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name).AppendLine();
153-
sb.AppendLine("{");
154-
sb.Append(" public static global::PatternKit.Application.ServiceLayer.ServiceLayerOperation<")
155-
.Append(requestName).Append(", ").Append(responseName).Append("> ").Append(factoryName).AppendLine("()");
156-
sb.AppendLine(" {");
157-
sb.Append(" var builder = global::PatternKit.Application.ServiceLayer.ServiceLayerOperation<")
158-
.Append(requestName).Append(", ").Append(responseName).Append(">.Create(\"").Append(Escape(operationName)).AppendLine("\");");
159-
foreach (var rule in rules)
160-
sb.Append(" builder.Require(\"").Append(Escape(rule.Code)).Append("\", \"").Append(Escape(rule.Message)).Append("\", ").Append(rule.Method.Name).AppendLine(");");
161-
sb.Append(" return builder.Handle(").Append(handlerName).AppendLine(").Build();");
162-
sb.AppendLine(" }");
163-
sb.AppendLine("}");
164-
return sb.ToString();
200+
sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name);
165201
}
166202

167203
private static bool IsHandler(IMethodSymbol method, INamedTypeSymbol requestType, INamedTypeSymbol responseType)

test/PatternKit.Generators.Tests/IdentityMapGeneratorTests.cs

Lines changed: 111 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,28 +29,50 @@ public static partial class OrderIdentityMap
2929
ScenarioExpect.Empty(result.Diagnostics);
3030
ScenarioExpect.Contains("CreateMap()", ScenarioExpect.Single(result.GeneratedSources));
3131
ScenarioExpect.Contains("IdentityMap<global::Demo.Order, string>.Create(SelectKey).Build()", result.GeneratedSources[0]);
32+
ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics));
3233
})
3334
.AssertPassed();
3435

35-
[Scenario("Generator emits internal struct factory in the global namespace")]
36+
[Scenario("Generator emits identity map defaults and host shapes")]
3637
[Fact]
37-
public Task Generator_Emits_Internal_Struct_Factory_In_The_Global_Namespace()
38-
=> Given("a valid internal struct declaration", () => Compile("""
38+
public Task Generator_Emits_Identity_Map_Defaults_And_Host_Shapes()
39+
=> Given("valid identity map declarations with default names and host shapes", () => Compile("""
3940
using PatternKit.Generators.IdentityMap;
41+
namespace Demo;
4042
public sealed record Order(string Id);
43+
44+
[GenerateIdentityMap(typeof(Order), typeof(string))]
45+
internal abstract partial class AbstractIdentityMap
46+
{
47+
[IdentityMapKeySelector]
48+
private static string SelectKey(Order order) => order.Id;
49+
}
50+
4151
[GenerateIdentityMap(typeof(Order), typeof(string))]
42-
internal partial struct OrderIdentityMap
52+
public sealed partial class SealedIdentityMap
53+
{
54+
[IdentityMapKeySelector]
55+
private static string SelectKey(Order order) => order.Id;
56+
}
57+
58+
[GenerateIdentityMap(typeof(Order), typeof(string))]
59+
internal partial struct StructIdentityMap
4360
{
4461
[IdentityMapKeySelector]
4562
private static string SelectKey(Order order) => order.Id;
4663
}
4764
"""))
48-
.Then("generated source keeps the declaration shape and default factory name", result =>
65+
.Then("generated source keeps declaration shapes and default factory names", result =>
4966
{
5067
ScenarioExpect.Empty(result.Diagnostics);
51-
ScenarioExpect.Contains("internal partial struct OrderIdentityMap", ScenarioExpect.Single(result.GeneratedSources));
52-
ScenarioExpect.Contains("Create()", result.GeneratedSources[0]);
53-
ScenarioExpect.DoesNotContain("namespace", result.GeneratedSources[0]);
68+
ScenarioExpect.Equal(3, result.GeneratedSources.Count);
69+
70+
var combined = string.Join("\n", result.GeneratedSources);
71+
ScenarioExpect.Contains("internal abstract partial class AbstractIdentityMap", combined);
72+
ScenarioExpect.Contains("public sealed partial class SealedIdentityMap", combined);
73+
ScenarioExpect.Contains("internal partial struct StructIdentityMap", combined);
74+
ScenarioExpect.Contains("Create()", combined);
75+
ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics));
5476
})
5577
.AssertPassed();
5678

@@ -61,6 +83,8 @@ internal partial struct OrderIdentityMap
6183
[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")]
6284
[InlineData("public static partial class OrderIdentityMap { [IdentityMapKeySelector] private static int SelectKey(Order order) => 1; }", "PKIM003")]
6385
[InlineData("public static partial class OrderIdentityMap { [IdentityMapKeySelector] private string SelectKey(Order order) => order.Id; }", "PKIM003")]
86+
[InlineData("public static partial class OrderIdentityMap { [IdentityMapKeySelector] private static string SelectKey() => string.Empty; }", "PKIM003")]
87+
[InlineData("public static partial class OrderIdentityMap { [IdentityMapKeySelector] private static string SelectKey(string order) => order; }", "PKIM003")]
6488
public Task Generator_Reports_Invalid_Identity_Map_Declarations(string declaration, string diagnosticId)
6589
=> Given("an invalid identity map declaration", () => Compile($$"""
6690
using PatternKit.Generators.IdentityMap;
@@ -72,18 +96,94 @@ public sealed record Order(string Id);
7296
ScenarioExpect.Contains(result.Diagnostics, diagnostic => diagnostic.Id == diagnosticId))
7397
.AssertPassed();
7498

99+
[Scenario("Generator emits nested identity map host wrappers")]
100+
[Fact]
101+
public Task Generator_Emits_Nested_Identity_Map_Host_Wrappers()
102+
=> Given("nested identity map declarations", () => Compile("""
103+
using PatternKit.Generators.IdentityMap;
104+
namespace Demo;
105+
public sealed record Order(string Id);
106+
107+
public partial class IdentityMapContainer
108+
{
109+
private partial class PrivateHost
110+
{
111+
[GenerateIdentityMap(typeof(Order), typeof(string))]
112+
protected partial class ProtectedIdentityMap
113+
{
114+
[IdentityMapKeySelector]
115+
private static string SelectKey(Order order) => order.Id;
116+
}
117+
118+
[GenerateIdentityMap(typeof(Order), typeof(string))]
119+
private protected partial class PrivateProtectedIdentityMap
120+
{
121+
[IdentityMapKeySelector]
122+
private static string SelectKey(Order order) => order.Id;
123+
}
124+
125+
[GenerateIdentityMap(typeof(Order), typeof(string))]
126+
protected internal partial class ProtectedInternalIdentityMap
127+
{
128+
[IdentityMapKeySelector]
129+
private static string SelectKey(Order order) => order.Id;
130+
}
131+
}
132+
}
133+
"""))
134+
.Then("generated sources preserve containing partial type wrappers", result =>
135+
{
136+
ScenarioExpect.Empty(result.Diagnostics);
137+
ScenarioExpect.Equal(3, result.GeneratedSources.Count);
138+
139+
var combined = string.Join("\n", result.GeneratedSources);
140+
ScenarioExpect.Contains("public partial class IdentityMapContainer", combined);
141+
ScenarioExpect.Contains("private partial class PrivateHost", combined);
142+
ScenarioExpect.Contains("protected partial class ProtectedIdentityMap", combined);
143+
ScenarioExpect.Contains("private protected partial class PrivateProtectedIdentityMap", combined);
144+
ScenarioExpect.Contains("protected internal partial class ProtectedInternalIdentityMap", combined);
145+
ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics));
146+
})
147+
.AssertPassed();
148+
149+
[Scenario("Generator skips malformed identity map type arguments")]
150+
[Theory]
151+
[InlineData("null!", "typeof(string)")]
152+
[InlineData("typeof(Order)", "null!")]
153+
public Task Generator_Skips_Malformed_Identity_Map_Type_Arguments(string entityType, string keyType)
154+
=> Given("an identity map declaration with a null type argument", () => Compile($$"""
155+
using PatternKit.Generators.IdentityMap;
156+
public sealed record Order(string Id);
157+
[GenerateIdentityMap({{entityType}}, {{keyType}})]
158+
public static partial class OrderIdentityMap
159+
{
160+
[IdentityMapKeySelector]
161+
private static string SelectKey(Order order) => order.Id;
162+
}
163+
"""))
164+
.Then("no source is generated", result =>
165+
ScenarioExpect.Empty(result.GeneratedSources))
166+
.AssertPassed();
167+
75168
private static GeneratorResult Compile(string source)
76169
{
77170
var compilation = RoslynTestHelpers.CreateCompilation(
78171
source,
79172
"IdentityMapGeneratorTests",
80173
extra: MetadataReference.CreateFromFile(typeof(IdentityMap<,>).Assembly.Location));
81-
_ = RoslynTestHelpers.Run(compilation, new IdentityMapGenerator(), out var run, out _);
174+
_ = RoslynTestHelpers.Run(compilation, new IdentityMapGenerator(), out var run, out var updated);
82175
var result = run.Results.Single();
176+
var emit = updated.Emit(Stream.Null);
83177
return new GeneratorResult(
84178
result.Diagnostics.ToArray(),
85-
result.GeneratedSources.Select(static source => source.SourceText.ToString()).ToArray());
179+
result.GeneratedSources.Select(static source => source.SourceText.ToString()).ToArray(),
180+
emit.Success,
181+
emit.Diagnostics.Select(static diagnostic => diagnostic.ToString()).ToArray());
86182
}
87183

88-
private sealed record GeneratorResult(IReadOnlyList<Diagnostic> Diagnostics, IReadOnlyList<string> GeneratedSources);
184+
private sealed record GeneratorResult(
185+
IReadOnlyList<Diagnostic> Diagnostics,
186+
IReadOnlyList<string> GeneratedSources,
187+
bool EmitSuccess,
188+
IReadOnlyList<string> EmitDiagnostics);
89189
}

0 commit comments

Comments
 (0)