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
Original file line number Diff line number Diff line change
Expand Up @@ -101,21 +101,59 @@ 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);
var indent = new string(' ', indentLevel * 4);
sb.AppendLine();
sb.AppendLine(indent + "{");
var memberIndent = indent + " ";
var bodyIndent = memberIndent + " ";
sb.Append(memberIndent).Append("public static global::PatternKit.Application.TableDataGateway.InMemoryTableDataGateway<")
.Append(rowName).Append(", ").Append(keyName).Append("> ").Append(factoryName).AppendLine("()");
sb.AppendLine(memberIndent + "{");
sb.Append(bodyIndent).Append("return global::PatternKit.Application.TableDataGateway.InMemoryTableDataGateway<")
.Append(rowName).Append(", ").Append(keyName).Append(">.Create(\"").Append(Escape(tableName)).Append("\", ").Append(selectorName).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.TableDataGateway.InMemoryTableDataGateway<")
.Append(rowName).Append(", ").Append(keyName).Append("> ").Append(factoryName).AppendLine("()");
sb.Append(" => global::PatternKit.Application.TableDataGateway.InMemoryTableDataGateway<")
.Append(rowName).Append(", ").Append(keyName).Append(">.Create(\"").Append(Escape(tableName)).Append("\", ").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 rowType, INamedTypeSymbol keyType)
Expand Down
193 changes: 185 additions & 8 deletions test/PatternKit.Generators.Tests/TableDataGatewayGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ public sealed partial class TableDataGatewayGeneratorTests(ITestOutputHelper out
public Task Generator_Emits_Table_Data_Gateway_Factory()
=> Given("a valid table data gateway declaration", () => Compile("""
using PatternKit.Generators.TableDataGateway;

namespace Demo;

public sealed record OrderRow(string OrderId);

[GenerateTableDataGateway(typeof(OrderRow), typeof(string), FactoryName = "Build", TableName = "orders")]
public static partial class OrderTableGateway
{
Expand All @@ -28,38 +31,212 @@ public static partial class OrderTableGateway
{
ScenarioExpect.Empty(result.Diagnostics);
var source = ScenarioExpect.Single(result.GeneratedSources);
ScenarioExpect.Contains("public static partial class OrderTableGateway", source);
ScenarioExpect.Contains("Build()", source);
ScenarioExpect.Contains("Create(\"orders\", SelectKey).Build()", source);
ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics));
})
.AssertPassed();

[Scenario("Generator reports invalid table data gateway declarations")]
[Scenario("Generator reports non-partial table data gateway declarations")]
[Fact]
public Task Generator_Reports_Non_Partial_Table_Data_Gateway_Declarations()
=> Given("a non-partial table data gateway declaration", () => Compile("""
using PatternKit.Generators.TableDataGateway;

public sealed record OrderRow(string OrderId);

[GenerateTableDataGateway(typeof(OrderRow), typeof(string))]
public static class OrderTableGateway
{
[TableGatewayKeySelector]
private static string SelectKey(OrderRow row) => row.OrderId;
}
"""))
.Then("the diagnostic identifies the host", result =>
ScenarioExpect.Contains(result.Diagnostics, diagnostic => diagnostic.Id == "PKTDG001"))
.AssertPassed();

[Scenario("Generator reports missing or duplicate table data gateway key selectors")]
[Theory]
[InlineData("public static class OrderTableGateway { [TableGatewayKeySelector] private static string SelectKey(OrderRow row) => row.OrderId; }", "PKTDG001")]
[InlineData("public static partial class OrderTableGateway;", "PKTDG002")]
[InlineData("public static partial class OrderTableGateway { [TableGatewayKeySelector] private static string One(OrderRow row) => row.OrderId; [TableGatewayKeySelector] private static string Two(OrderRow row) => row.OrderId; }", "PKTDG002")]
[InlineData("public static partial class OrderTableGateway { [TableGatewayKeySelector] private static int SelectKey(OrderRow row) => 1; }", "PKTDG003")]
public Task Generator_Reports_Invalid_Table_Data_Gateway_Declarations(string declaration, string diagnosticId)
=> Given("an invalid table data gateway declaration", () => Compile($$"""
public Task Generator_Reports_Missing_Or_Duplicate_Table_Data_Gateway_Key_Selectors(string declaration, string diagnosticId)
=> Given("a table data gateway declaration with an invalid selector count", () => Compile($$"""
using PatternKit.Generators.TableDataGateway;

public sealed record OrderRow(string OrderId);

[GenerateTableDataGateway(typeof(OrderRow), typeof(string))]
{{declaration}}
"""))
.Then("the expected diagnostic is reported", result =>
ScenarioExpect.Contains(result.Diagnostics, diagnostic => diagnostic.Id == diagnosticId))
.AssertPassed();

[Scenario("Generator reports invalid table data gateway key selector signatures")]
[Theory]
[InlineData("[TableGatewayKeySelector] private string SelectKey(OrderRow row) => row.OrderId;")]
[InlineData("[TableGatewayKeySelector] private static T SelectKey<T>(OrderRow row) => default!;")]
[InlineData("[TableGatewayKeySelector] private static string SelectKey() => \"missing\";")]
[InlineData("[TableGatewayKeySelector] private static string SelectKey(OrderRow row, string tenant) => row.OrderId;")]
[InlineData("[TableGatewayKeySelector] private static string SelectKey(string row) => row;")]
[InlineData("[TableGatewayKeySelector] private static int SelectKey(OrderRow row) => 1;")]
public Task Generator_Reports_Invalid_Table_Data_Gateway_Key_Selector_Signatures(string selector)
=> Given("a table data gateway declaration with an invalid selector signature", () => Compile($$"""
using PatternKit.Generators.TableDataGateway;

public sealed record OrderRow(string OrderId);

[GenerateTableDataGateway(typeof(OrderRow), typeof(string))]
public partial class OrderTableGateway
{
{{selector}}
}
"""))
.Then("the expected diagnostic is reported", result =>
ScenarioExpect.Contains(result.Diagnostics, diagnostic => diagnostic.Id == "PKTDG003"))
.AssertPassed();

[Scenario("Generator emits table data gateway defaults and type shapes")]
[Fact]
public Task Generator_Emits_Table_Data_Gateway_Defaults_And_Type_Shapes()
=> Given("table data gateway declarations using default names and different host shapes", () => Compile("""
using PatternKit.Generators.TableDataGateway;

namespace Demo;

public sealed record OrderRow(string OrderId);

[GenerateTableDataGateway(typeof(OrderRow), typeof(string))]
internal abstract partial class AbstractGateway
{
[TableGatewayKeySelector]
private static string SelectKey(OrderRow row) => row.OrderId;
}

[GenerateTableDataGateway(typeof(OrderRow), typeof(string), TableName = "tenant\\\"orders")]
public sealed partial class SealedGateway
{
[TableGatewayKeySelector]
private static string SelectKey(OrderRow row) => row.OrderId;
}

[GenerateTableDataGateway(typeof(OrderRow), typeof(string))]
internal partial struct StructGateway
{
[TableGatewayKeySelector]
private static string SelectKey(OrderRow row) => row.OrderId;
}
"""))
.Then("generated sources preserve host shape and configured names", result =>
{
ScenarioExpect.Empty(result.Diagnostics);
ScenarioExpect.Equal(3, result.GeneratedSources.Count);

var combined = string.Join("\n", result.GeneratedSources);
ScenarioExpect.Contains("internal abstract partial class AbstractGateway", combined);
ScenarioExpect.Contains("Create()", combined);
ScenarioExpect.Contains("Create(\"AbstractGateway\", SelectKey).Build()", combined);
ScenarioExpect.Contains("public sealed partial class SealedGateway", combined);
ScenarioExpect.Contains("Create(\"tenant\\\\\\\"orders\", SelectKey).Build()", combined);
ScenarioExpect.Contains("internal partial struct StructGateway", combined);
ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics));
})
.AssertPassed();

[Scenario("Generator emits nested table data gateway host wrappers")]
[Fact]
public Task Generator_Emits_Nested_Table_Data_Gateway_Host_Wrappers()
=> Given("nested table data gateway declarations with non-public accessibility", () => Compile("""
using PatternKit.Generators.TableDataGateway;

namespace Demo;

public sealed record OrderRow(string OrderId);

public partial class GatewayContainer
{
private partial class PrivateHost
{
[GenerateTableDataGateway(typeof(OrderRow), typeof(string))]
protected partial class ProtectedGateway
{
[TableGatewayKeySelector]
private static string SelectKey(OrderRow row) => row.OrderId;
}

[GenerateTableDataGateway(typeof(OrderRow), typeof(string))]
private protected partial class PrivateProtectedGateway
{
[TableGatewayKeySelector]
private static string SelectKey(OrderRow row) => row.OrderId;
}

[GenerateTableDataGateway(typeof(OrderRow), typeof(string))]
protected internal partial class ProtectedInternalGateway
{
[TableGatewayKeySelector]
private static string SelectKey(OrderRow row) => row.OrderId;
}
}
}
"""))
.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 GatewayContainer", combined);
ScenarioExpect.Contains("private partial class PrivateHost", combined);
ScenarioExpect.Contains("protected partial class ProtectedGateway", combined);
ScenarioExpect.Contains("private protected partial class PrivateProtectedGateway", combined);
ScenarioExpect.Contains("protected internal partial class ProtectedInternalGateway", combined);
ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics));
})
.AssertPassed();

