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 @@ -76,21 +76,57 @@ 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.PipesAndFilters.PipesAndFiltersPipeline<").Append(contextTypeName).Append(">.Builder ").Append(factoryMethodName).AppendLine("()");
sb.AppendLine(memberIndent + "{");
sb.Append(bodyIndent).Append("return global::PatternKit.Messaging.PipesAndFilters.PipesAndFiltersPipeline<").Append(contextTypeName).Append(">.Create(\"").Append(Escape(pipelineName)).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.PipesAndFilters.PipesAndFiltersPipeline<").Append(contextTypeName).Append(">.Builder ").Append(factoryMethodName).AppendLine("()");
sb.AppendLine(" {");
sb.Append(" return global::PatternKit.Messaging.PipesAndFilters.PipesAndFiltersPipeline<").Append(contextTypeName).Append(">.Create(\"").Append(Escape(pipelineName)).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 @@ -42,6 +42,94 @@ public static class PipelineHost;
ScenarioExpect.Contains(result.Diagnostics, diagnostic => diagnostic.Id == "PKPF001"))
.AssertPassed();

[Scenario("Generates pipes and filters host defaults and type shapes")]
[Fact]
public Task Generates_Pipes_And_Filters_Host_Defaults_And_Type_Shapes()
=> Given("pipeline declarations using default names and different host shapes", () => Compile("""
using PatternKit.Generators.Messaging;

namespace Demo;

public sealed record FulfillmentContext(string OrderId);

[GeneratePipesAndFiltersPipeline(typeof(FulfillmentContext))]
internal abstract partial class AbstractPipeline;

[GeneratePipesAndFiltersPipeline(typeof(FulfillmentContext), PipelineName = "tenant\\\"pipeline")]
public sealed partial class SealedPipeline;

[GeneratePipesAndFiltersPipeline(typeof(FulfillmentContext))]
internal partial struct StructPipeline;
"""))
.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 AbstractPipeline", combined);
ScenarioExpect.Contains("Create()", combined);
ScenarioExpect.Contains("Create(\"pipes-and-filters\")", combined);
ScenarioExpect.Contains("public sealed partial class SealedPipeline", combined);
ScenarioExpect.Contains("Create(\"tenant\\\\\\\"pipeline\")", combined);
ScenarioExpect.Contains("internal partial struct StructPipeline", combined);
ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics));
})
.AssertPassed();

[Scenario("Generates nested pipes and filters host wrappers")]
[Fact]
public Task Generates_Nested_Pipes_And_Filters_Host_Wrappers()
=> Given("nested pipeline declarations with non-public accessibility", () => Compile("""
using PatternKit.Generators.Messaging;

namespace Demo;

public sealed record FulfillmentContext(string OrderId);

public partial class PipelineContainer
{
private partial class PrivateHost
{
[GeneratePipesAndFiltersPipeline(typeof(FulfillmentContext))]
protected partial class ProtectedPipeline;

[GeneratePipesAndFiltersPipeline(typeof(FulfillmentContext))]
private protected partial class PrivateProtectedPipeline;

[GeneratePipesAndFiltersPipeline(typeof(FulfillmentContext))]
protected internal partial class ProtectedInternalPipeline;
}
}
"""))
.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 PipelineContainer", combined);
ScenarioExpect.Contains("private partial class PrivateHost", combined);
ScenarioExpect.Contains("protected partial class ProtectedPipeline", combined);
ScenarioExpect.Contains("private protected partial class PrivateProtectedPipeline", combined);
ScenarioExpect.Contains("protected internal partial class ProtectedInternalPipeline", combined);
ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics));
})
.AssertPassed();

[Scenario("Skips malformed pipes and filters type arguments")]
[Fact]
public Task Skips_Malformed_Pipes_And_Filters_Type_Arguments()
=> Given("a pipeline declaration with a null context type", () => Compile("""
using PatternKit.Generators.Messaging;

[GeneratePipesAndFiltersPipeline(null!)]
public static partial class BrokenPipeline;
"""))
.Then("no source is generated", result =>
ScenarioExpect.Empty(result.GeneratedSources))
.AssertPassed();

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