Skip to content

Commit 40b9e43

Browse files
authored
test: cover data mapper generator hosts (#430)
1 parent 10995d8 commit 40b9e43

2 files changed

Lines changed: 256 additions & 22 deletions

File tree

src/PatternKit.Generators/DataMapping/DataMapperGenerator.cs

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -116,24 +116,60 @@ private static string GenerateSource(
116116
sb.AppendLine();
117117
}
118118

119+
var containingTypes = GetContainingTypes(type);
120+
var indentLevel = 0;
121+
foreach (var containingType in containingTypes)
122+
{
123+
AppendTypeDeclaration(sb, containingType, indentLevel);
124+
sb.AppendLine();
125+
sb.AppendLine(new string(' ', indentLevel * 4) + "{");
126+
indentLevel++;
127+
}
128+
129+
AppendTypeDeclaration(sb, type, indentLevel);
130+
var indent = new string(' ', indentLevel * 4);
131+
sb.AppendLine();
132+
sb.AppendLine(indent + "{");
133+
var memberIndent = indent + " ";
134+
var bodyIndent = memberIndent + " ";
135+
sb.Append(memberIndent).Append("public static global::PatternKit.Application.DataMapping.DataMapper<")
136+
.Append(domainName).Append(", ").Append(dataName).Append("> ").Append(factoryName).AppendLine("()");
137+
sb.Append(bodyIndent).Append("=> global::PatternKit.Application.DataMapping.DataMapper<")
138+
.Append(domainName).Append(", ").Append(dataName).AppendLine(">.Create()");
139+
sb.Append(bodyIndent).Append(" .MapToData(").Append(toDataName).AppendLine(")");
140+
sb.Append(bodyIndent).Append(" .MapToDomain(").Append(toDomainName).AppendLine(")");
141+
sb.AppendLine(bodyIndent + " .Build();");
142+
sb.AppendLine(indent + "}");
143+
for (var i = containingTypes.Length - 1; i >= 0; i--)
144+
{
145+
sb.AppendLine(new string(' ', i * 4) + "}");
146+
}
147+
148+
return sb.ToString();
149+
}
150+
151+
private static INamedTypeSymbol[] GetContainingTypes(INamedTypeSymbol type)
152+
{
153+
var containingTypes = new Stack<INamedTypeSymbol>();
154+
for (var current = type.ContainingType; current is not null; current = current.ContainingType)
155+
{
156+
containingTypes.Push(current);
157+
}
158+
159+
return containingTypes.ToArray();
160+
}
161+
162+
private static void AppendTypeDeclaration(StringBuilder sb, INamedTypeSymbol type, int indentLevel)
163+
{
164+
sb.Append(new string(' ', indentLevel * 4));
119165
sb.Append(GetAccessibility(type.DeclaredAccessibility)).Append(' ');
120166
if (type.IsStatic)
121167
sb.Append("static ");
122168
else if (type.IsAbstract && type.TypeKind == TypeKind.Class)
123169
sb.Append("abstract ");
124170
else if (type.IsSealed && type.TypeKind == TypeKind.Class)
125171
sb.Append("sealed ");
126-
sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name).AppendLine();
127-
sb.AppendLine("{");
128-
sb.Append(" public static global::PatternKit.Application.DataMapping.DataMapper<")
129-
.Append(domainName).Append(", ").Append(dataName).Append("> ").Append(factoryName).AppendLine("()");
130-
sb.Append(" => global::PatternKit.Application.DataMapping.DataMapper<")
131-
.Append(domainName).Append(", ").Append(dataName).AppendLine(">.Create()");
132-
sb.Append(" .MapToData(").Append(toDataName).AppendLine(")");
133-
sb.Append(" .MapToDomain(").Append(toDomainName).AppendLine(")");
134-
sb.AppendLine(" .Build();");
135-
sb.AppendLine("}");
136-
return sb.ToString();
172+
sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name);
137173
}
138174

139175
private static bool IsProjection(IMethodSymbol method, INamedTypeSymbol sourceType, INamedTypeSymbol targetType)

test/PatternKit.Generators.Tests/DataMapperGeneratorTests.cs

Lines changed: 209 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public static partial class OrderDataMapper
3838
ScenarioExpect.Contains("CreateMapper()", source);
3939
ScenarioExpect.Contains(".MapToData(ToData)", source);
4040
ScenarioExpect.Contains(".MapToDomain(ToDomain)", source);
41+
ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics));
4142
})
4243
.AssertPassed();
4344

