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
43 changes: 43 additions & 0 deletions docs/examples/abstract-factory-widget-families.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Abstract Factory Widget Families

This example demonstrates a generated Abstract Factory for platform-specific UI widget families. The runtime path is still `AbstractFactory<Platform>`; the generated path removes repetitive family registration code.

Source:

- `src/PatternKit.Examples/AbstractFactoryDemo/AbstractFactoryDemo.cs`
- `test/PatternKit.Examples.Tests/AbstractFactoryDemo/AbstractFactoryDemoTests.cs`

## Generated Family Matrix

The example declares the family matrix once:

```csharp
[GenerateAbstractFactory(typeof(AbstractFactoryDemo.Platform), FactoryMethodName = "Create", ServiceProviderFactoryMethodName = "CreateFromServices")]
[AbstractFactoryProduct(AbstractFactoryDemo.Platform.Windows, typeof(AbstractFactoryDemo.IButton), typeof(AbstractFactoryDemo.WindowsButton))]
[AbstractFactoryProduct(AbstractFactoryDemo.Platform.Windows, typeof(AbstractFactoryDemo.ITextBox), typeof(AbstractFactoryDemo.WindowsTextBox))]
[AbstractFactoryProduct(AbstractFactoryDemo.Platform.Linux, typeof(AbstractFactoryDemo.IButton), typeof(AbstractFactoryDemo.LinuxButton))]
[AbstractFactoryProduct(AbstractFactoryDemo.Platform.Linux, typeof(AbstractFactoryDemo.ITextBox), typeof(AbstractFactoryDemo.LinuxTextBox))]
public static partial class GeneratedPlatformWidgetFactory;
```

`CreateUIFactory()` calls the generated factory and returns the same runtime abstraction consumers already use:

```csharp
var factory = AbstractFactoryDemo.CreateUIFactory();
var windows = factory.GetFamily(AbstractFactoryDemo.Platform.Windows);
var button = windows.Create<AbstractFactoryDemo.IButton>();
```

## IServiceCollection Import

The example also exposes an importable DI path:

```csharp
services.AddAbstractFactoryWidgetExample();
```

The registration uses the generated `CreateFromServices(IServiceProvider)` overload so concrete widget products can evolve toward constructor-injected dependencies without changing client code.

## Tested Behavior

The TinyBDD coverage validates that every platform family can create every widget contract, product behavior remains platform-specific, and the DI registration resolves an `AbstractFactory<Platform>` that importing applications can use directly.
48 changes: 48 additions & 0 deletions docs/generators/abstract-factory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Abstract Factory Generator

`[GenerateAbstractFactory]` emits an `AbstractFactory<TKey>` from declarative product-family attributes. Use it when the family matrix is known at compile time and should stay reviewable, type-safe, and importable through normal application composition.

## Quick Start

```csharp
using PatternKit.Generators.Factories;

[GenerateAbstractFactory(typeof(Platform), FactoryMethodName = "Create", ServiceProviderFactoryMethodName = "CreateFromServices")]
[AbstractFactoryProduct(Platform.Windows, typeof(IButton), typeof(WindowsButton))]
[AbstractFactoryProduct(Platform.Windows, typeof(ITextBox), typeof(WindowsTextBox))]
[AbstractFactoryProduct(Platform.Linux, typeof(IButton), typeof(LinuxButton))]
[AbstractFactoryProduct(Platform.Linux, typeof(ITextBox), typeof(LinuxTextBox))]
public static partial class PlatformWidgetFactory;
```

Generated API:

```csharp
public static AbstractFactory<Platform> Create();
public static AbstractFactory<Platform> CreateFromServices(IServiceProvider services);
```

The normal factory path uses public parameterless constructors. The optional service-provider path uses `ActivatorUtilities.CreateInstance<T>(services)`, so products can add constructor dependencies when the importing app registers them with `IServiceCollection`.

## Behavior

