Skip to content

Commit dab3ffc

Browse files
authored
test(generators): cover feature toggles and gateway aggregation (#440)
1 parent 1de30bd commit dab3ffc

4 files changed

Lines changed: 363 additions & 70 deletions

File tree

src/PatternKit.Generators/FeatureToggles/FeatureToggleSetGenerator.cs

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -123,27 +123,63 @@ private static string GenerateSource(
123123
sb.AppendLine();
124124
}
125125

126+
var containingTypes = GetContainingTypes(type);
127+
var indentLevel = 0;
128+
foreach (var containingType in containingTypes)
129+
{
130+
AppendTypeDeclaration(sb, containingType, indentLevel);
131+
sb.AppendLine();
132+
sb.AppendLine(new string(' ', indentLevel * 4) + "{");
133+
indentLevel++;
134+
}
135+
136+
AppendTypeDeclaration(sb, type, indentLevel);
137+
sb.AppendLine();
138+
var indent = new string(' ', indentLevel * 4);
139+
sb.AppendLine(indent + "{");
140+
var memberIndent = indent + " ";
141+
var bodyIndent = memberIndent + " ";
142+
sb.Append(memberIndent).Append("public static global::PatternKit.Application.FeatureToggles.FeatureToggleSet<").Append(contextTypeName).Append("> ").Append(factoryName).AppendLine("()");
143+
sb.AppendLine(memberIndent + "{");
144+
sb.Append(bodyIndent).Append("return global::PatternKit.Application.FeatureToggles.FeatureToggleSet<").Append(contextTypeName).Append(">.Create(\"").Append(Escape(setName)).AppendLine("\")");
145+
foreach (var rule in rules)
146+
{
147+
sb.Append(bodyIndent).Append(" .AddRule(\"").Append(Escape(rule.Name)).Append("\", ").Append(rule.DefaultEnabled ? "true" : "false").Append(", ").Append(rule.Method.Name).AppendLine(")");
148+
}
149+
150+
sb.Append(bodyIndent).AppendLine(" .Build();");
151+
sb.AppendLine(memberIndent + "}");
152+
sb.AppendLine(indent + "}");
153+
for (var i = containingTypes.Length - 1; i >= 0; i--)
154+
{
155+
sb.AppendLine(new string(' ', i * 4) + "}");
156+
}
157+
158+
return sb.ToString();
159+
}
160+
161+
private static INamedTypeSymbol[] GetContainingTypes(INamedTypeSymbol type)
162+
{
163+
var containingTypes = new Stack<INamedTypeSymbol>();
164+
for (var current = type.ContainingType; current is not null; current = current.ContainingType)
165+
{
166+
containingTypes.Push(current);
167+
}
168+
169+
return containingTypes.ToArray();
170+
}
171+
172+
private static void AppendTypeDeclaration(StringBuilder sb, INamedTypeSymbol type, int indentLevel)
173+
{
174+
sb.Append(new string(' ', indentLevel * 4));
126175
sb.Append(GetAccessibility(type.DeclaredAccessibility)).Append(' ');
127176
if (type.IsStatic)
128177
sb.Append("static ");
129178
else if (type.IsAbstract && type.TypeKind == TypeKind.Class)
130179
sb.Append("abstract ");
131180
else if (type.IsSealed && type.TypeKind == TypeKind.Class)
132181
sb.Append("sealed ");
133-
sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name).AppendLine();
134-
sb.AppendLine("{");
135-
sb.Append(" public static global::PatternKit.Application.FeatureToggles.FeatureToggleSet<").Append(contextTypeName).Append("> ").Append(factoryName).AppendLine("()");
136-
sb.AppendLine(" {");
137-
sb.Append(" return global::PatternKit.Application.FeatureToggles.FeatureToggleSet<").Append(contextTypeName).Append(">.Create(\"").Append(Escape(setName)).AppendLine("\")");
138-
foreach (var rule in rules)
139-
{
140-
sb.Append(" .AddRule(\"").Append(Escape(rule.Name)).Append("\", ").Append(rule.DefaultEnabled ? "true" : "false").Append(", ").Append(rule.Method.Name).AppendLine(")");
141-
}
142-
143-
sb.AppendLine(" .Build();");
144-
sb.AppendLine(" }");
145-
sb.AppendLine("}");
146-
return sb.ToString();
182+
sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name);
147183
}
148184

