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 @@ -123,27 +123,63 @@ 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);
sb.AppendLine();
var indent = new string(' ', indentLevel * 4);
sb.AppendLine(indent + "{");
var memberIndent = indent + " ";
var bodyIndent = memberIndent + " ";
sb.Append(memberIndent).Append("public static global::PatternKit.Application.FeatureToggles.FeatureToggleSet<").Append(contextTypeName).Append("> ").Append(factoryName).AppendLine("()");
sb.AppendLine(memberIndent + "{");
sb.Append(bodyIndent).Append("return global::PatternKit.Application.FeatureToggles.FeatureToggleSet<").Append(contextTypeName).Append(">.Create(\"").Append(Escape(setName)).AppendLine("\")");
foreach (var rule in rules)
{
sb.Append(bodyIndent).Append(" .AddRule(\"").Append(Escape(rule.Name)).Append("\", ").Append(rule.DefaultEnabled ? "true" : "false").Append(", ").Append(rule.Method.Name).AppendLine(")");
}

sb.Append(bodyIndent).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.FeatureToggles.FeatureToggleSet<").Append(contextTypeName).Append("> ").Append(factoryName).AppendLine("()");
sb.AppendLine(" {");
sb.Append(" return global::PatternKit.Application.FeatureToggles.FeatureToggleSet<").Append(contextTypeName).Append(">.Create(\"").Append(Escape(setName)).AppendLine("\")");
foreach (var rule in rules)
{
sb.Append(" .AddRule(\"").Append(Escape(rule.Name)).Append("\", ").Append(rule.DefaultEnabled ? "true" : "false").Append(", ").Append(rule.Method.Name).AppendLine(")");
}

sb.AppendLine(" .Build();");
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
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -158,27 +159,63 @@ 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);
sb.AppendLine();
var indent = new string(' ', indentLevel * 4);
sb.AppendLine(indent + "{");
var memberIndent = indent + " ";
var bodyIndent = memberIndent + " ";
sb.Append(memberIndent).Append("public static global::PatternKit.Cloud.GatewayAggregation.GatewayAggregation<")
.Append(requestTypeName).Append(", ").Append(responseTypeName).Append("> ").Append(factoryMethodName).AppendLine("()");
sb.AppendLine(memberIndent + "{");
sb.Append(bodyIndent).Append("return global::PatternKit.Cloud.GatewayAggregation.GatewayAggregation<")
.Append(requestTypeName).Append(", ").Append(responseTypeName).Append(">.Create(\"").Append(Escape(gatewayName)).AppendLine("\")");
foreach (var fetch in fetches)
sb.Append(bodyIndent).Append(" .Fetch<").Append(fetch.Method.ReturnType.ToDisplayString(TypeFormat)).Append(">(\"").Append(Escape(fetch.Name)).Append("\", ").Append(fetch.Method.Name).AppendLine(")");
sb.Append(bodyIndent).Append(" .Compose(").Append(composerName).AppendLine(")");
sb.Append(bodyIndent).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.Cloud.GatewayAggregation.GatewayAggregation<")
.Append(requestTypeName).Append(", ").Append(responseTypeName).Append("> ").Append(factoryMethodName).AppendLine("()");
sb.AppendLine(" {");
sb.Append(" return global::PatternKit.Cloud.GatewayAggregation.GatewayAggregation<")
.Append(requestTypeName).Append(", ").Append(responseTypeName).Append(">.Create(\"").Append(Escape(gatewayName)).AppendLine("\")");
foreach (var fetch in fetches)
sb.Append(" .Fetch<").Append(fetch.Method.ReturnType.ToDisplayString(TypeFormat)).Append(">(\"").Append(Escape(fetch.Name)).Append("\", ").Append(fetch.Method.Name).AppendLine(")");
sb.Append(" .Compose(").Append(composerName).AppendLine(")");
sb.AppendLine(" .Build();");
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name);
}

private static string? GetNamedString(AttributeData attribute, string name)
Expand Down
114 changes: 114 additions & 0 deletions test/PatternKit.Generators.Tests/FeatureToggleSetGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public static partial class CheckoutToggles
[Theory]
[InlineData("public static class CheckoutToggles { [FeatureToggleRule(\"x\")] private static bool IsEnabled(CheckoutContext context) => true; }", "PKFT001")]
[InlineData("public static partial class CheckoutToggles;", "PKFT002")]
[InlineData("public static partial class CheckoutToggles { [FeatureToggleRule(\"x\")] private static bool IsEnabled() => true; }", "PKFT003")]
[InlineData("public static partial class CheckoutToggles { [FeatureToggleRule(\"x\")] private static bool IsEnabled(string context) => true; }", "PKFT003")]
[InlineData("public static partial class CheckoutToggles { [FeatureToggleRule(\"x\")] private static string IsEnabled(CheckoutContext context) => \"yes\"; }", "PKFT003")]
[InlineData("public static partial class CheckoutToggles { [FeatureToggleRule(\"x\")] private bool IsEnabled(CheckoutContext context) => true; }", "PKFT003")]
public Task Generator_Reports_Invalid_Feature_Toggle_Declarations(string declaration, string diagnosticId)
Expand All @@ -56,6 +58,118 @@ public sealed record CheckoutContext(string Tenant, decimal Total);
ScenarioExpect.Contains(result.Diagnostics, diagnostic => diagnostic.Id == diagnosticId))
.AssertPassed();