- `[GenerateAbstractFactory]` must be placed on a partial class or struct.
- `[AbstractFactoryProduct]` declares one contract/implementation pair for one family key.
- `ImplementationType` must be a concrete class assignable to `ContractType`.
- Each generated factory calls the runtime fluent API: `AbstractFactory<TKey>.Create().Family(...).Product<...>().Build()`.
- Set `IsDefaultFamily = true` on a product to add it to the default family instead of a keyed family.

## Diagnostics

| ID | Meaning |
| --- | --- |
| `PKAF001` | The generated host type is not partial. |
| `PKAF002` | No products were declared. |
| `PKAF003` | A product declaration has an invalid key, contract, implementation, or constructor. |
| `PKAF004` | A family declares the same product contract more than once. |

## Example Source

- `src/PatternKit.Examples/AbstractFactoryDemo/AbstractFactoryDemo.cs`
- `test/PatternKit.Examples.Tests/AbstractFactoryDemo/AbstractFactoryDemoTests.cs`
- `test/PatternKit.Generators.Tests/AbstractFactoryGeneratorTests.cs`
7 changes: 7 additions & 0 deletions docs/generators/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ PatternKit includes a Roslyn incremental generator package (`PatternKit.Generato

| Generator | Description | Attribute |
|---|---|---|
| [**Abstract Factory**](abstract-factory.md) | Product-family factories with optional IServiceProvider construction | `[GenerateAbstractFactory]` |
| [**Builder**](builder.md) | GoF-aligned builders with mutable or state-projection models, sync/async pipelines | `[GenerateBuilder]` |
| [**Factory Method**](factory-method.md) | Keyed dispatcher from a static partial class | `[FactoryMethod]` |
| [**Factory Class**](factory-class.md) | GoF-style factory mapping keys to products | `[FactoryClass]` |
Expand Down Expand Up @@ -87,6 +88,12 @@ public partial class Person { public string Name { get; set; } }
[GenerateFactory(typeof(INotification), typeof(NotificationKind))]
public abstract partial class NotificationFactory { }

// Abstract factory - generated product families
[GenerateAbstractFactory(typeof(Platform))]
[AbstractFactoryProduct(Platform.Windows, typeof(IButton), typeof(WindowsButton))]
[AbstractFactoryProduct(Platform.Linux, typeof(IButton), typeof(LinuxButton))]
public static partial class PlatformWidgets { }

// Prototype - cloning
[Prototype]
public partial class Document { }
Expand Down
3 changes: 3 additions & 0 deletions docs/generators/toc.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
- name: Generators Overview
href: index.md

- name: Abstract Factory
href: abstract-factory.md

- name: Adapter
href: adapter.md

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/pattern-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The source of truth is `PatternKitPatternCatalog` in `src/PatternKit.Examples/Pr

| Family | Pattern | Fluent path | Source-generated path |
| --- | --- | --- | --- |
| Creational | Abstract Factory | `AbstractFactory<,>` | Tracked in [#207](https://github.com/JerrettDavis/PatternKit/issues/207) |
| Creational | Abstract Factory | `AbstractFactory<TKey>` | Abstract Factory generator |
| Creational | Builder | Builder helpers | Builder generator |
| Creational | Factory Method | `Factory<TKey, TValue>` | Factory Method generator |
| Creational | Prototype | `Prototype<TKey, TValue>` | Prototype generator |
Expand Down
9 changes: 9 additions & 0 deletions docs/patterns/creational/abstract-factory/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ IButton button = family.Create<IButton>(); // DarkButton
ITextBox textBox = family.Create<ITextBox>(); // DarkTextBox
```

For static family matrices, use the [Abstract Factory Generator](../../../generators/abstract-factory.md) to emit the same `AbstractFactory<TKey>` runtime object from attributes:

```csharp
[GenerateAbstractFactory(typeof(Theme))]
[AbstractFactoryProduct(Theme.Light, typeof(IButton), typeof(LightButton))]
[AbstractFactoryProduct(Theme.Dark, typeof(IButton), typeof(DarkButton))]
public static partial class ThemeWidgets;
```

## What It Is

Abstract Factory provides a way to encapsulate a group of individual factories that have a common theme. It creates families of related or dependent objects without specifying their concrete classes.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using PatternKit.Creational.AbstractFactory;
using PatternKit.Generators.Factories;

namespace PatternKit.Examples.AbstractFactoryDemo;

Expand Down Expand Up @@ -172,31 +173,13 @@ public static Platform DetectPlatform()
/// Each platform (Windows, macOS, Linux) is a separate product family.
/// </summary>
public static AbstractFactory<Platform> CreateUIFactory()
{
return AbstractFactory<Platform>.Create()
// Windows family
.Family(Platform.Windows)
.Product<IButton>(() => new WindowsButton())
.Product<ITextBox>(() => new WindowsTextBox())
.Product<ICheckBox>(() => new WindowsCheckBox())
.Product<IDialog>(() => new WindowsDialog())

// macOS family
.Family(Platform.MacOS)
.Product<IButton>(() => new MacButton())
.Product<ITextBox>(() => new MacTextBox())
.Product<ICheckBox>(() => new MacCheckBox())
.Product<IDialog>(() => new MacDialog())

// Linux family
.Family(Platform.Linux)
.Product<IButton>(() => new LinuxButton())
.Product<ITextBox>(() => new LinuxTextBox())
.Product<ICheckBox>(() => new LinuxCheckBox())
.Product<IDialog>(() => new LinuxDialog())

.Build();
}
=> GeneratedPlatformWidgetFactory.Create();

/// <summary>
/// Creates a platform-specific UI factory through <see cref="IServiceProvider"/> so product constructors can use application services.
/// </summary>
public static AbstractFactory<Platform> CreateUIFactory(IServiceProvider services)
=> GeneratedPlatformWidgetFactory.CreateFromServices(services);

// ─────────────────────────────────────────────────────────────────────────
// Client Code - Platform Agnostic
Expand Down Expand Up @@ -290,3 +273,18 @@ public static void Run()
Console.WriteLine("═══════════════════════════════════════════════════════════════");
}
}

[GenerateAbstractFactory(typeof(AbstractFactoryDemo.Platform), FactoryMethodName = "Create", ServiceProviderFactoryMethodName = "CreateFromServices")]
[AbstractFactoryProduct(AbstractFactoryDemo.Platform.Windows, typeof(AbstractFactoryDemo.IButton), typeof(AbstractFactoryDemo.WindowsButton))]
[AbstractFactoryProduct(AbstractFactoryDemo.Platform.Windows, typeof(AbstractFactoryDemo.ITextBox), typeof(AbstractFactoryDemo.WindowsTextBox))]
[AbstractFactoryProduct(AbstractFactoryDemo.Platform.Windows, typeof(AbstractFactoryDemo.ICheckBox), typeof(AbstractFactoryDemo.WindowsCheckBox))]
[AbstractFactoryProduct(AbstractFactoryDemo.Platform.Windows, typeof(AbstractFactoryDemo.IDialog), typeof(AbstractFactoryDemo.WindowsDialog))]
[AbstractFactoryProduct(AbstractFactoryDemo.Platform.MacOS, typeof(AbstractFactoryDemo.IButton), typeof(AbstractFactoryDemo.MacButton))]
[AbstractFactoryProduct(AbstractFactoryDemo.Platform.MacOS, typeof(AbstractFactoryDemo.ITextBox), typeof(AbstractFactoryDemo.MacTextBox))]
[AbstractFactoryProduct(AbstractFactoryDemo.Platform.MacOS, typeof(AbstractFactoryDemo.ICheckBox), typeof(AbstractFactoryDemo.MacCheckBox))]
[AbstractFactoryProduct(AbstractFactoryDemo.Platform.MacOS, typeof(AbstractFactoryDemo.IDialog), typeof(AbstractFactoryDemo.MacDialog))]
[AbstractFactoryProduct(AbstractFactoryDemo.Platform.Linux, typeof(AbstractFactoryDemo.IButton), typeof(AbstractFactoryDemo.LinuxButton))]
[AbstractFactoryProduct(AbstractFactoryDemo.Platform.Linux, typeof(AbstractFactoryDemo.ITextBox), typeof(AbstractFactoryDemo.LinuxTextBox))]
[AbstractFactoryProduct(AbstractFactoryDemo.Platform.Linux, typeof(AbstractFactoryDemo.ICheckBox), typeof(AbstractFactoryDemo.LinuxCheckBox))]
[AbstractFactoryProduct(AbstractFactoryDemo.Platform.Linux, typeof(AbstractFactoryDemo.IDialog), typeof(AbstractFactoryDemo.LinuxDialog))]
public static partial class GeneratedPlatformWidgetFactory;
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using PatternKit.Behavioral.Chain;
using PatternKit.Behavioral.Strategy;
using PatternKit.Behavioral.TypeDispatcher;
using PatternKit.Creational.AbstractFactory;
using PatternKit.Creational.Prototype;
using PatternKit.Creational.Singleton;
using PatternKit.Examples.ApiGateway;
Expand Down Expand Up @@ -42,6 +43,7 @@
using ShowcaseFacade = PatternKit.Examples.PatternShowcase.PatternShowcase.IOrderProcessingFacade;
using TransactionPipeline = PatternKit.Examples.Chain.TransactionPipeline;
using VisitorTender = PatternKit.Examples.VisitorDemo.Tender;
using WidgetDemo = PatternKit.Examples.AbstractFactoryDemo.AbstractFactoryDemo;

namespace PatternKit.Examples.DependencyInjection;

Expand Down Expand Up @@ -70,6 +72,7 @@ public sealed class CoercerService<T> : ICoercer<T>
}

public sealed record ProductionReadyExampleIntegrations(IPatternKitExampleCatalog ExampleCatalog, IPatternKitPatternCatalog PatternCatalog);
public sealed record AbstractFactoryWidgetExample(AbstractFactory<WidgetDemo.Platform> Factory);
public sealed record AuthLoggingChainExample(ActionChain<HttpRequest> Chain, List<string> Log);
public sealed record CoercionExample(ICoercer<int> Integers, ICoercer<bool> Booleans, ICoercer<string> Strings);
public sealed record ComposedNotificationStrategyExample(AsyncStrategy<SendContext, SendResult> Strategy);
Expand Down Expand Up @@ -114,6 +117,7 @@ public static class PatternKitExampleServiceCollectionExtensions
public static IServiceCollection AddPatternKitExamples(this IServiceCollection services, IConfiguration? configuration = null)
=> services
.AddProductionReadyExampleIntegrations()
.AddAbstractFactoryWidgetExample()
.AddAuthLoggingChainExample()
.AddStrategyBasedDataCoercionExample()
.AddComposedNotificationStrategyExample()
Expand Down Expand Up @@ -161,6 +165,13 @@ public static IServiceCollection AddProductionReadyExampleIntegrations(this ISer
return services.RegisterExample<ProductionReadyExampleIntegrations>("Production-Ready Example Integrations", ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost | ExampleIntegrationSurface.AspNetCore);
}

public static IServiceCollection AddAbstractFactoryWidgetExample(this IServiceCollection services)
{
services.AddSingleton(static sp => WidgetDemo.CreateUIFactory(sp));
services.AddSingleton<AbstractFactoryWidgetExample>(sp => new(sp.GetRequiredService<AbstractFactory<WidgetDemo.Platform>>()));
return services.RegisterExample<AbstractFactoryWidgetExample>("Abstract Factory Widget Families", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection);
}

public static IServiceCollection AddAuthLoggingChainExample(this IServiceCollection services)
{
services.AddSingleton<AuthLoggingChainExample>(_ =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,14 @@ public sealed class PatternKitExampleCatalog : IPatternKitExampleCatalog
ExampleIntegrationSurface.GenericHost | ExampleIntegrationSurface.Messaging | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.ExternalInfrastructure,
["Facade", "Mailbox", "Outbox", "IdempotentReceiver"],
["host setup", "generated request/reply topology", "generated pub/sub topology", "transport boundary"]),
Descriptor(
"Abstract Factory Widget Families",
"src/PatternKit.Examples/AbstractFactoryDemo/AbstractFactoryDemo.cs",
"test/PatternKit.Examples.Tests/AbstractFactoryDemo/AbstractFactoryDemoTests.cs",
"docs/examples/abstract-factory-widget-families.md",
ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection,
["AbstractFactory"],
["generated family factory", "platform widgets", "DI composition"]),
Descriptor(
"Prototype Game Character Factory",
"src/PatternKit.Examples/PrototypeDemo/PrototypeDemo.cs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,14 @@ public sealed class PatternKitPatternCatalog : IPatternKitPatternCatalog
"docs/patterns/creational/abstract-factory/index.md",
"src/PatternKit.Core/Creational/AbstractFactory/AbstractFactory.cs",
"test/PatternKit.Tests/Creational/AbstractFactoryTests.cs",
"docs/generators/abstract-factory.md",
"src/PatternKit.Generators/Factories/AbstractFactoryGenerator.cs",
"test/PatternKit.Generators.Tests/AbstractFactoryGeneratorTests.cs",
null,
null,
null,
"https://github.com/JerrettDavis/PatternKit/issues/207",
"docs/examples/enterprise-order.md",
"docs/examples/abstract-factory-widget-families.md",
"src/PatternKit.Examples/AbstractFactoryDemo/AbstractFactoryDemo.cs",
"test/PatternKit.Examples.Tests/AbstractFactoryDemo/AbstractFactoryDemoTests.cs",
["fluent family factory", "dedicated generator tracked", "example importable through AddPatternKitExamples"]),
["fluent family factory", "generated family factory", "example importable through AddPatternKitExamples"]),

Pattern("Builder", PatternFamily.Creational,
"docs/patterns/creational/builder/index.md",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
namespace PatternKit.Generators.Factories;

[AttributeUsage(AttributeTargets.Class)]
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class FactoryMethodAttribute(Type keyType) : Attribute
{
public Type KeyType { get; } = keyType;
public string CreateMethodName { get; set; } = "Create";
public bool CaseInsensitiveStrings { get; set; } = true;
}

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
public sealed class FactoryCaseAttribute(object key) : Attribute
{
public object Key { get; } = key;
}

[AttributeUsage(AttributeTargets.Method)]
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public sealed class FactoryDefaultAttribute : Attribute
{
}

[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class)]
[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class, Inherited = false)]
public sealed class FactoryClassAttribute(Type keyType) : Attribute
{
public Type KeyType { get; } = keyType;
Expand All @@ -28,8 +28,25 @@ public sealed class FactoryClassAttribute(Type keyType) : Attribute
public bool GenerateEnumKeys { get; set; } = false;
}

[AttributeUsage(AttributeTargets.Class)]
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class FactoryClassKeyAttribute(object key) : Attribute
{
public object Key { get; } = key;
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)]
public sealed class GenerateAbstractFactoryAttribute(Type keyType) : Attribute
{
public Type KeyType { get; } = keyType;
public string FactoryMethodName { get; set; } = "Create";
public string? ServiceProviderFactoryMethodName { get; set; }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true, Inherited = false)]
public sealed class AbstractFactoryProductAttribute(object familyKey, Type contractType, Type implementationType) : Attribute
{
public object FamilyKey { get; } = familyKey;
public Type ContractType { get; } = contractType;
public Type ImplementationType { get; } = implementationType;
public bool IsDefaultFamily { get; set; }
}
Loading
Loading