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 @@ -94,22 +94,58 @@ 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.Messaging.CompetingConsumers.CompetingConsumerGroup<").Append(messageTypeName).Append(", ").Append(resultTypeName).Append(">.Builder ").Append(factoryMethodName).AppendLine("()");
sb.AppendLine(memberIndent + "{");
sb.Append(bodyIndent).Append("return global::PatternKit.Messaging.CompetingConsumers.CompetingConsumerGroup<").Append(messageTypeName).Append(", ").Append(resultTypeName).Append(">.Create(\"").Append(Escape(groupName)).AppendLine("\")");
sb.Append(bodyIndent).Append(" .WithMaxConcurrentDeliveries(").Append(maxConcurrentDeliveries).AppendLine(");");
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.Messaging.CompetingConsumers.CompetingConsumerGroup<").Append(messageTypeName).Append(", ").Append(resultTypeName).Append(">.Builder ").Append(factoryMethodName).AppendLine("()");
sb.AppendLine(" {");
sb.Append(" return global::PatternKit.Messaging.CompetingConsumers.CompetingConsumerGroup<").Append(messageTypeName).Append(", ").Append(resultTypeName).Append(">.Create(\"").Append(Escape(groupName)).AppendLine("\")");
sb.Append(" .WithMaxConcurrentDeliveries(").Append(maxConcurrentDeliveries).AppendLine(");");
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name);
}

private static string Escape(string value) => value.Replace("\\", "\\\\").Replace("\"", "\\\"");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ public sealed partial class CompetingConsumerGroupGeneratorTests(ITestOutputHelp
public Task Generates_Competing_Consumer_Group_Builder_Factory()
=> Given("a competing consumer group declaration", () => Compile("""
using PatternKit.Generators.Messaging;

namespace Demo;

public sealed record FulfillmentWork(string OrderId);
public sealed record FulfillmentResult(string OrderId, string Consumer);

[GenerateCompetingConsumerGroup(typeof(FulfillmentWork), typeof(FulfillmentResult), FactoryMethodName = "Build", GroupName = "fulfillment-consumers", MaxConcurrentDeliveries = 4)]
public static partial class FulfillmentConsumers;
"""))
Expand Down Expand Up @@ -55,6 +58,103 @@ public static partial class ConsumerHost;
})
.AssertPassed();

[Scenario("Generates competing consumer group defaults and type shapes")]
[Fact]
public Task Generates_Competing_Consumer_Group_Defaults_And_Type_Shapes()
=> Given("competing consumer declarations using default names and different host shapes", () => Compile("""
using PatternKit.Generators.Messaging;

namespace Demo;

public sealed record FulfillmentWork(string OrderId);
public sealed record FulfillmentResult(string OrderId, string Consumer);

[GenerateCompetingConsumerGroup(typeof(FulfillmentWork), typeof(FulfillmentResult))]
internal abstract partial class AbstractConsumers;

[GenerateCompetingConsumerGroup(typeof(FulfillmentWork), typeof(FulfillmentResult), GroupName = "tenant\\\"consumers", MaxConcurrentDeliveries = 3)]
public sealed partial class SealedConsumers;

[GenerateCompetingConsumerGroup(typeof(FulfillmentWork), typeof(FulfillmentResult))]
internal partial struct StructConsumers;
"""))
.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 AbstractConsumers", combined);
ScenarioExpect.Contains("Create()", combined);
ScenarioExpect.Contains("Create(\"competing-consumers\")", combined);
ScenarioExpect.Contains(".WithMaxConcurrentDeliveries(1)", combined);
ScenarioExpect.Contains("public sealed partial class SealedConsumers", combined);
ScenarioExpect.Contains("Create(\"tenant\\\\\\\"consumers\")", combined);
ScenarioExpect.Contains(".WithMaxConcurrentDeliveries(3)", combined);
ScenarioExpect.Contains("internal partial struct StructConsumers", combined);
ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics));
})
.AssertPassed();

[Scenario("Generates nested competing consumer group host wrappers")]
[Fact]
public Task Generates_Nested_Competing_Consumer_Group_Host_Wrappers()
=> Given("nested competing consumer declarations with non-public accessibility", () => Compile("""
using PatternKit.Generators.Messaging;

namespace Demo;

public sealed record FulfillmentWork(string OrderId);
public sealed record FulfillmentResult(string OrderId, string Consumer);

public partial class ConsumerContainer
{
private partial class PrivateHost
{
[GenerateCompetingConsumerGroup(typeof(FulfillmentWork), typeof(FulfillmentResult))]
protected partial class ProtectedConsumers;

[GenerateCompetingConsumerGroup(typeof(FulfillmentWork), typeof(FulfillmentResult))]
private protected partial class PrivateProtectedConsumers;

[GenerateCompetingConsumerGroup(typeof(FulfillmentWork), typeof(FulfillmentResult))]
protected internal partial class ProtectedInternalConsumers;
}
}
"""))
.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 ConsumerContainer", combined);
ScenarioExpect.Contains("private partial class PrivateHost", combined);
ScenarioExpect.Contains("protected partial class ProtectedConsumers", combined);
ScenarioExpect.Contains("private protected partial class PrivateProtectedConsumers", combined);
ScenarioExpect.Contains("protected internal partial class ProtectedInternalConsumers", combined);
ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics));
})
.AssertPassed();

[Scenario("Skips malformed competing consumer group type arguments")]
[Theory]
[InlineData("null!", "typeof(FulfillmentResult)")]
[InlineData("typeof(FulfillmentWork)", "null!")]
public Task Skips_Malformed_Competing_Consumer_Group_Type_Arguments(string messageType, string resultType)
=> Given("a competing consumer declaration with a null type argument", () => Compile($$"""
using PatternKit.Generators.Messaging;

public sealed record FulfillmentWork(string OrderId);
public sealed record FulfillmentResult(string OrderId, string Consumer);

[GenerateCompetingConsumerGroup({{messageType}}, {{resultType}})]
public static partial class FulfillmentConsumers;
"""))
.Then("no source is generated", result =>
ScenarioExpect.Empty(result.GeneratedSources))
.AssertPassed();

private static GeneratorResult Compile(string source)
{
var compilation = RoslynTestHelpers.CreateCompilation(
Expand Down
Loading