From 5cc4b156389fe0987288120d1790ccf98abe2c71 Mon Sep 17 00:00:00 2001 From: JerrettDavis Date: Tue, 19 May 2026 21:01:44 -0500 Subject: [PATCH] docs: add GoF pattern coverage catalog --- .../examples/production-ready-integrations.md | 8 +- docs/guides/pattern-coverage.md | 51 +++ docs/guides/toc.yml | 2 + ...rnKitExampleServiceCollectionExtensions.cs | 7 +- .../PatternKitPatternCatalog.cs | 404 ++++++++++++++++++ .../PatternKitPatternCatalogTests.cs | 167 ++++++++ 6 files changed, 636 insertions(+), 3 deletions(-) create mode 100644 docs/guides/pattern-coverage.md create mode 100644 src/PatternKit.Examples/ProductionReadiness/PatternKitPatternCatalog.cs create mode 100644 test/PatternKit.Examples.Tests/ProductionReadiness/PatternKitPatternCatalogTests.cs diff --git a/docs/examples/production-ready-integrations.md b/docs/examples/production-ready-integrations.md index acfc6342..d2811449 100644 --- a/docs/examples/production-ready-integrations.md +++ b/docs/examples/production-ready-integrations.md @@ -12,10 +12,12 @@ using PatternKit.Examples.ProductionReadiness; var services = new ServiceCollection() .AddLogging() - .AddPatternKitExampleCatalog(); + .AddPatternKitExampleCatalog() + .AddPatternKitPatternCatalog(); using var provider = services.BuildServiceProvider(validateScopes: true); var catalog = provider.GetRequiredService(); +var patterns = provider.GetRequiredService(); ``` ## Register runnable examples @@ -25,6 +27,7 @@ The examples package also exposes a fluent IoC surface for every catalog entry. ```csharp using Microsoft.Extensions.DependencyInjection; using PatternKit.Examples.DependencyInjection; +using PatternKit.Examples.ProductionReadiness; var services = new ServiceCollection() .AddLogging() @@ -34,6 +37,7 @@ using var provider = services.BuildServiceProvider(validateScopes: true); var pricing = provider.GetRequiredService(); var catalog = provider.GetRequiredService(); +var patterns = provider.GetRequiredService(); ``` Each example also has its own focused extension, so sample applications can import only the slice they need: @@ -59,6 +63,8 @@ Each `PatternKitExampleDescriptor` includes: | `Patterns` | PatternKit primitives demonstrated by the example. | | `ProductionChecks` | The behaviors that make the example production-shaped and regression-testable. | +The companion `IPatternKitPatternCatalog` records the canonical GoF pattern matrix. It links each pattern to its fluent runtime path, TinyBDD tests, real-world example, and source-generated path. Any missing generated path must point to a tracked issue; currently that is limited to the dedicated Interpreter generator and the dedicated Abstract Factory family generator. + ## Validate in a generic host Applications can fail fast during startup when catalog metadata is malformed or when source/docs/tests are missing from a repository checkout: diff --git a/docs/guides/pattern-coverage.md b/docs/guides/pattern-coverage.md new file mode 100644 index 00000000..06ffe03d --- /dev/null +++ b/docs/guides/pattern-coverage.md @@ -0,0 +1,51 @@ +# Pattern Coverage + +PatternKit tracks the canonical Gang of Four patterns as production surfaces, not just API names. Each pattern should have: + +- a fluent runtime path in `PatternKit.Core` +- TinyBDD coverage for the runtime path +- user documentation and real-world examples +- an importable example path through `Microsoft.Extensions.DependencyInjection` where the example assembly is used +- a source-generated path, or a tracked issue when the generator is still planned + +The source of truth is `PatternKitPatternCatalog` in `src/PatternKit.Examples/ProductionReadiness`. The TinyBDD tests in `PatternKitPatternCatalogTests` validate the catalog against the repository so missing files, missing examples, or undocumented generator gaps fail in CI. + +## Current GoF Coverage + +| Family | Pattern | Fluent path | Source-generated path | +| --- | --- | --- | --- | +| Creational | Abstract Factory | `AbstractFactory<,>` | Tracked in [#207](https://github.com/JerrettDavis/PatternKit/issues/207) | +| Creational | Builder | Builder helpers | Builder generator | +| Creational | Factory Method | `Factory` | Factory Method generator | +| Creational | Prototype | `Prototype` | Prototype generator | +| Creational | Singleton | `Singleton` | Singleton generator | +| Structural | Adapter | `Adapter` | Adapter generator | +| Structural | Bridge | `Bridge` | Bridge generator | +| Structural | Composite | `Composite` | Composite generator | +| Structural | Decorator | `Decorator` | Decorator generator | +| Structural | Facade | `Facade` and `TypedFacade` | Facade generator | +| Structural | Flyweight | `Flyweight` | Flyweight generator | +| Structural | Proxy | `Proxy` | Proxy generator | +| Behavioral | Chain of Responsibility | `ActionChain` and `ResultChain` | Chain generator | +| Behavioral | Command | `Command` | Command generator | +| Behavioral | Interpreter | `Interpreter` | Tracked in [#206](https://github.com/JerrettDavis/PatternKit/issues/206) | +| Behavioral | Iterator | `Flow` and sequence helpers | Iterator generator | +| Behavioral | Mediator | `Mediator` | Dispatcher generator | +| Behavioral | Memento | `Memento` | Memento generator | +| Behavioral | Observer | Observer primitives | Observer generator | +| Behavioral | State | `StateMachine` | State Machine generator | +| Behavioral | Strategy | `Strategy` and variants | Strategy generator | +| Behavioral | Template Method | `TemplateMethod` and fluent templates | Template Method generator | +| Behavioral | Visitor | `Visitor` and variants | Visitor generator | + +## Adding Or Extending A Pattern + +1. Add or update the fluent runtime implementation and TinyBDD tests. +2. Add or update the source generator, generator attributes, diagnostics, and TinyBDD generator tests. +3. Add a real-world example that can be imported from a normal application. +4. Register the example in `AddPatternKitExamples`. +5. Update the examples catalog and the pattern coverage catalog. +6. Add docs for the runtime path, generated path, and production example. +7. Run the relevant tests and land only when CI, docs, CodeQL, and coverage are green. + +If a generator is intentionally deferred, create a GitHub issue and list the issue URL in the catalog. The tests allow only explicit, reviewed gaps. diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml index 82d64a8f..767277c1 100644 --- a/docs/guides/toc.yml +++ b/docs/guides/toc.yml @@ -1,5 +1,7 @@ - name: Choosing Patterns href: choosing-patterns.md +- name: Pattern Coverage + href: pattern-coverage.md - name: Composing Patterns href: composing-patterns.md - name: Migrating from Traditional Patterns diff --git a/src/PatternKit.Examples/DependencyInjection/PatternKitExampleServiceCollectionExtensions.cs b/src/PatternKit.Examples/DependencyInjection/PatternKitExampleServiceCollectionExtensions.cs index f5549ff9..55f4d685 100644 --- a/src/PatternKit.Examples/DependencyInjection/PatternKitExampleServiceCollectionExtensions.cs +++ b/src/PatternKit.Examples/DependencyInjection/PatternKitExampleServiceCollectionExtensions.cs @@ -69,7 +69,7 @@ public sealed class CoercerService : ICoercer public T? From(object? value) => Coercer.From(value); } -public sealed record ProductionReadyExampleIntegrations(IPatternKitExampleCatalog Catalog); +public sealed record ProductionReadyExampleIntegrations(IPatternKitExampleCatalog ExampleCatalog, IPatternKitPatternCatalog PatternCatalog); public sealed record AuthLoggingChainExample(ActionChain Chain, List Log); public sealed record CoercionExample(ICoercer Integers, ICoercer Booleans, ICoercer Strings); public sealed record ComposedNotificationStrategyExample(AsyncStrategy Strategy); @@ -141,8 +141,11 @@ public static IServiceCollection AddPatternKitExamples(this IServiceCollection s public static IServiceCollection AddProductionReadyExampleIntegrations(this IServiceCollection services) { services.AddPatternKitExampleCatalog(); + services.AddPatternKitPatternCatalog(); services.AddSingleton(sp => - new(sp.GetRequiredService())); + new( + sp.GetRequiredService(), + sp.GetRequiredService())); return services.RegisterExample("Production-Ready Example Integrations", ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost | ExampleIntegrationSurface.AspNetCore); } diff --git a/src/PatternKit.Examples/ProductionReadiness/PatternKitPatternCatalog.cs b/src/PatternKit.Examples/ProductionReadiness/PatternKitPatternCatalog.cs new file mode 100644 index 00000000..11ba1047 --- /dev/null +++ b/src/PatternKit.Examples/ProductionReadiness/PatternKitPatternCatalog.cs @@ -0,0 +1,404 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace PatternKit.Examples.ProductionReadiness; + +/// +/// GoF pattern family used by the PatternKit pattern coverage catalog. +/// +public enum PatternFamily +{ + Creational, + Structural, + Behavioral +} + +/// +/// Describes a PatternKit implementation path for a design pattern. +/// +public sealed record PatternImplementationPath( + string Name, + string FluentDocumentationPath, + string FluentSourcePath, + string FluentTestPath, + string? GeneratorDocumentationPath, + string? GeneratorSourcePath, + string? GeneratorTestPath, + string? TrackingIssueUrl, + string ExampleDocumentationPath, + string ExampleSourcePath, + string ExampleTestPath) +{ + public bool HasSourceGeneratedPath => + !string.IsNullOrWhiteSpace(GeneratorDocumentationPath) + && !string.IsNullOrWhiteSpace(GeneratorSourcePath) + && !string.IsNullOrWhiteSpace(GeneratorTestPath); + + public bool HasTrackedGeneratorGap => !string.IsNullOrWhiteSpace(TrackingIssueUrl); +} + +/// +/// Describes one canonical GoF pattern and the PatternKit surfaces that support it. +/// +public sealed record PatternCoverageDescriptor( + string Name, + PatternFamily Family, + PatternImplementationPath Implementation, + IReadOnlyList IntegrationNotes); + +/// +/// Read-only catalog of PatternKit's coverage for the canonical GoF design patterns. +/// +public interface IPatternKitPatternCatalog +{ + IReadOnlyList Patterns { get; } +} + +/// +/// Pattern coverage manifest used by docs, tests, and production-readiness audits. +/// +public sealed class PatternKitPatternCatalog : IPatternKitPatternCatalog +{ + private static readonly IReadOnlyList Items = + [ + Pattern("Abstract Factory", PatternFamily.Creational, + "docs/patterns/creational/abstract-factory/index.md", + "src/PatternKit.Core/Creational/AbstractFactory/AbstractFactory.cs", + "test/PatternKit.Tests/Creational/AbstractFactoryTests.cs", + null, + null, + null, + "https://github.com/JerrettDavis/PatternKit/issues/207", + "docs/examples/enterprise-order.md", + "src/PatternKit.Examples/AbstractFactoryDemo/AbstractFactoryDemo.cs", + "test/PatternKit.Examples.Tests/AbstractFactoryDemo/AbstractFactoryDemoTests.cs", + ["fluent family factory", "dedicated generator tracked", "example importable through AddPatternKitExamples"]), + + Pattern("Builder", PatternFamily.Creational, + "docs/patterns/creational/builder/index.md", + "src/PatternKit.Core/Creational/Builder/MutableBuilder.cs", + "test/PatternKit.Tests/Creational/Builder/MutableBuilderTests.cs", + "docs/generators/builder.md", + "src/PatternKit.Generators/Builders/BuilderGenerator.cs", + "test/PatternKit.Generators.Tests/BuilderGeneratorTests.cs", + null, + "docs/examples/source-generator-application-suite.md", + "src/PatternKit.Examples/Generators/Builders/CorporateApplicationBuilderDemo/CorporateApplication.cs", + "test/PatternKit.Examples.Tests/Generators/CorporateApplicationBuilderDemoTests.cs", + ["fluent builders", "generated builder", "Generic Host module example"]), + + Pattern("Factory Method", PatternFamily.Creational, + "docs/patterns/creational/factory/index.md", + "src/PatternKit.Core/Creational/Factory/Factory.cs", + "test/PatternKit.Tests/Creational/Factory/FactoryTests.cs", + "docs/generators/factory-method.md", + "src/PatternKit.Generators/Factories/FactoriesGenerator.cs", + "test/PatternKit.Generators.Tests/FactoriesGeneratorTests.cs", + null, + "docs/examples/source-generator-application-suite.md", + "src/PatternKit.Examples/Generators/Factories/FactoryGeneratorExamples.cs", + "test/PatternKit.Examples.Tests/Generators/FactoryGeneratorExamplesTests.cs", + ["fluent keyed factory", "generated factory method", "startup module creation"]), + + Pattern("Prototype", PatternFamily.Creational, + "docs/patterns/creational/prototype/index.md", + "src/PatternKit.Core/Creational/Prototype/Prototype.cs", + "test/PatternKit.Tests/Creational/Prototype/PrototypeTests.cs", + "docs/generators/prototype.md", + "src/PatternKit.Generators/PrototypeGenerator.cs", + "test/PatternKit.Generators.Tests/PrototypeGeneratorTests.cs", + null, + "docs/examples/prototype-demo.md", + "src/PatternKit.Examples/PrototypeDemo/PrototypeDemo.cs", + "test/PatternKit.Examples.Tests/PrototypeDemo/PrototypeDemoTests.cs", + ["fluent clone registry", "generated clone surface", "game character example"]), + + Pattern("Singleton", PatternFamily.Creational, + "docs/patterns/creational/singleton/index.md", + "src/PatternKit.Core/Creational/Singleton/Singleton.cs", + "test/PatternKit.Tests/Creational/Singleton/SingletonTests.cs", + "docs/generators/singleton.md", + "src/PatternKit.Generators/Singleton/SingletonGenerator.cs", + "test/PatternKit.Generators.Tests/SingletonGeneratorTests.cs", + null, + "docs/examples/pos-app-state-singleton.md", + "src/PatternKit.Examples/Singleton/PosAppStateDemo.cs", + "test/PatternKit.Examples.Tests/Singleton/PosAppStateDemoTests.cs", + ["fluent singleton", "generated singleton", "DI-safe POS example"]), + + Pattern("Adapter", PatternFamily.Structural, + "docs/patterns/structural/adapter/index.md", + "src/PatternKit.Core/Structural/Adapter/Adapter.cs", + "test/PatternKit.Tests/Structural/Adapters/AdapterTests.cs", + "docs/generators/adapter.md", + "src/PatternKit.Generators/Adapter/AdapterGenerator.cs", + "test/PatternKit.Generators.Tests/AdapterGeneratorTests.cs", + null, + "docs/examples/source-generator-application-suite.md", + "src/PatternKit.Examples/AdapterGeneratorDemo/PaymentAdapter.cs", + "test/PatternKit.Examples.Tests/AdapterGeneratorDemo/AdapterGeneratorDemoTests.cs", + ["fluent DTO mapping", "generated adapter", "payment adapter example"]), + + Pattern("Bridge", PatternFamily.Structural, + "docs/patterns/structural/bridge/index.md", + "src/PatternKit.Core/Structural/Bridge/Bridge.cs", + "test/PatternKit.Tests/Structural/Bridge/BridgeTests.cs", + "docs/generators/bridge.md", + "src/PatternKit.Generators/Bridge/BridgeGenerator.cs", + "test/PatternKit.Generators.Tests/BridgeGeneratorTests.cs", + null, + "docs/patterns/structural/bridge/real-world-examples.md", + "src/PatternKit.Examples/BridgeDemo/BridgeDemo.cs", + "test/PatternKit.Examples.Tests/BridgeDemo/BridgeDemoTests.cs", + ["fluent abstraction implementation split", "generated bridge", "notification bridge example"]), + + Pattern("Composite", PatternFamily.Structural, + "docs/patterns/structural/composite/index.md", + "src/PatternKit.Core/Structural/Composite/Composite.cs", + "test/PatternKit.Tests/Structural/Composite/CompositeTests.cs", + "docs/generators/composite.md", + "src/PatternKit.Generators/Composite/CompositeGenerator.cs", + "test/PatternKit.Generators.Tests/CompositeGeneratorTests.cs", + null, + "docs/patterns/structural/composite/real-world-examples.md", + "src/PatternKit.Examples/CompositeDemo/CompositeDemo.cs", + "test/PatternKit.Examples.Tests/CompositeDemo/CompositeDemoTests.cs", + ["fluent folding", "generated composite", "file system style example"]), + + Pattern("Decorator", PatternFamily.Structural, + "docs/patterns/structural/decorator/index.md", + "src/PatternKit.Core/Structural/Decorator/Decorator.cs", + "test/PatternKit.Tests/Structural/Decorator/DecoratorTests.cs", + "docs/generators/decorator.md", + "src/PatternKit.Generators/DecoratorGenerator.cs", + "test/PatternKit.Generators.Tests/DecoratorGeneratorTests.cs", + null, + "docs/examples/payment-processor-decorator.md", + "src/PatternKit.Examples/PointOfSale/PaymentProcessorDemo.cs", + "test/PatternKit.Examples.Tests/PointOfSale/PaymentProcessorTests.cs", + ["fluent wrapping", "generated decorator", "payment processor example"]), + + Pattern("Facade", PatternFamily.Structural, + "docs/patterns/structural/facade/index.md", + "src/PatternKit.Core/Structural/Facade/Facade.cs", + "test/PatternKit.Tests/Structural/Facade/FacadeTests.cs", + "docs/generators/facade.md", + "src/PatternKit.Generators/FacadeGenerator.cs", + "test/PatternKit.Generators.Tests/FacadeGeneratorTests.cs", + null, + "docs/examples/messaging-backplane-facade.md", + "src/PatternKit.Examples/Messaging/BackplaneFacadeDemo.cs", + "test/PatternKit.Examples.Tests/Messaging/BackplaneFacadeDemoTests.cs", + ["fluent facade", "generated facade", "messaging backplane example"]), + + Pattern("Flyweight", PatternFamily.Structural, + "docs/patterns/structural/flyweight/index.md", + "src/PatternKit.Core/Structural/Flyweight/Flyweight.cs", + "test/PatternKit.Tests/Structural/Flyweight/FlyweightTests.cs", + "docs/generators/flyweight.md", + "src/PatternKit.Generators/Flyweight/FlyweightGenerator.cs", + "test/PatternKit.Generators.Tests/FlyweightGeneratorTests.cs", + null, + "docs/examples/flyweight-glyph-cache.md", + "src/PatternKit.Examples/FlyweightDemo/FlyweightDemo.cs", + "test/PatternKit.Examples.Tests/FlyweightDemos/FlyweightDemoTests.cs", + ["fluent cache", "generated cache", "glyph cache example"]), + + Pattern("Proxy", PatternFamily.Structural, + "docs/patterns/structural/proxy/index.md", + "src/PatternKit.Core/Structural/Proxy/Proxy.cs", + "test/PatternKit.Tests/Structural/Proxy/ProxyTests.cs", + "docs/generators/proxy.md", + "src/PatternKit.Generators/ProxyGenerator.cs", + "test/PatternKit.Generators.Tests/ProxyGeneratorTests.cs", + null, + "docs/examples/proxy-demo.md", + "src/PatternKit.Examples/ProxyDemo/ProxyDemo.cs", + "test/PatternKit.Examples.Tests/ProxyDemo/ProxyDemoTests.cs", + ["fluent proxy", "generated proxy", "virtual and protection examples"]), + + Pattern("Chain of Responsibility", PatternFamily.Behavioral, + "docs/patterns/behavioral/chain/index.md", + "src/PatternKit.Core/Behavioral/Chain/ActionChain.cs", + "test/PatternKit.Tests/Behavioral/Chain/ActionChainTests.cs", + "docs/generators/chain.md", + "src/PatternKit.Generators/Chain/ChainGenerator.cs", + "test/PatternKit.Generators.Tests/ChainGeneratorTests.cs", + null, + "docs/examples/auth-logging-chain.md", + "src/PatternKit.Examples/Chain/AuthLoggingDemo.cs", + "test/PatternKit.Examples.Tests/Chain/AuthLoggingDemoTests.cs", + ["fluent chain", "generated chain", "request pipeline example"]), + + Pattern("Command", PatternFamily.Behavioral, + "docs/patterns/behavioral/command/index.md", + "src/PatternKit.Core/Behavioral/Command/Command.cs", + "test/PatternKit.Tests/Behavioral/Command/CommandTests.cs", + "docs/generators/command.md", + "src/PatternKit.Generators/Command/CommandGenerator.cs", + "test/PatternKit.Generators.Tests/CommandGeneratorTests.cs", + null, + "docs/examples/resilient-checkout-and-mailboxes.md", + "src/PatternKit.Examples/Messaging/ResilientCheckoutDemo.cs", + "test/PatternKit.Examples.Tests/Messaging/ResilientCheckoutDemoTests.cs", + ["fluent command", "generated command", "checkout command example"]), + + Pattern("Interpreter", PatternFamily.Behavioral, + "docs/patterns/behavioral/interpreter/index.md", + "src/PatternKit.Core/Behavioral/Interpreter/Interpreter.cs", + "test/PatternKit.Tests/Behavioral/InterpreterTests.cs", + null, + null, + null, + "https://github.com/JerrettDavis/PatternKit/issues/206", + "docs/patterns/behavioral/interpreter/real-world-examples.md", + "src/PatternKit.Examples/InterpreterDemo/InterpreterDemo.cs", + "test/PatternKit.Examples.Tests/InterpreterDemo/InterpreterDemoTests.cs", + ["fluent interpreter", "dedicated generator tracked", "rules engine example"]), + + Pattern("Iterator", PatternFamily.Behavioral, + "docs/patterns/behavioral/iterator/index.md", + "src/PatternKit.Core/Behavioral/Iterator/Flow.cs", + "test/PatternKit.Tests/Behavioral/Iterator/FlowTests.cs", + "docs/generators/iterator.md", + "src/PatternKit.Generators/Iterator/IteratorGenerator.cs", + "test/PatternKit.Generators.Tests/IteratorGeneratorTests.cs", + null, + "docs/examples/source-generator-application-suite.md", + "src/PatternKit.Examples/IteratorDemo/IteratorDemo.cs", + "test/PatternKit.Examples.Tests/IteratorDemo/IteratorDemoTests.cs", + ["fluent flow", "generated iterator", "paged stream example"]), + + Pattern("Mediator", PatternFamily.Behavioral, + "docs/patterns/behavioral/mediator/index.md", + "src/PatternKit.Core/Behavioral/Mediator/Mediator.cs", + "test/PatternKit.Tests/Behavioral/Mediator/MediatorTests.cs", + "docs/generators/dispatcher.md", + "src/PatternKit.Generators/Messaging/DispatcherGenerator.cs", + "test/PatternKit.Generators.Tests/DispatcherGeneratorTests.cs", + null, + "docs/examples/mediator-demo.md", + "src/PatternKit.Examples/MediatorDemo/Demo.cs", + "test/PatternKit.Examples.Tests/MediatorDemo/MediatorDemoTests.cs", + ["fluent mediator", "generated dispatcher mediator", "service collaboration example"]), + + Pattern("Memento", PatternFamily.Behavioral, + "docs/patterns/behavioral/memento/index.md", + "src/PatternKit.Core/Behavioral/Memento/Memento.cs", + "test/PatternKit.Tests/Behavioral/Memento/MementoTests.cs", + "docs/generators/memento.md", + "src/PatternKit.Generators/MementoGenerator.cs", + "test/PatternKit.Generators.Tests/MementoGeneratorTests.cs", + null, + "docs/examples/text-editor-memento.md", + "src/PatternKit.Examples/MementoDemo/MementoDemo.cs", + "test/PatternKit.Examples.Tests/MementoDemo/MementoDemoTests.cs", + ["fluent memento", "generated snapshots", "text editor history example"]), + + Pattern("Observer", PatternFamily.Behavioral, + "docs/patterns/behavioral/observer/index.md", + "src/PatternKit.Core/Behavioral/Observer/Observer.cs", + "test/PatternKit.Tests/Behavioral/Observer/ObserverTests.cs", + "docs/generators/observer.md", + "src/PatternKit.Generators/Observer/ObserverGenerator.cs", + "test/PatternKit.Generators.Tests/ObserverGeneratorTests.cs", + null, + "docs/examples/observer-demo.md", + "src/PatternKit.Examples/ObserverDemo/SimpleEventHub.cs", + "test/PatternKit.Examples.Tests/ObserverDemo/EventHubTests.cs", + ["fluent observer", "generated observer hub", "event hub example"]), + + Pattern("State", PatternFamily.Behavioral, + "docs/patterns/behavioral/state/index.md", + "src/PatternKit.Core/Behavioral/State/StateMachine.cs", + "test/PatternKit.Tests/Behavioral/State/StateMachineTests.cs", + "docs/generators/state-machine.md", + "src/PatternKit.Generators/StateMachineGenerator.cs", + "test/PatternKit.Generators.Tests/StateMachineGeneratorTests.cs", + null, + "docs/examples/state-machine.md", + "src/PatternKit.Examples/StateDemo/StateDemo.cs", + "test/PatternKit.Examples.Tests/StateDemo/StateDemoTests.cs", + ["fluent state machine", "generated state machine", "order lifecycle example"]), + + Pattern("Strategy", PatternFamily.Behavioral, + "docs/patterns/behavioral/strategy/index.md", + "src/PatternKit.Core/Behavioral/Strategy/Strategy.cs", + "test/PatternKit.Tests/Behavioral/Strategy/StrategyTests.cs", + "docs/generators/strategy.md", + "src/PatternKit.Generators/StrategyGenerator.cs", + "test/PatternKit.Generators.Tests/StrategyGeneratorTests.cs", + null, + "docs/examples/composed-notification-strategy.md", + "src/PatternKit.Examples/Strategies/Composed/ComposedStrategies.cs", + "test/PatternKit.Examples.Tests/Strategies/Composed/ComposedStrategiesTests.cs", + ["fluent strategy", "generated strategy", "notification channel example"]), + + Pattern("Template Method", PatternFamily.Behavioral, + "docs/patterns/behavioral/template/index.md", + "src/PatternKit.Core/Behavioral/Template/TemplateMethod.cs", + "test/PatternKit.Tests/Behavioral/TemplateMethodTests.cs", + "docs/generators/template-method-generator.md", + "src/PatternKit.Generators/TemplateGenerator.cs", + "test/PatternKit.Generators.Tests/TemplateGeneratorTests.cs", + null, + "docs/examples/template-method-demo.md", + "src/PatternKit.Examples/TemplateDemo/TemplateDemo.cs", + "test/PatternKit.Examples.Tests/TemplateDemo/TemplateDemoTests.cs", + ["fluent and base template", "generated template", "import workflow example"]), + + Pattern("Visitor", PatternFamily.Behavioral, + "docs/patterns/behavioral/visitor/index.md", + "src/PatternKit.Core/Behavioral/Visitor/Visitor.cs", + "test/PatternKit.Tests/Behavioral/VisitorTests.cs", + "docs/generators/visitor-generator.md", + "src/PatternKit.Generators/VisitorGenerator.cs", + "test/PatternKit.Generators.Tests/VisitorGeneratorTests.cs", + null, + "docs/examples/document-processing-visitor.md", + "src/PatternKit.Examples/Generators/Visitors/DocumentProcessingDemo.cs", + "test/PatternKit.Examples.Tests/Generators/VisitorGeneratorExamplesTests.cs", + ["fluent visitor", "generated visitor", "document processing example"]) + ]; + + public IReadOnlyList Patterns => Items; + + private static PatternCoverageDescriptor Pattern( + string name, + PatternFamily family, + string fluentDocumentationPath, + string fluentSourcePath, + string fluentTestPath, + string? generatorDocumentationPath, + string? generatorSourcePath, + string? generatorTestPath, + string? trackingIssueUrl, + string exampleDocumentationPath, + string exampleSourcePath, + string exampleTestPath, + IReadOnlyList integrationNotes) + => new( + name, + family, + new PatternImplementationPath( + name, + fluentDocumentationPath, + fluentSourcePath, + fluentTestPath, + generatorDocumentationPath, + generatorSourcePath, + generatorTestPath, + trackingIssueUrl, + exampleDocumentationPath, + exampleSourcePath, + exampleTestPath), + integrationNotes); +} + +public static class PatternKitPatternCatalogServiceCollectionExtensions +{ + public static IServiceCollection AddPatternKitPatternCatalog(this IServiceCollection services) + { + services.AddSingleton(); + return services; + } +} diff --git a/test/PatternKit.Examples.Tests/ProductionReadiness/PatternKitPatternCatalogTests.cs b/test/PatternKit.Examples.Tests/ProductionReadiness/PatternKitPatternCatalogTests.cs new file mode 100644 index 00000000..e2fb01ce --- /dev/null +++ b/test/PatternKit.Examples.Tests/ProductionReadiness/PatternKitPatternCatalogTests.cs @@ -0,0 +1,167 @@ +using Microsoft.Extensions.DependencyInjection; +using PatternKit.Examples.ProductionReadiness; +using TinyBDD; +using TinyBDD.Xunit; +using Xunit.Abstractions; + +namespace PatternKit.Examples.Tests.ProductionReadiness; + +[Feature("GoF pattern coverage catalog")] +public sealed class PatternKitPatternCatalogTests(ITestOutputHelper output) : TinyBddXunitBase(output) +{ + private static readonly string[] CanonicalGofPatterns = + [ + "Abstract Factory", + "Builder", + "Factory Method", + "Prototype", + "Singleton", + "Adapter", + "Bridge", + "Composite", + "Decorator", + "Facade", + "Flyweight", + "Proxy", + "Chain of Responsibility", + "Command", + "Interpreter", + "Iterator", + "Mediator", + "Memento", + "Observer", + "State", + "Strategy", + "Template Method", + "Visitor" + ]; + + [Scenario("Catalog covers every canonical GoF pattern")] + [Fact] + public Task Catalog_Covers_Every_Canonical_Gof_Pattern() + => Given("the PatternKit pattern catalog", () => new PatternKitPatternCatalog()) + .When("reading the catalog entries", catalog => catalog.Patterns) + .Then("all canonical GoF patterns are represented exactly once", patterns => + { + ScenarioExpect.Equal(CanonicalGofPatterns.OrderBy(static x => x), patterns.Select(static p => p.Name).OrderBy(static x => x)); + ScenarioExpect.Equal(CanonicalGofPatterns.Length, patterns.Select(static p => p.Name).Distinct(StringComparer.Ordinal).Count()); + }) + .And("the catalog keeps the GoF family counts honest", patterns => + { + ScenarioExpect.Equal(5, patterns.Count(static p => p.Family == PatternFamily.Creational)); + ScenarioExpect.Equal(7, patterns.Count(static p => p.Family == PatternFamily.Structural)); + ScenarioExpect.Equal(11, patterns.Count(static p => p.Family == PatternFamily.Behavioral)); + }) + .AssertPassed(); + + [Scenario("Each pattern has fluent generated documented and example paths")] + [Fact] + public Task Each_Pattern_Has_Fluent_Generated_Documented_And_Example_Paths() + => Given("the PatternKit pattern catalog and repository root", () => new + { + Catalog = new PatternKitPatternCatalog(), + RepositoryRoot = FindRepoRoot() + }) + .When("validating implementation paths", ctx => ctx.Catalog.Patterns + .SelectMany(pattern => ValidatePattern(ctx.RepositoryRoot, pattern)) + .ToArray()) + .Then("all fluent documentation source tests and examples exist", issues => + ScenarioExpect.Empty(issues.Where(static issue => !issue.Contains("tracked source-generated gap", StringComparison.Ordinal)))) + .And("only approved source-generator gaps remain tracked", issues => + { + var tracked = issues + .Where(static issue => issue.Contains("tracked source-generated gap", StringComparison.Ordinal)) + .OrderBy(static issue => issue) + .ToArray(); + + ScenarioExpect.Equal( + [ + "Abstract Factory has a tracked source-generated gap: https://github.com/JerrettDavis/PatternKit/issues/207", + "Interpreter has a tracked source-generated gap: https://github.com/JerrettDavis/PatternKit/issues/206" + ], tracked); + }) + .AssertPassed(); + + [Scenario("Pattern catalog is available through IServiceCollection")] + [Fact] + public Task Pattern_Catalog_Is_Available_Through_IServiceCollection() + => Given("a service collection configured with the pattern catalog", () => + { + var services = new ServiceCollection(); + services.AddPatternKitPatternCatalog(); + return services.BuildServiceProvider(validateScopes: true); + }) + .When("resolving the catalog", provider => + { + using (provider) + return provider.GetRequiredService(); + }) + .Then("the catalog resolves all GoF patterns", catalog => + ScenarioExpect.Equal(CanonicalGofPatterns.Length, catalog.Patterns.Count)) + .And("all patterns include user-facing integration notes", catalog => + ScenarioExpect.True(catalog.Patterns.All(static pattern => pattern.IntegrationNotes.Count > 0))) + .AssertPassed(); + + private static IEnumerable ValidatePattern(string repositoryRoot, PatternCoverageDescriptor pattern) + { + var implementation = pattern.Implementation; + + foreach (var issue in ValidatePath(repositoryRoot, pattern.Name, "fluent docs", implementation.FluentDocumentationPath)) + yield return issue; + foreach (var issue in ValidatePath(repositoryRoot, pattern.Name, "fluent source", implementation.FluentSourcePath)) + yield return issue; + foreach (var issue in ValidatePath(repositoryRoot, pattern.Name, "fluent tests", implementation.FluentTestPath)) + yield return issue; + foreach (var issue in ValidatePath(repositoryRoot, pattern.Name, "example docs", implementation.ExampleDocumentationPath)) + yield return issue; + foreach (var issue in ValidatePath(repositoryRoot, pattern.Name, "example source", implementation.ExampleSourcePath)) + yield return issue; + foreach (var issue in ValidatePath(repositoryRoot, pattern.Name, "example tests", implementation.ExampleTestPath)) + yield return issue; + + if (implementation.HasSourceGeneratedPath) + { + foreach (var issue in ValidatePath(repositoryRoot, pattern.Name, "generator docs", implementation.GeneratorDocumentationPath!)) + yield return issue; + foreach (var issue in ValidatePath(repositoryRoot, pattern.Name, "generator source", implementation.GeneratorSourcePath!)) + yield return issue; + foreach (var issue in ValidatePath(repositoryRoot, pattern.Name, "generator tests", implementation.GeneratorTestPath!)) + yield return issue; + } + else if (implementation.HasTrackedGeneratorGap) + { + yield return $"{pattern.Name} has a tracked source-generated gap: {implementation.TrackingIssueUrl}"; + } + else + { + yield return $"{pattern.Name} is missing a source-generated path and a tracking issue."; + } + } + + private static IEnumerable ValidatePath(string repositoryRoot, string patternName, string surface, string relativePath) + { + if (string.IsNullOrWhiteSpace(relativePath)) + { + yield return $"{patternName} {surface} path is required."; + yield break; + } + + var fullPath = Path.GetFullPath(Path.Combine(repositoryRoot, relativePath.Replace('/', Path.DirectorySeparatorChar))); + if (!File.Exists(fullPath)) + yield return $"{patternName} {surface} path does not exist: {relativePath}"; + } + + private static string FindRepoRoot() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory is not null) + { + if (File.Exists(Path.Combine(directory.FullName, "PatternKit.slnx"))) + return directory.FullName; + + directory = directory.Parent; + } + + throw new DirectoryNotFoundException("Could not find PatternKit repository root."); + } +}