@@ -65,25 +66,73 @@ public static class OrderDataMapper
6566
.AssertPassed();
6667

6768
[Scenario("Generator reports missing mapper projections")]
68-
[Fact]
69-
public Task Generator_Reports_Missing_Mapper_Projections()
70-
=> Given("a mapper declaration without projections", () => Compile("""
69+
[Theory]
70+
[InlineData("")]
71+
[InlineData("""
72+
[DataMapperToData]
73+
private static OrderRow ToData(DomainOrder order) => new(order.Id);
74+
[DataMapperToData]
75+
private static OrderRow ToDataAgain(DomainOrder order) => new(order.Id);
76+
[DataMapperToDomain]
77+
private static DomainOrder ToDomain(OrderRow row) => new(row.OrderId);
78+
""")]
79+
[InlineData("""
80+
[DataMapperToData]
81+
private static OrderRow ToData(DomainOrder order) => new(order.Id);
82+
[DataMapperToDomain]
83+
private static DomainOrder ToDomain(OrderRow row) => new(row.OrderId);
84+
[DataMapperToDomain]
85+
private static DomainOrder ToDomainAgain(OrderRow row) => new(row.OrderId);
86+
""")]
87+
public Task Generator_Reports_Missing_Mapper_Projections(string projections)
88+
=> Given("a mapper declaration with missing or duplicate projections", () => Compile($$"""
7189
using PatternKit.Generators.DataMapping;
7290
7391
public sealed record DomainOrder(string Id);
7492
public sealed record OrderRow(string OrderId);
7593
7694
[GenerateDataMapper(typeof(DomainOrder), typeof(OrderRow))]
77-
public static partial class OrderDataMapper;
95+
public static partial class OrderDataMapper
96+
{
97+
{{projections}}
98+
}
7899
"""))
79100
.Then("PKMAP002 is reported", result =>
80101
ScenarioExpect.Contains(result.Diagnostics, static diagnostic => diagnostic.Id == "PKMAP002"))
81102
.AssertPassed();
82103

83104
[Scenario("Generator reports invalid mapper projection signatures")]
105+
[Theory]
106+
[InlineData("[DataMapperToData] private OrderRow ToData(DomainOrder order) => new(order.Id);")]
107+
[InlineData("[DataMapperToData] private static T ToData<T>(DomainOrder order) => default!;")]
108+
[InlineData("[DataMapperToData] private static OrderRow ToData() => new(\"missing\");")]
109+
[InlineData("[DataMapperToData] private static OrderRow ToData(DomainOrder order, string tenant) => new(order.Id);")]
110+
[InlineData("[DataMapperToData] private static OrderRow ToData(OrderRow row) => row;")]
111+
[InlineData("[DataMapperToData] private static string ToData(DomainOrder order) => order.Id;")]
112+
public Task Generator_Reports_Invalid_To_Data_Projection_Signatures(string projection)
113+
=> Given("a mapper declaration with an invalid to-data projection", () => Compile($$"""
114+
using PatternKit.Generators.DataMapping;
115+
116+
public sealed record DomainOrder(string Id);
117+
public sealed record OrderRow(string OrderId);
118+
119+
[GenerateDataMapper(typeof(DomainOrder), typeof(OrderRow))]
120+
public static partial class OrderDataMapper
121+
{
122+
{{projection}}
123+
124+
[DataMapperToDomain]
125+
private static DomainOrder ToDomain(OrderRow row) => new(row.OrderId);
126+
}
127+
"""))
128+
.Then("PKMAP003 is reported", result =>
129+
ScenarioExpect.Contains(result.Diagnostics, static diagnostic => diagnostic.Id == "PKMAP003"))
130+
.AssertPassed();
131+
132+
[Scenario("Generator reports invalid to-domain mapper projection signatures")]
84133
[Fact]
85-
public Task Generator_Reports_Invalid_Mapper_Projection_Signatures()
86-
=> Given("a mapper declaration with an invalid projection", () => Compile("""
134+
public Task Generator_Reports_Invalid_To_Domain_Mapper_Projection_Signatures()
135+
=> Given("a mapper declaration with an invalid to-domain projection", () => Compile("""
87136
using PatternKit.Generators.DataMapping;
88137
89138
public sealed record DomainOrder(string Id);
@@ -93,28 +142,177 @@ public sealed record OrderRow(string OrderId);
93142
public static partial class OrderDataMapper
94143
{
95144
[DataMapperToData]
96-
private static string ToData(DomainOrder order) => order.Id;
145+
private static OrderRow ToData(DomainOrder order) => new(order.Id);
97146
98147
[DataMapperToDomain]
99-
private static DomainOrder ToDomain(OrderRow row) => new(row.OrderId);
148+
private static OrderRow ToDomain(OrderRow row) => row;
100149
}
101150
"""))
102151
.Then("PKMAP003 is reported", result =>
103152
ScenarioExpect.Contains(result.Diagnostics, static diagnostic => diagnostic.Id == "PKMAP003"))
104153
.AssertPassed();
105154

155+
[Scenario("Generator emits mapper defaults and type shapes")]
156+
[Fact]
157+
public Task Generator_Emits_Mapper_Defaults_And_Type_Shapes()
158+
=> Given("mapper declarations using default names and different host shapes", () => Compile("""
159+
using PatternKit.Generators.DataMapping;
160+
161+
namespace Demo;
162+
163+
public sealed record DomainOrder(string Id);
164+
public sealed record OrderRow(string OrderId);
165+
166+
[GenerateDataMapper(typeof(DomainOrder), typeof(OrderRow))]
167+
internal abstract partial class AbstractMapper
168+
{
169+
[DataMapperToData]
170+
private static OrderRow ToData(DomainOrder order) => new(order.Id);
171+
172+
[DataMapperToDomain]
173+
private static DomainOrder ToDomain(OrderRow row) => new(row.OrderId);
174+
}
175+
176+
[GenerateDataMapper(typeof(DomainOrder), typeof(OrderRow))]
177+
public sealed partial class SealedMapper
178+
{
179+
[DataMapperToData]
180+
private static OrderRow ToData(DomainOrder order) => new(order.Id);
181+
182+
[DataMapperToDomain]
183+
private static DomainOrder ToDomain(OrderRow row) => new(row.OrderId);
184+
}
185+
186+
[GenerateDataMapper(typeof(DomainOrder), typeof(OrderRow))]
187+
internal partial struct StructMapper
188+
{
189+
[DataMapperToData]
190+
private static OrderRow ToData(DomainOrder order) => new(order.Id);
191+
192+
[DataMapperToDomain]
193+
private static DomainOrder ToDomain(OrderRow row) => new(row.OrderId);
194+
}
195+
"""))
196+
.Then("generated sources preserve host shape and default factory names", result =>
197+
{
198+
ScenarioExpect.Empty(result.Diagnostics.Where(static d => d.Severity == DiagnosticSeverity.Error));
199+
ScenarioExpect.Equal(3, result.GeneratedSources.Count);
200+
201+
var combined = string.Join("\n", result.GeneratedSources);
202+
ScenarioExpect.Contains("internal abstract partial class AbstractMapper", combined);
203+
ScenarioExpect.Contains("DataMapper<global::Demo.DomainOrder, global::Demo.OrderRow> Create()", combined);
204+
ScenarioExpect.Contains("public sealed partial class SealedMapper", combined);
205+
ScenarioExpect.Contains("internal partial struct StructMapper", combined);
206+
ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics));
207+
})
208+
.AssertPassed();
209+
210+
[Scenario("Generator emits nested mapper host wrappers")]
211+
[Fact]
212+
public Task Generator_Emits_Nested_Mapper_Host_Wrappers()
213+
=> Given("nested mapper declarations with non-public accessibility", () => Compile("""
214+
using PatternKit.Generators.DataMapping;
215+
216+
namespace Demo;
217+
218+
public sealed record DomainOrder(string Id);
219+
public sealed record OrderRow(string OrderId);
220+
221+
public partial class MapperContainer
222+
{
223+
private partial class PrivateHost
224+
{
225+
[GenerateDataMapper(typeof(DomainOrder), typeof(OrderRow))]
226+
protected partial class ProtectedMapper
227+
{
228+
[DataMapperToData]
229+
private static OrderRow ToData(DomainOrder order) => new(order.Id);
230+
231+
[DataMapperToDomain]
232+
private static DomainOrder ToDomain(OrderRow row) => new(row.OrderId);
233+
}
234+
235+
[GenerateDataMapper(typeof(DomainOrder), typeof(OrderRow))]
236+
private protected partial class PrivateProtectedMapper
237+
{
238+
[DataMapperToData]
239+
private static OrderRow ToData(DomainOrder order) => new(order.Id);
240+
241+
[DataMapperToDomain]
242+
private static DomainOrder ToDomain(OrderRow row) => new(row.OrderId);
243+
}
244+
245+
[GenerateDataMapper(typeof(DomainOrder), typeof(OrderRow))]
246+
protected internal partial class ProtectedInternalMapper
247+
{
248+
[DataMapperToData]
249+
private static OrderRow ToData(DomainOrder order) => new(order.Id);
250+
251+
[DataMapperToDomain]
252+
private static DomainOrder ToDomain(OrderRow row) => new(row.OrderId);
253+
}
254+
}
255+
}
256+
"""))
257+
.Then("generated sources preserve containing partial type wrappers", result =>
258+
{
259+
ScenarioExpect.Empty(result.Diagnostics.Where(static d => d.Severity == DiagnosticSeverity.Error));
260+
ScenarioExpect.Equal(3, result.GeneratedSources.Count);
261+
262+
var combined = string.Join("\n", result.GeneratedSources);
263+
ScenarioExpect.Contains("public partial class MapperContainer", combined);
264+
ScenarioExpect.Contains("private partial class PrivateHost", combined);
265+
ScenarioExpect.Contains("protected partial class ProtectedMapper", combined);
266+
ScenarioExpect.Contains("private protected partial class PrivateProtectedMapper", combined);
267+
ScenarioExpect.Contains("protected internal partial class ProtectedInternalMapper", combined);
268+
ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics));
269+
})
270+
.AssertPassed();
271+
272+
[Scenario("Generator skips malformed mapper type arguments")]
273+
[Theory]
274+
[InlineData("null!", "typeof(OrderRow)")]
275+
[InlineData("typeof(DomainOrder)", "null!")]
276+
public Task Generator_Skips_Malformed_Mapper_Type_Arguments(string domainType, string dataType)
277+
=> Given("a mapper declaration with a null type argument", () => Compile($$"""
278+
using PatternKit.Generators.DataMapping;
279+
280+
public sealed record DomainOrder(string Id);
281+
public sealed record OrderRow(string OrderId);
282+
283+
[GenerateDataMapper({{domainType}}, {{dataType}})]
284+
public static partial class OrderDataMapper
285+
{
286+
[DataMapperToData]
287+
private static OrderRow ToData(DomainOrder order) => new(order.Id);
288+
289+
[DataMapperToDomain]
290+
private static DomainOrder ToDomain(OrderRow row) => new(row.OrderId);
291+
}
292+
"""))
293+
.Then("no source is generated", result =>
294+
ScenarioExpect.Empty(result.GeneratedSources))
295+
.AssertPassed();
296+
106297
private static GeneratorResult Compile(string source)
107298
{
108299
var compilation = RoslynTestHelpers.CreateCompilation(
109300
source,
110301
"DataMapperGeneratorTests",
111302
extra: MetadataReference.CreateFromFile(typeof(DataMapper<,>).Assembly.Location));
112-
_ = RoslynTestHelpers.Run(compilation, new DataMapperGenerator(), out var run, out _);
303+
_ = RoslynTestHelpers.Run(compilation, new DataMapperGenerator(), out var run, out var updated);
113304
var result = run.Results.Single();
305+
var emit = updated.Emit(Stream.Null);
114306
return new GeneratorResult(
115307
result.Diagnostics.ToArray(),
116-
result.GeneratedSources.Select(static source => source.SourceText.ToString()).ToArray());
308+
result.GeneratedSources.Select(static source => source.SourceText.ToString()).ToArray(),
309+
emit.Success,
310+
emit.Diagnostics.Select(static diagnostic => diagnostic.ToString()).ToArray());
117311
}
118312

119-
private sealed record GeneratorResult(IReadOnlyList<Diagnostic> Diagnostics, IReadOnlyList<string> GeneratedSources);
313+
private sealed record GeneratorResult(
314+
IReadOnlyList<Diagnostic> Diagnostics,
315+
IReadOnlyList<string> GeneratedSources,
316+
bool EmitSuccess,
317+
IReadOnlyList<string> EmitDiagnostics);
120318
}

0 commit comments

Comments
 (0)