149185
private static string Escape(string value) => value.Replace("\\", "\\\\").Replace("\"", "\\\"");

src/PatternKit.Generators/GatewayAggregation/GatewayAggregationGenerator.cs

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Generic;
12
using System.Linq;
23
using System.Text;
34
using Microsoft.CodeAnalysis;
@@ -158,27 +159,63 @@ private static string GenerateSource(
158159
sb.AppendLine();
159160
}
160161

162+
var containingTypes = GetContainingTypes(type);
163+
var indentLevel = 0;
164+
foreach (var containingType in containingTypes)
165+
{
166+
AppendTypeDeclaration(sb, containingType, indentLevel);
167+
sb.AppendLine();
168+
sb.AppendLine(new string(' ', indentLevel * 4) + "{");
169+
indentLevel++;
170+
}
171+
172+
AppendTypeDeclaration(sb, type, indentLevel);
173+
sb.AppendLine();
174+
var indent = new string(' ', indentLevel * 4);
175+
sb.AppendLine(indent + "{");
176+
var memberIndent = indent + " ";
177+
var bodyIndent = memberIndent + " ";
178+
sb.Append(memberIndent).Append("public static global::PatternKit.Cloud.GatewayAggregation.GatewayAggregation<")
179+
.Append(requestTypeName).Append(", ").Append(responseTypeName).Append("> ").Append(factoryMethodName).AppendLine("()");
180+
sb.AppendLine(memberIndent + "{");
181+
sb.Append(bodyIndent).Append("return global::PatternKit.Cloud.GatewayAggregation.GatewayAggregation<")
182+
.Append(requestTypeName).Append(", ").Append(responseTypeName).Append(">.Create(\"").Append(Escape(gatewayName)).AppendLine("\")");
183+
foreach (var fetch in fetches)
184+
sb.Append(bodyIndent).Append(" .Fetch<").Append(fetch.Method.ReturnType.ToDisplayString(TypeFormat)).Append(">(\"").Append(Escape(fetch.Name)).Append("\", ").Append(fetch.Method.Name).AppendLine(")");
185+
sb.Append(bodyIndent).Append(" .Compose(").Append(composerName).AppendLine(")");
186+
sb.Append(bodyIndent).AppendLine(" .Build();");
187+
sb.AppendLine(memberIndent + "}");
188+
sb.AppendLine(indent + "}");
189+
for (var i = containingTypes.Length - 1; i >= 0; i--)
190+
{
191+
sb.AppendLine(new string(' ', i * 4) + "}");
192+
}
193+
194+
return sb.ToString();
195+
}
196+
197+
private static INamedTypeSymbol[] GetContainingTypes(INamedTypeSymbol type)
198+
{
199+
var containingTypes = new Stack<INamedTypeSymbol>();
200+
for (var current = type.ContainingType; current is not null; current = current.ContainingType)
201+
{
202+
containingTypes.Push(current);
203+
}
204+
205+
return containingTypes.ToArray();
206+
}
207+
208+
private static void AppendTypeDeclaration(StringBuilder sb, INamedTypeSymbol type, int indentLevel)
209+
{
210+
sb.Append(new string(' ', indentLevel * 4));
161211
sb.Append(GetAccessibility(type.DeclaredAccessibility)).Append(' ');
162212
if (type.IsStatic)
163213
sb.Append("static ");
164214
else if (type.IsAbstract && type.TypeKind == TypeKind.Class)
165215
sb.Append("abstract ");
166216
else if (type.IsSealed && type.TypeKind == TypeKind.Class)
167217
sb.Append("sealed ");
168-
sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name).AppendLine();
169-
sb.AppendLine("{");
170-
sb.Append(" public static global::PatternKit.Cloud.GatewayAggregation.GatewayAggregation<")
171-
.Append(requestTypeName).Append(", ").Append(responseTypeName).Append("> ").Append(factoryMethodName).AppendLine("()");
172-
sb.AppendLine(" {");
173-
sb.Append(" return global::PatternKit.Cloud.GatewayAggregation.GatewayAggregation<")
174-
.Append(requestTypeName).Append(", ").Append(responseTypeName).Append(">.Create(\"").Append(Escape(gatewayName)).AppendLine("\")");
175-
foreach (var fetch in fetches)
176-
sb.Append(" .Fetch<").Append(fetch.Method.ReturnType.ToDisplayString(TypeFormat)).Append(">(\"").Append(Escape(fetch.Name)).Append("\", ").Append(fetch.Method.Name).AppendLine(")");
177-
sb.Append(" .Compose(").Append(composerName).AppendLine(")");
178-
sb.AppendLine(" .Build();");
179-
sb.AppendLine(" }");
180-
sb.AppendLine("}");
181-
return sb.ToString();
218+
sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name);
182219
}
183220