[Scenario("Generator emits feature toggle defaults and host shapes")]
[Fact]
public Task Generator_Emits_Feature_Toggle_Defaults_And_Host_Shapes()
=> Given("feature toggle declarations with default names and host shapes", () => Compile("""
using PatternKit.Generators.FeatureToggles;
namespace Demo;
public sealed record CheckoutContext(string Tenant, decimal Total);

[GenerateFeatureToggleSet(typeof(CheckoutContext))]
internal abstract partial class AbstractToggles
{
[FeatureToggleRule("enabled")]
private static bool IsEnabled(CheckoutContext context) => true;
}

[GenerateFeatureToggleSet(typeof(CheckoutContext), SetName = "tenant\\\"toggles")]
public sealed partial class SealedToggles
{
[FeatureToggleRule("beta", DefaultEnabled = true)]
private static bool Beta(CheckoutContext context) => context.Tenant == "beta";
}

[GenerateFeatureToggleSet(typeof(CheckoutContext))]
internal partial struct StructToggles
{
[FeatureToggleRule("large-order")]
private static bool LargeOrder(CheckoutContext context) => context.Total >= 500m;
}
"""))
.Then("generated sources preserve host shape and configured defaults", result =>
{
ScenarioExpect.Empty(result.Diagnostics);
ScenarioExpect.Equal(3, result.GeneratedSources.Count);

var combined = string.Join("\n", result.GeneratedSources);
ScenarioExpect.Contains("internal abstract partial class AbstractToggles", combined);
ScenarioExpect.Contains("public sealed partial class SealedToggles", combined);
ScenarioExpect.Contains("internal partial struct StructToggles", combined);
ScenarioExpect.Contains("Create(\"feature-toggles\")", combined);
ScenarioExpect.Contains("Create(\"tenant\\\\\\\"toggles\")", combined);
ScenarioExpect.Contains(".AddRule(\"enabled\", false, IsEnabled)", combined);
ScenarioExpect.Contains(".AddRule(\"beta\", true, Beta)", combined);
ScenarioExpect.True(result.EmitSuccess, result.EmitDiagnostics);
})
.AssertPassed();

[Scenario("Generator emits nested feature toggle host wrappers")]
[Fact]
public Task Generator_Emits_Nested_Feature_Toggle_Host_Wrappers()
=> Given("nested feature toggle declarations", () => Compile("""
using PatternKit.Generators.FeatureToggles;
namespace Demo;
public sealed record CheckoutContext(string Tenant, decimal Total);

public partial class ToggleContainer
{
private partial class PrivateHost
{
[GenerateFeatureToggleSet(typeof(CheckoutContext))]
protected partial class ProtectedToggles
{
[FeatureToggleRule("protected")]
private static bool Protected(CheckoutContext context) => true;
}

[GenerateFeatureToggleSet(typeof(CheckoutContext))]
private protected partial class PrivateProtectedToggles
{
[FeatureToggleRule("private-protected")]
private static bool PrivateProtected(CheckoutContext context) => true;
}

[GenerateFeatureToggleSet(typeof(CheckoutContext))]
protected internal partial class ProtectedInternalToggles
{
[FeatureToggleRule("protected-internal")]
private static bool ProtectedInternal(CheckoutContext context) => true;
}
}
}
"""))
.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 ToggleContainer", combined);
ScenarioExpect.Contains("private partial class PrivateHost", combined);
ScenarioExpect.Contains("protected partial class ProtectedToggles", combined);
ScenarioExpect.Contains("private protected partial class PrivateProtectedToggles", combined);
ScenarioExpect.Contains("protected internal partial class ProtectedInternalToggles", combined);
ScenarioExpect.True(result.EmitSuccess, result.EmitDiagnostics);
})
.AssertPassed();

[Scenario("Generator skips malformed feature toggle context type")]
[Fact]
public Task Generator_Skips_Malformed_Feature_Toggle_Context_Type()
=> Given("a feature toggle declaration with a null context type", () => Compile("""
using PatternKit.Generators.FeatureToggles;
[GenerateFeatureToggleSet(null!)]
public static partial class CheckoutToggles
{
[FeatureToggleRule("x")]
private static bool IsEnabled(string context) => true;
}
"""))
.Then("no source is generated", result =>
ScenarioExpect.Empty(result.GeneratedSources))
.AssertPassed();

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