diff --git a/src/PatternKit.Generators/Ambassador/AmbassadorGenerator.cs b/src/PatternKit.Generators/Ambassador/AmbassadorGenerator.cs index ac11bab7..8f50b7a7 100644 --- a/src/PatternKit.Generators/Ambassador/AmbassadorGenerator.cs +++ b/src/PatternKit.Generators/Ambassador/AmbassadorGenerator.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; @@ -206,35 +207,72 @@ private static string GenerateSource( sb.AppendLine(); } - 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.Ambassador.Ambassador<") + 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 + " "; + var chainIndent = bodyIndent + " "; + sb.Append(memberIndent).Append("public static global::PatternKit.Cloud.Ambassador.Ambassador<") .Append(requestTypeName).Append(", ").Append(responseTypeName).Append("> ").Append(factoryMethodName).AppendLine("()"); - sb.AppendLine(" {"); - sb.Append(" return global::PatternKit.Cloud.Ambassador.Ambassador<") + sb.Append(memberIndent).AppendLine("{"); + sb.Append(bodyIndent).Append("return global::PatternKit.Cloud.Ambassador.Ambassador<") .Append(requestTypeName).Append(", ").Append(responseTypeName).Append(">.Create(\"").Append(Escape(ambassadorName)).AppendLine("\")"); foreach (var transform in transforms) - sb.Append(" .Transform(").Append(transform.Name).AppendLine(")"); + sb.Append(chainIndent).Append(".Transform(").Append(transform.Name).AppendLine(")"); if (policyName is not null) - sb.Append(" .ConnectionPolicy(").Append(policyName).AppendLine(")"); + sb.Append(chainIndent).Append(".ConnectionPolicy(").Append(policyName).AppendLine(")"); foreach (var item in telemetry) - sb.Append(" .Telemetry(\"").Append(Escape(item.Name)).Append("\", ").Append(item.Method.Name).AppendLine(")"); - sb.Append(" .Call(").Append(callName).AppendLine(")"); + sb.Append(chainIndent).Append(".Telemetry(\"").Append(Escape(item.Name)).Append("\", ").Append(item.Method.Name).AppendLine(")"); + sb.Append(chainIndent).Append(".Call(").Append(callName).AppendLine(")"); if (fallbackName is not null) - sb.Append(" .Fallback(").Append(fallbackName).AppendLine(")"); - sb.AppendLine(" .Build();"); - sb.AppendLine(" }"); - sb.AppendLine("}"); + sb.Append(chainIndent).Append(".Fallback(").Append(fallbackName).AppendLine(")"); + sb.Append(chainIndent).AppendLine(".Build();"); + sb.Append(memberIndent).AppendLine("}"); + 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(); + 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); + } + private static string? GetNamedString(AttributeData attribute, string name) => attribute.NamedArguments.FirstOrDefault(kv => kv.Key == name).Value.Value as string; diff --git a/src/PatternKit.Generators/CanonicalDataModel/CanonicalDataModelGenerator.cs b/src/PatternKit.Generators/CanonicalDataModel/CanonicalDataModelGenerator.cs index 97f03abe..dad59640 100644 --- a/src/PatternKit.Generators/CanonicalDataModel/CanonicalDataModelGenerator.cs +++ b/src/PatternKit.Generators/CanonicalDataModel/CanonicalDataModelGenerator.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; @@ -114,6 +115,52 @@ 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 + " "; + var chainIndent = bodyIndent + " "; + sb.Append(memberIndent).Append("public static global::PatternKit.EnterpriseIntegration.CanonicalDataModel.CanonicalDataModel<").Append(canonicalTypeName).Append("> ").Append(factoryMethodName).AppendLine("()"); + sb.Append(memberIndent).AppendLine("{"); + sb.Append(bodyIndent).Append("return global::PatternKit.EnterpriseIntegration.CanonicalDataModel.CanonicalDataModel<").Append(canonicalTypeName).Append(">.Create(\"").Append(Escape(modelName)).AppendLine("\")"); + sb.Append(chainIndent).Append(".From<").Append(sourceTypeName).Append(">(\"").Append(Escape(adapterName)).Append("\", ").Append(mapperName).AppendLine(")"); + sb.Append(chainIndent).AppendLine(".Build();"); + sb.Append(memberIndent).AppendLine("}"); + 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(); + 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 "); @@ -121,16 +168,7 @@ private static string GenerateSource( 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.EnterpriseIntegration.CanonicalDataModel.CanonicalDataModel<").Append(canonicalTypeName).Append("> ").Append(factoryMethodName).AppendLine("()"); - sb.AppendLine(" {"); - sb.Append(" return global::PatternKit.EnterpriseIntegration.CanonicalDataModel.CanonicalDataModel<").Append(canonicalTypeName).Append(">.Create(\"").Append(Escape(modelName)).AppendLine("\")"); - sb.Append(" .From<").Append(sourceTypeName).Append(">(\"").Append(Escape(adapterName)).Append("\", ").Append(mapperName).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) diff --git a/test/PatternKit.Generators.Tests/AmbassadorGeneratorTests.cs b/test/PatternKit.Generators.Tests/AmbassadorGeneratorTests.cs index bd586e81..9b7da8d9 100644 --- a/test/PatternKit.Generators.Tests/AmbassadorGeneratorTests.cs +++ b/test/PatternKit.Generators.Tests/AmbassadorGeneratorTests.cs @@ -50,54 +50,148 @@ public static partial class InventoryAmbassador .AssertPassed(); [Scenario("Reports diagnostics for invalid ambassador declarations")] + [Theory] + [InlineData("public static class AmbassadorHost { [AmbassadorCall] private static int Call(AmbassadorContext ctx) => 1; }", "PKAMB001")] + [InlineData("public static partial class AmbassadorHost;", "PKAMB002")] + [InlineData("public static partial class AmbassadorHost { [AmbassadorCall] private static int One(AmbassadorContext ctx) => 1; [AmbassadorCall] private static int Two(AmbassadorContext ctx) => 2; }", "PKAMB002")] + [InlineData("public static partial class AmbassadorHost { [AmbassadorConnectionPolicy] private static bool One(string request) => true; [AmbassadorConnectionPolicy] private static bool Two(string request) => true; [AmbassadorCall] private static int Call(AmbassadorContext ctx) => 1; }", "PKAMB002")] + [InlineData("public static partial class AmbassadorHost { [AmbassadorFallback] private static int One(AmbassadorContext ctx) => 1; [AmbassadorFallback] private static int Two(AmbassadorContext ctx) => 2; [AmbassadorCall] private static int Call(AmbassadorContext ctx) => 1; }", "PKAMB002")] + [InlineData("public static partial class AmbassadorHost { [AmbassadorTransform] private static int Transform(string request) => 1; [AmbassadorCall] private static int Call(AmbassadorContext ctx) => 1; }", "PKAMB003")] + [InlineData("public static partial class AmbassadorHost { [AmbassadorConnectionPolicy] private static string CanConnect(string request) => request; [AmbassadorCall] private static int Call(AmbassadorContext ctx) => 1; }", "PKAMB003")] + [InlineData("public static partial class AmbassadorHost { [AmbassadorTelemetry(\"trace\")] private static int Trace(AmbassadorContext ctx) => 1; [AmbassadorCall] private static int Call(AmbassadorContext ctx) => 1; }", "PKAMB003")] + [InlineData("public static partial class AmbassadorHost { [AmbassadorCall] private static string Call(AmbassadorContext ctx) => \"\"; }", "PKAMB003")] + [InlineData("public static partial class AmbassadorHost { [AmbassadorCall] private static int Call(AmbassadorContext ctx) => 1; [AmbassadorFallback] private static string Fallback(AmbassadorContext ctx) => \"\"; }", "PKAMB003")] + [InlineData("public static partial class AmbassadorHost { [AmbassadorTelemetry(\"trace\")] private static void Trace(AmbassadorContext ctx) { } [AmbassadorTelemetry(\"TRACE\")] private static void Trace2(AmbassadorContext ctx) { } [AmbassadorCall] private static int Call(AmbassadorContext ctx) => 1; }", "PKAMB004")] + public Task Reports_Diagnostics_For_Invalid_Ambassador_Declarations(string declaration, string expected) + => Given("an invalid ambassador declaration", () => Compile($$""" + using PatternKit.Cloud.Ambassador; + using PatternKit.Generators.Ambassador; + [GenerateAmbassador(typeof(string), typeof(int))] + {{declaration}} + """)) + .Then("diagnostics identify invalid declarations", result => + ScenarioExpect.Contains(result.Diagnostics, diagnostic => diagnostic.Id == expected)) + .AssertPassed(); + + [Scenario("Generates ambassador defaults and host shapes")] [Fact] - public Task Reports_Diagnostics_For_Invalid_Ambassador_Declarations() - => Given("invalid ambassador declarations", () => new[] + public Task Generates_Ambassador_Defaults_And_Host_Shapes() + => Given("ambassador declarations with default names and different host shapes", () => Compile(""" + using PatternKit.Cloud.Ambassador; + using PatternKit.Generators.Ambassador; + namespace Demo; + public sealed record InventoryRequest(string Sku); + public sealed record InventoryResponse(string Status); + + [GenerateAmbassador(typeof(InventoryRequest), typeof(InventoryResponse))] + internal abstract partial class AbstractAmbassador + { + [AmbassadorCall] + private static InventoryResponse Call(AmbassadorContext ctx) => new("ok"); + } + + [GenerateAmbassador(typeof(InventoryRequest), typeof(InventoryResponse), AmbassadorName = "tenant\\\"ambassador")] + public sealed partial class SealedAmbassador + { + [AmbassadorCall] + private static InventoryResponse Call(AmbassadorContext ctx) => new("ok"); + } + + [GenerateAmbassador(typeof(InventoryRequest), typeof(InventoryResponse))] + internal partial struct StructAmbassador + { + [AmbassadorCall] + private static InventoryResponse Call(AmbassadorContext ctx) => new("ok"); + } + """)) + .Then("generated sources preserve host shape and configured names", result => { - Compile(""" - using PatternKit.Generators.Ambassador; - [GenerateAmbassador(typeof(string), typeof(int))] - public static class AmbassadorHost; - """), - Compile(""" - using PatternKit.Generators.Ambassador; - [GenerateAmbassador(typeof(string), typeof(int))] - public static partial class AmbassadorHost; - """), - Compile(""" - using PatternKit.Cloud.Ambassador; - using PatternKit.Generators.Ambassador; - [GenerateAmbassador(typeof(string), typeof(int))] - public static partial class AmbassadorHost - { - [AmbassadorCall] - private static string Call(AmbassadorContext ctx) => ""; - } - """), - Compile(""" - using PatternKit.Cloud.Ambassador; - using PatternKit.Generators.Ambassador; - [GenerateAmbassador(typeof(string), typeof(int))] - public static partial class AmbassadorHost + ScenarioExpect.Empty(result.Diagnostics); + ScenarioExpect.Equal(3, result.GeneratedSources.Count); + + var combined = string.Join("\n", result.GeneratedSources); + ScenarioExpect.Contains("internal abstract partial class AbstractAmbassador", combined); + ScenarioExpect.Contains("public sealed partial class SealedAmbassador", combined); + ScenarioExpect.Contains("internal partial struct StructAmbassador", combined); + ScenarioExpect.Contains("Create(\"ambassador\")", combined); + ScenarioExpect.Contains("Create(\"tenant\\\\\\\"ambassador\")", combined); + ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics)); + }) + .AssertPassed(); + + [Scenario("Generates nested ambassador host wrappers")] + [Fact] + public Task Generates_Nested_Ambassador_Host_Wrappers() + => Given("nested ambassador declarations", () => Compile(""" + using PatternKit.Cloud.Ambassador; + using PatternKit.Generators.Ambassador; + namespace Demo; + public sealed record InventoryRequest(string Sku); + public sealed record InventoryResponse(string Status); + + public partial class AmbassadorContainer + { + private partial class PrivateHost { - [AmbassadorTelemetry("trace")] - private static void Trace(AmbassadorContext ctx) { } - [AmbassadorTelemetry("TRACE")] - private static void Trace2(AmbassadorContext ctx) { } - [AmbassadorCall] - private static int Call(AmbassadorContext ctx) => 1; + [GenerateAmbassador(typeof(InventoryRequest), typeof(InventoryResponse))] + protected partial class ProtectedAmbassador + { + [AmbassadorCall] + private static InventoryResponse Call(AmbassadorContext ctx) => new("ok"); + } + + [GenerateAmbassador(typeof(InventoryRequest), typeof(InventoryResponse))] + private protected partial class PrivateProtectedAmbassador + { + [AmbassadorCall] + private static InventoryResponse Call(AmbassadorContext ctx) => new("ok"); + } + + [GenerateAmbassador(typeof(InventoryRequest), typeof(InventoryResponse))] + protected internal partial class ProtectedInternalAmbassador + { + [AmbassadorCall] + private static InventoryResponse Call(AmbassadorContext ctx) => new("ok"); + } } - """) - }) - .Then("diagnostics identify invalid declarations", results => + } + """)) + .Then("generated sources preserve containing partial type wrappers", result => { - ScenarioExpect.Contains(results[0].Diagnostics, diagnostic => diagnostic.Id == "PKAMB001"); - ScenarioExpect.Contains(results[1].Diagnostics, diagnostic => diagnostic.Id == "PKAMB002"); - ScenarioExpect.Contains(results[2].Diagnostics, diagnostic => diagnostic.Id == "PKAMB003"); - ScenarioExpect.Contains(results[3].Diagnostics, diagnostic => diagnostic.Id == "PKAMB004"); + ScenarioExpect.Empty(result.Diagnostics); + ScenarioExpect.Equal(3, result.GeneratedSources.Count); + + var combined = string.Join("\n", result.GeneratedSources); + ScenarioExpect.Contains("public partial class AmbassadorContainer", combined); + ScenarioExpect.Contains("private partial class PrivateHost", combined); + ScenarioExpect.Contains("protected partial class ProtectedAmbassador", combined); + ScenarioExpect.Contains("private protected partial class PrivateProtectedAmbassador", combined); + ScenarioExpect.Contains("protected internal partial class ProtectedInternalAmbassador", combined); + ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics)); }) .AssertPassed(); + [Scenario("Skips malformed ambassador type arguments")] + [Theory] + [InlineData("null!", "typeof(InventoryResponse)")] + [InlineData("typeof(InventoryRequest)", "null!")] + public Task Skips_Malformed_Ambassador_Type_Arguments(string requestType, string responseType) + => Given("an ambassador declaration with a null type argument", () => Compile($$""" + using PatternKit.Cloud.Ambassador; + using PatternKit.Generators.Ambassador; + public sealed record InventoryRequest(string Sku); + public sealed record InventoryResponse(string Status); + [GenerateAmbassador({{requestType}}, {{responseType}})] + public static partial class InventoryAmbassador + { + [AmbassadorCall] + private static InventoryResponse Call(AmbassadorContext ctx) => new("ok"); + } + """)) + .Then("no source is generated", result => + ScenarioExpect.Empty(result.GeneratedSources)) + .AssertPassed(); + private static GeneratorResult Compile(string source) { var compilation = RoslynTestHelpers.CreateCompilation( diff --git a/test/PatternKit.Generators.Tests/CanonicalDataModelGeneratorTests.cs b/test/PatternKit.Generators.Tests/CanonicalDataModelGeneratorTests.cs index b469b45f..5a72ff73 100644 --- a/test/PatternKit.Generators.Tests/CanonicalDataModelGeneratorTests.cs +++ b/test/PatternKit.Generators.Tests/CanonicalDataModelGeneratorTests.cs @@ -37,38 +37,142 @@ public static partial class PartnerOrderCanonicalModel .AssertPassed(); [Scenario("Reports diagnostics for invalid canonical data model declarations")] + [Theory] + [InlineData("public static class CanonicalHost { [CanonicalDataModelMapper] private static int Map(string value) => value.Length; }", "PKCDM001")] + [InlineData("public static partial class CanonicalHost;", "PKCDM002")] + [InlineData("public static partial class CanonicalHost { [CanonicalDataModelMapper] private static int One(string value) => value.Length; [CanonicalDataModelMapper] private static int Two(string value) => value.Length; }", "PKCDM002")] + [InlineData("public partial class CanonicalHost { [CanonicalDataModelMapper] private int Map(string value) => value.Length; }", "PKCDM003")] + [InlineData("public static partial class CanonicalHost { [CanonicalDataModelMapper] private static string Map(string value) => value; }", "PKCDM003")] + [InlineData("public static partial class CanonicalHost { [CanonicalDataModelMapper] private static int Map() => 1; }", "PKCDM003")] + [InlineData("public static partial class CanonicalHost { [CanonicalDataModelMapper] private static int Map(string value, string tenant) => value.Length; }", "PKCDM003")] + [InlineData("public static partial class CanonicalHost { [CanonicalDataModelMapper] private static int Map(int value) => value; }", "PKCDM003")] + public Task Reports_Diagnostics_For_Invalid_Canonical_Data_Model_Declarations(string declaration, string expected) + => Given("an invalid canonical data model declaration", () => Compile($$""" + using PatternKit.Generators.CanonicalDataModel; + [GenerateCanonicalDataModel(typeof(string), typeof(int))] + {{declaration}} + """)) + .Then("diagnostics identify the invalid declaration", result => + ScenarioExpect.Contains(result.Diagnostics, diagnostic => diagnostic.Id == expected)) + .AssertPassed(); + + [Scenario("Generates canonical data model defaults and host shapes")] [Fact] - public Task Reports_Diagnostics_For_Invalid_Canonical_Data_Model_Declarations() - => Given("invalid canonical data model declarations", () => new[] + public Task Generates_Canonical_Data_Model_Defaults_And_Host_Shapes() + => Given("canonical data model declarations with default names and different host shapes", () => Compile(""" + using PatternKit.Generators.CanonicalDataModel; + namespace Demo; + public sealed record PartnerOrder(string Id); + public sealed record CanonicalOrder(string OrderId); + + [GenerateCanonicalDataModel(typeof(PartnerOrder), typeof(CanonicalOrder))] + internal abstract partial class AbstractCanonicalModel + { + [CanonicalDataModelMapper] + private static CanonicalOrder Map(PartnerOrder order) => new(order.Id); + } + + [GenerateCanonicalDataModel(typeof(PartnerOrder), typeof(CanonicalOrder), ModelName = "tenant\\\"orders", AdapterName = "partner\\adapter")] + public sealed partial class SealedCanonicalModel + { + [CanonicalDataModelMapper] + private static CanonicalOrder Map(PartnerOrder order) => new(order.Id); + } + + [GenerateCanonicalDataModel(typeof(PartnerOrder), typeof(CanonicalOrder))] + internal partial struct StructCanonicalModel + { + [CanonicalDataModelMapper] + private static CanonicalOrder Map(PartnerOrder order) => new(order.Id); + } + """)) + .Then("generated sources preserve host shape and configured names", result => { - Compile(""" - using PatternKit.Generators.CanonicalDataModel; - [GenerateCanonicalDataModel(typeof(string), typeof(int))] - public static class CanonicalHost; - """), - Compile(""" - using PatternKit.Generators.CanonicalDataModel; - [GenerateCanonicalDataModel(typeof(string), typeof(int))] - public static partial class CanonicalHost; - """), - Compile(""" - using PatternKit.Generators.CanonicalDataModel; - [GenerateCanonicalDataModel(typeof(string), typeof(int))] - public static partial class CanonicalHost + ScenarioExpect.Empty(result.Diagnostics); + ScenarioExpect.Equal(3, result.GeneratedSources.Count); + + var combined = string.Join("\n", result.GeneratedSources); + ScenarioExpect.Contains("internal abstract partial class AbstractCanonicalModel", combined); + ScenarioExpect.Contains("public sealed partial class SealedCanonicalModel", combined); + ScenarioExpect.Contains("internal partial struct StructCanonicalModel", combined); + ScenarioExpect.Contains("Create(\"canonical-data-model\")", combined); + ScenarioExpect.Contains("Create(\"tenant\\\\\\\"orders\")", combined); + ScenarioExpect.Contains(".From(\"partner\\\\adapter\", Map)", combined); + ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics)); + }) + .AssertPassed(); + + [Scenario("Generates nested canonical data model host wrappers")] + [Fact] + public Task Generates_Nested_Canonical_Data_Model_Host_Wrappers() + => Given("nested canonical data model declarations", () => Compile(""" + using PatternKit.Generators.CanonicalDataModel; + namespace Demo; + public sealed record PartnerOrder(string Id); + public sealed record CanonicalOrder(string OrderId); + + public partial class CanonicalContainer + { + private partial class PrivateHost { - [CanonicalDataModelMapper] - private static string Map(string value) => value; + [GenerateCanonicalDataModel(typeof(PartnerOrder), typeof(CanonicalOrder))] + protected partial class ProtectedCanonicalModel + { + [CanonicalDataModelMapper] + private static CanonicalOrder Map(PartnerOrder order) => new(order.Id); + } + + [GenerateCanonicalDataModel(typeof(PartnerOrder), typeof(CanonicalOrder))] + private protected partial class PrivateProtectedCanonicalModel + { + [CanonicalDataModelMapper] + private static CanonicalOrder Map(PartnerOrder order) => new(order.Id); + } + + [GenerateCanonicalDataModel(typeof(PartnerOrder), typeof(CanonicalOrder))] + protected internal partial class ProtectedInternalCanonicalModel + { + [CanonicalDataModelMapper] + private static CanonicalOrder Map(PartnerOrder order) => new(order.Id); + } } - """) - }) - .Then("diagnostics identify the invalid declarations", results => + } + """)) + .Then("generated sources preserve containing partial type wrappers", result => { - ScenarioExpect.Contains(results[0].Diagnostics, diagnostic => diagnostic.Id == "PKCDM001"); - ScenarioExpect.Contains(results[1].Diagnostics, diagnostic => diagnostic.Id == "PKCDM002"); - ScenarioExpect.Contains(results[2].Diagnostics, diagnostic => diagnostic.Id == "PKCDM003"); + ScenarioExpect.Empty(result.Diagnostics); + ScenarioExpect.Equal(3, result.GeneratedSources.Count); + + var combined = string.Join("\n", result.GeneratedSources); + ScenarioExpect.Contains("public partial class CanonicalContainer", combined); + ScenarioExpect.Contains("private partial class PrivateHost", combined); + ScenarioExpect.Contains("protected partial class ProtectedCanonicalModel", combined); + ScenarioExpect.Contains("private protected partial class PrivateProtectedCanonicalModel", combined); + ScenarioExpect.Contains("protected internal partial class ProtectedInternalCanonicalModel", combined); + ScenarioExpect.True(result.EmitSuccess, string.Join(Environment.NewLine, result.EmitDiagnostics)); }) .AssertPassed(); + [Scenario("Skips malformed canonical data model type arguments")] + [Theory] + [InlineData("null!", "typeof(CanonicalOrder)")] + [InlineData("typeof(PartnerOrder)", "null!")] + public Task Skips_Malformed_Canonical_Data_Model_Type_Arguments(string sourceType, string canonicalType) + => Given("a canonical data model declaration with a null type argument", () => Compile($$""" + using PatternKit.Generators.CanonicalDataModel; + public sealed record PartnerOrder(string Id); + public sealed record CanonicalOrder(string OrderId); + [GenerateCanonicalDataModel({{sourceType}}, {{canonicalType}})] + public static partial class PartnerOrderCanonicalModel + { + [CanonicalDataModelMapper] + private static CanonicalOrder Map(PartnerOrder order) => new(order.Id); + } + """)) + .Then("no source is generated", result => + ScenarioExpect.Empty(result.GeneratedSources)) + .AssertPassed(); + private static GeneratorResult Compile(string source) { var compilation = RoslynTestHelpers.CreateCompilation(