184221
private static string? GetNamedString(AttributeData attribute, string name)

test/PatternKit.Generators.Tests/FeatureToggleSetGeneratorTests.cs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ public static partial class CheckoutToggles
4343
[Theory]
4444
[InlineData("public static class CheckoutToggles { [FeatureToggleRule(\"x\")] private static bool IsEnabled(CheckoutContext context) => true; }", "PKFT001")]
4545
[InlineData("public static partial class CheckoutToggles;", "PKFT002")]
46+
[InlineData("public static partial class CheckoutToggles { [FeatureToggleRule(\"x\")] private static bool IsEnabled() => true; }", "PKFT003")]
47+
[InlineData("public static partial class CheckoutToggles { [FeatureToggleRule(\"x\")] private static bool IsEnabled(string context) => true; }", "PKFT003")]
4648
[InlineData("public static partial class CheckoutToggles { [FeatureToggleRule(\"x\")] private static string IsEnabled(CheckoutContext context) => \"yes\"; }", "PKFT003")]
4749
[InlineData("public static partial class CheckoutToggles { [FeatureToggleRule(\"x\")] private bool IsEnabled(CheckoutContext context) => true; }", "PKFT003")]
4850
public Task Generator_Reports_Invalid_Feature_Toggle_Declarations(string declaration, string diagnosticId)
@@ -56,6 +58,118 @@ public sealed record CheckoutContext(string Tenant, decimal Total);
5658
ScenarioExpect.Contains(result.Diagnostics, diagnostic => diagnostic.Id == diagnosticId))
5759
.AssertPassed();
5860