[Scenario("Generator skips malformed table data gateway type arguments")]
[Theory]
[InlineData("null!", "typeof(string)")]
[InlineData("typeof(OrderRow)", "null!")]
public Task Generator_Skips_Malformed_Table_Data_Gateway_Type_Arguments(string rowType, string keyType)
=> Given("a table data gateway declaration with a null type argument", () => Compile($$"""
using PatternKit.Generators.TableDataGateway;

public sealed record OrderRow(string OrderId);

[GenerateTableDataGateway({{rowType}}, {{keyType}})]
public static partial class OrderTableGateway
{
[TableGatewayKeySelector]
private static string SelectKey(OrderRow row) => row.OrderId;
}
"""))
.Then("no source is generated", result =>
ScenarioExpect.Empty(result.GeneratedSources))
.AssertPassed();

private static GeneratorResult Compile(string source)
{
var compilation = RoslynTestHelpers.CreateCompilation(
source,
"TableDataGatewayGeneratorTests",
extra: MetadataReference.CreateFromFile(typeof(InMemoryTableDataGateway<,>).Assembly.Location));
_ = RoslynTestHelpers.Run(compilation, new TableDataGatewayGenerator(), out var run, out _);
_ = RoslynTestHelpers.Run(compilation, new TableDataGatewayGenerator(), out var run, out var updated);
var result = run.Results.Single();
return new GeneratorResult(result.Diagnostics.ToArray(), result.GeneratedSources.Select(static source => source.SourceText.ToString()).ToArray());
var emit = updated.Emit(Stream.Null);
return new GeneratorResult(
result.Diagnostics.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