61+
[Scenario("Generator emits feature toggle defaults and host shapes")]
62+
[Fact]
63+
public Task Generator_Emits_Feature_Toggle_Defaults_And_Host_Shapes()
64+
=> Given("feature toggle declarations with default names and host shapes", () => Compile("""
65+
using PatternKit.Generators.FeatureToggles;
66+
namespace Demo;
67+
public sealed record CheckoutContext(string Tenant, decimal Total);
68+
69+
[GenerateFeatureToggleSet(typeof(CheckoutContext))]
70+
internal abstract partial class AbstractToggles
71+
{
72+
[FeatureToggleRule("enabled")]
73+
private static bool IsEnabled(CheckoutContext context) => true;
74+
}
75+
76+
[GenerateFeatureToggleSet(typeof(CheckoutContext), SetName = "tenant\\\"toggles")]
77+
public sealed partial class SealedToggles
78+
{
79+
[FeatureToggleRule("beta", DefaultEnabled = true)]
80+
private static bool Beta(CheckoutContext context) => context.Tenant == "beta";
81+
}
82+
83+
[GenerateFeatureToggleSet(typeof(CheckoutContext))]
84+
internal partial struct StructToggles
85+
{
86+
[FeatureToggleRule("large-order")]
87+
private static bool LargeOrder(CheckoutContext context) => context.Total >= 500m;
88+
}
89+
"""))
90+
.Then("generated sources preserve host shape and configured defaults", result =>
91+
{
92+
ScenarioExpect.Empty(result.Diagnostics);
93+
ScenarioExpect.Equal(3, result.GeneratedSources.Count);
94+
95+
var combined = string.Join("\n", result.GeneratedSources);
96+
ScenarioExpect.Contains("internal abstract partial class AbstractToggles", combined);
97+
ScenarioExpect.Contains("public sealed partial class SealedToggles", combined);
98+
ScenarioExpect.Contains("internal partial struct StructToggles", combined);
99+
ScenarioExpect.Contains("Create(\"feature-toggles\")", combined);
100+
ScenarioExpect.Contains("Create(\"tenant\\\\\\\"toggles\")", combined);
101+
ScenarioExpect.Contains(".AddRule(\"enabled\", false, IsEnabled)", combined);
102+
ScenarioExpect.Contains(".AddRule(\"beta\", true, Beta)", combined);
103+
ScenarioExpect.True(result.EmitSuccess, result.EmitDiagnostics);
104+
})
105+
.AssertPassed();
106+
107+
[Scenario("Generator emits nested feature toggle host wrappers")]
108+
[Fact]
109+
public Task Generator_Emits_Nested_Feature_Toggle_Host_Wrappers()
110+
=> Given("nested feature toggle declarations", () => Compile("""
111+
using PatternKit.Generators.FeatureToggles;
112+
namespace Demo;
113+
public sealed record CheckoutContext(string Tenant, decimal Total);
114+
115+
public partial class ToggleContainer
116+
{
117+
private partial class PrivateHost
118+
{
119+
[GenerateFeatureToggleSet(typeof(CheckoutContext))]
120+
protected partial class ProtectedToggles
121+
{
122+
[FeatureToggleRule("protected")]
123+
private static bool Protected(CheckoutContext context) => true;
124+
}
125+
126+
[GenerateFeatureToggleSet(typeof(CheckoutContext))]
127+
private protected partial class PrivateProtectedToggles
128+
{
129+
[FeatureToggleRule("private-protected")]
130+
private static bool PrivateProtected(CheckoutContext context) => true;
131+
}
132+
133+
[GenerateFeatureToggleSet(typeof(CheckoutContext))]
134+
protected internal partial class ProtectedInternalToggles
135+
{
136+
[FeatureToggleRule("protected-internal")]
137+
private static bool ProtectedInternal(CheckoutContext context) => true;
138+
}
139+
}
140+
}
141+
"""))
142+
.Then("generated sources preserve containing partial type wrappers", result =>
143+
{
144+
ScenarioExpect.Empty(result.Diagnostics);
145+
ScenarioExpect.Equal(3, result.GeneratedSources.Count);
146+
147+
var combined = string.Join("\n", result.GeneratedSources);
148+
ScenarioExpect.Contains("public partial class ToggleContainer", combined);
149+
ScenarioExpect.Contains("private partial class PrivateHost", combined);
150+
ScenarioExpect.Contains("protected partial class ProtectedToggles", combined);
151+
ScenarioExpect.Contains("private protected partial class PrivateProtectedToggles", combined);
152+
ScenarioExpect.Contains("protected internal partial class ProtectedInternalToggles", combined);
153+
ScenarioExpect.True(result.EmitSuccess, result.EmitDiagnostics);
154+
})
155+
.AssertPassed();
156+
157+
[Scenario("Generator skips malformed feature toggle context type")]
158+
[Fact]
159+
public Task Generator_Skips_Malformed_Feature_Toggle_Context_Type()
160+
=> Given("a feature toggle declaration with a null context type", () => Compile("""
161+
using PatternKit.Generators.FeatureToggles;
162+
[GenerateFeatureToggleSet(null!)]
163+
public static partial class CheckoutToggles
164+
{
165+
[FeatureToggleRule("x")]
166+
private static bool IsEnabled(string context) => true;
167+
}
168+
"""))
169+
.Then("no source is generated", result =>
170+
ScenarioExpect.Empty(result.GeneratedSources))
171+
.AssertPassed();
172+
59173
private static GeneratorResult Compile(string source)
60174
{
61175
var compilation = RoslynTestHelpers.CreateCompilation(

0 commit comments

Comments
 (0)