Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 30, 2026

Template Method Pattern Generator - Complete Implementation

Summary

Source generator for Template Method pattern with deterministic step ordering, lifecycle hooks, and async/await support. All tests passing (21 tests).

Latest Changes - Second PR Review Round

Fixed CancellationToken detection - Now uses fully qualified type name System.Threading.CancellationToken instead of just type name to avoid false matches

  • Added helper method IsCancellationToken for consistency
  • Updated all usages in DetermineIfAsync and code generation sections
    Updated documentation to accurately reflect HandleAndContinue behavior:
  • Changed "Execution continues with next step" to "Exception is suppressed (not rethrown), Workflow terminates"
  • Changed "remaining steps are all optional" to "all steps are optional"
  • Fixed example to mark Step1 as optional
  • Updated diagnostic resolution text to "Make all steps optional"
    Improved error message in ImportWorkflowDemo - Changed from hardcoded "Import failed at validation stage" to generic "Import failed" since OnError can be invoked from any step
    Refactored validation loops - Changed ValidateSignatures to use LINQ .Any() instead of foreach with continue/break

Previous Changes - First PR Review Round

✅ Removed unused imports, PKTMP006 diagnostic
✅ Fixed ValueTask validation to distinguish non-generic from generic
✅ Added 5 new tests (21 total, all passing)
✅ Refactored loops to use explicit LINQ filtering
✅ Simplified if-else and foreach in examples

Core Features

  • Deterministic step ordering
  • Lifecycle hooks (BeforeAll, AfterAll, OnError)
  • Async/await with non-generic ValueTask and CancellationToken
  • Error handling with configurable policies
  • Support for class, struct, record class, record struct
  • 7 actionable diagnostics
  • Zero runtime dependency
Original prompt

This section details on the original issue you should resolve

<issue_title>Generator: Create Template Method Pattern</issue_title>
<issue_description>## Summary

Add a source generator that produces a complete implementation of the Template Method pattern for consumer-defined workflows.

The generator lives in PatternKit.Generators and emits self-contained, readable C# with no runtime PatternKit dependency.

Primary goals:

  • Define a canonical sequence of steps (the “template”).
  • Allow consumers to override hooks and optional steps safely.
  • Support sync + async flows (favoring ValueTask).
  • Provide deterministic ordering, diagnostics, and testability.

Motivation / Problem

Template Method is commonly re-implemented as:

  • base classes with virtual methods (inheritance tax)
  • ad-hoc pipelines with no clear contract for overrides
  • scattered hooks with inconsistent error handling

We want a declarative, boilerplate-free way to define a workflow that:

  • makes step order explicit
  • makes override points explicit
  • is friendly to records/immutability
  • is reflection-free and generator-deterministic

Supported Targets (must-have)

The generator must support:

  • partial class
  • partial struct
  • partial record class
  • partial record struct

The annotated type represents the workflow host (the place consumers call Execute/ExecuteAsync).


Proposed User Experience

Minimal template with required steps

[Template]
public partial class ImportWorkflow
{
    [TemplateStep(Order = 0)]
    private void Validate(ImportContext ctx);

    [TemplateStep(Order = 1)]
    private void Transform(ImportContext ctx);

    [TemplateStep(Order = 2)]
    private void Persist(ImportContext ctx);
}

Generated (representative shape):

  • Execute(ImportContext ctx) invokes steps in deterministic order.
  • ExecuteAsync(ImportContext ctx, CancellationToken ct = default) emitted when async steps/hooks exist or ForceAsync=true.

Hooks + error handling

[Template(GenerateAsync = true)]
public partial class ImportWorkflow
{
    [TemplateHook(HookPoint.BeforeAll)]
    private void OnStart(ImportContext ctx);

    [TemplateStep(0)]
    private ValueTask ValidateAsync(ImportContext ctx, CancellationToken ct);

    [TemplateStep(1)]
    private void Transform(ImportContext ctx);

    [TemplateHook(HookPoint.OnError)]
    private void OnError(ImportContext ctx, Exception ex);

    [TemplateHook(HookPoint.AfterAll)]
    private void OnComplete(ImportContext ctx);
}

Attributes / Surface Area

Namespace: PatternKit.Generators.Template

Core attributes

  • [Template] on the workflow host
  • [TemplateStep] on methods that form the required step sequence
  • [TemplateHook] on methods that plug into hook points

Suggested shapes:

TemplateAttribute

  • string ExecuteMethodName = "Execute"
  • string ExecuteAsyncMethodName = "ExecuteAsync"
  • bool GenerateAsync (default: inferred)
  • bool ForceAsync (default: false)
  • TemplateErrorPolicy ErrorPolicy (default: Rethrow)

TemplateStepAttribute

  • int Order (required in v1)
  • bool Optional (default: false)
  • string? Name (optional; for diagnostics)

TemplateHookAttribute

  • HookPoint HookPoint (BeforeAll, AfterAll, OnError)
  • int? StepOrder (reserved for v2: BeforeStep/AfterStep targeting)

Step Semantics

  • Steps execute in ascending Order.
  • Duplicate orders are errors.
  • Steps are invoked exactly once unless marked optional and suppressed (v2). In v1, Optional only affects error-policy eligibility.

Async rules

  • If any step/hook returns ValueTask or accepts CancellationToken, generator emits async path.
  • Sync Execute can exist alongside async ExecuteAsync.
  • Generated async APIs use ValueTask.

Error rules

  • Default: throw after invoking OnError (if present).
  • If ErrorPolicy = HandleAndContinue, only allowed when all remaining steps are optional. Otherwise emit diagnostic.

Diagnostics (must-have)

Stable IDs, actionable:

  • PKTMP001 Type marked [Template] must be partial.
  • PKTMP002 No [TemplateStep] methods found.
  • PKTMP003 Duplicate step order detected.
  • PKTMP004 Step method signature invalid.
  • PKTMP005 Hook method signature invalid.
  • PKTMP006 Mixed sync/async signatures not supported (explain required shapes).
  • PKTMP007 CancellationToken required for async step but missing.
  • PKTMP008 HandleAndContinue not allowed when non-optional steps remain.

Generated Code Layout

  • TypeName.Template.g.cs

Deterministic ordering:

  • steps ordered by Order, then by fully-qualified method name for tie-breaking in diagnostics.

Testing Expectations

  • Deterministic ordering with multiple steps.
  • Hooks fire at correct points.
  • Error behavior: OnError invoked; default rethrow policy enforced.
  • Async path uses ValueTask and respects cancellation....

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 5 commits January 30, 2026 06:22
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
Copilot AI changed the title [WIP] Add source generator for Template Method pattern Add Template Method pattern source generator Jan 30, 2026
Copilot AI requested a review from JerrettDavis January 30, 2026 06:45
@JerrettDavis JerrettDavis marked this pull request as ready for review January 30, 2026 19:07
@JerrettDavis JerrettDavis requested a review from Copilot January 30, 2026 19:07
@github-actions
Copy link
Contributor

github-actions bot commented Jan 30, 2026

⚠️ Deprecation Warning: The deny-licenses option is deprecated for possible removal in the next major release. For more information, see issue 997.

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@github-actions
Copy link
Contributor

github-actions bot commented Jan 30, 2026

Test Results

313 tests   313 ✅  1m 27s ⏱️
  1 suites    0 💤
  1 files      0 ❌

Results for commit 699b33d.

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 30, 2026

🔍 PR Validation Results

Version: ``

✅ Validation Steps

  • Build solution
  • Run tests
  • Build documentation
  • Dry-run NuGet packaging

📊 Artifacts

Dry-run artifacts have been uploaded and will be available for 7 days.


This comment was automatically generated by the PR validation workflow.

@codecov
Copy link

codecov bot commented Jan 30, 2026

Codecov Report

❌ Patch coverage is 78.30882% with 118 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.49%. Comparing base (461ad82) to head (699b33d).

Files with missing lines Patch % Lines
src/PatternKit.Generators/TemplateGenerator.cs 78.41% 56 Missing and 23 partials ⚠️
.../TemplateMethodGeneratorDemo/ImportWorkflowDemo.cs 83.54% 10 Missing and 3 partials ⚠️
...TemplateMethodGeneratorDemo/OrderProcessingDemo.cs 87.95% 8 Missing and 2 partials ⚠️
...ors.Abstractions/Template/TemplateStepAttribute.cs 0.00% 6 Missing ⚠️
...erators.Abstractions/Template/TemplateAttribute.cs 0.00% 5 Missing ⚠️
...ors.Abstractions/Template/TemplateHookAttribute.cs 0.00% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #104      +/-   ##
==========================================
- Coverage   82.61%   82.49%   -0.13%     
==========================================
  Files         169      175       +6     
  Lines       16150    16694     +544     
  Branches     2273     2367      +94     
==========================================
+ Hits        13343    13771     +428     
- Misses       2207     2296      +89     
- Partials      600      627      +27     
Flag Coverage Δ
unittests 82.49% <78.30%> (-0.13%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements an incremental source generator for the Template Method pattern that generates Execute/ExecuteAsync methods from declarative workflow definitions with ordered steps and lifecycle hooks.

Changes:

  • Adds Template Method pattern generator with support for sync/async workflows, deterministic step ordering, and lifecycle hooks (BeforeAll, AfterAll, OnError)
  • Introduces attribute-based API ([Template], [TemplateStep], [TemplateHook]) with configurable error handling policies
  • Includes comprehensive unit tests (16 tests), BDD-style integration tests, real-world examples, and complete documentation

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 17 comments.

Show a summary per file
File Description
src/PatternKit.Generators/TemplateGenerator.cs Core incremental generator implementation with 8 diagnostics, signature validation, and code generation for sync/async execution methods
src/PatternKit.Generators.Abstractions/Template/*.cs Attribute definitions for Template, TemplateStep, TemplateHook, HookPoint enum, and TemplateErrorPolicy enum
test/PatternKit.Generators.Tests/TemplateGeneratorTests.cs Comprehensive unit tests covering basic templates, hooks, async patterns, type targets, and diagnostics
test/PatternKit.Examples.Tests/TemplateMethodGeneratorDemo/*.cs BDD-style integration tests for ImportWorkflow and OrderProcessing examples
src/PatternKit.Examples/TemplateMethodGeneratorDemo/*.cs Real-world examples demonstrating data import pipeline and async order processing workflows
docs/generators/template-method-generator.md Complete documentation with quick start, real-world examples, configuration options, and migration guide
docs/generators/toc.yml Table of contents update to include Template Method generator documentation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@JerrettDavis
Copy link
Owner

@copilot apply changes based on the comments in this thread

…iagnostic, refactor loops, add tests

Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
Copilot AI requested a review from JerrettDavis January 31, 2026 01:32
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 582 to 642
sb.AppendLine(" try");
sb.AppendLine(" {");

// Steps
foreach (var step in sortedSteps)
{
var isAsync = IsNonGenericValueTask(step.Method.ReturnType);
if (isAsync)
{
var hasCt = step.Method.Parameters.Any(p => p.Type.Name == "CancellationToken");
var args = hasCt ? "ctx, ct" : "ctx";
sb.AppendLine($" await {step.Method.Name}({args}).ConfigureAwait(false);");
}
else
{
sb.AppendLine($" {step.Method.Name}(ctx);");
}
}

// AfterAll hooks (inside try - only execute on success)
foreach (var hook in afterAllHooks)
{
var isAsync = IsNonGenericValueTask(hook.Method.ReturnType);
if (isAsync)
{
var hasCt = hook.Method.Parameters.Any(p => p.Type.Name == "CancellationToken");
var args = hasCt ? "ctx, ct" : "ctx";
sb.AppendLine($" await {hook.Method.Name}({args}).ConfigureAwait(false);");
}
else
{
sb.AppendLine($" {hook.Method.Name}(ctx);");
}
}

sb.AppendLine(" }");
sb.AppendLine(" catch (System.Exception ex)");
sb.AppendLine(" {");

// OnError hooks
foreach (var hook in onErrorHooks)
{
var isAsync = IsNonGenericValueTask(hook.Method.ReturnType);
if (isAsync)
{
var hasCt = hook.Method.Parameters.Any(p => p.Type.Name == "CancellationToken");
var args = hasCt ? "ctx, ex, ct" : "ctx, ex";
sb.AppendLine($" await {hook.Method.Name}({args}).ConfigureAwait(false);");
}
else
{
sb.AppendLine($" {hook.Method.Name}(ctx, ex);");
}
}

if (config.ErrorPolicy == 0) // Rethrow
{
sb.AppendLine(" throw;");
}

sb.AppendLine(" }");
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states that HandleAndContinue policy "continues execution with next step" (template-method-generator.md:318), but the implementation only suppresses the exception rethrow without actually continuing to execute remaining steps. All steps are in a single try-catch block, so when an exception occurs, execution jumps to the catch block and then exits the method. Consider either: (1) wrapping each step in its own try-catch when HandleAndContinue is enabled to actually continue with remaining steps, or (2) updating the documentation to accurately reflect that HandleAndContinue only suppresses the exception without continuing execution.

Suggested change
sb.AppendLine(" try");
sb.AppendLine(" {");
// Steps
foreach (var step in sortedSteps)
{
var isAsync = IsNonGenericValueTask(step.Method.ReturnType);
if (isAsync)
{
var hasCt = step.Method.Parameters.Any(p => p.Type.Name == "CancellationToken");
var args = hasCt ? "ctx, ct" : "ctx";
sb.AppendLine($" await {step.Method.Name}({args}).ConfigureAwait(false);");
}
else
{
sb.AppendLine($" {step.Method.Name}(ctx);");
}
}
// AfterAll hooks (inside try - only execute on success)
foreach (var hook in afterAllHooks)
{
var isAsync = IsNonGenericValueTask(hook.Method.ReturnType);
if (isAsync)
{
var hasCt = hook.Method.Parameters.Any(p => p.Type.Name == "CancellationToken");
var args = hasCt ? "ctx, ct" : "ctx";
sb.AppendLine($" await {hook.Method.Name}({args}).ConfigureAwait(false);");
}
else
{
sb.AppendLine($" {hook.Method.Name}(ctx);");
}
}
sb.AppendLine(" }");
sb.AppendLine(" catch (System.Exception ex)");
sb.AppendLine(" {");
// OnError hooks
foreach (var hook in onErrorHooks)
{
var isAsync = IsNonGenericValueTask(hook.Method.ReturnType);
if (isAsync)
{
var hasCt = hook.Method.Parameters.Any(p => p.Type.Name == "CancellationToken");
var args = hasCt ? "ctx, ex, ct" : "ctx, ex";
sb.AppendLine($" await {hook.Method.Name}({args}).ConfigureAwait(false);");
}
else
{
sb.AppendLine($" {hook.Method.Name}(ctx, ex);");
}
}
if (config.ErrorPolicy == 0) // Rethrow
{
sb.AppendLine(" throw;");
}
sb.AppendLine(" }");
if (config.ErrorPolicy == 0) // Rethrow: single try-catch around all steps and AfterAll hooks
{
sb.AppendLine(" try");
sb.AppendLine(" {");
// Steps
foreach (var step in sortedSteps)
{
var isAsync = IsNonGenericValueTask(step.Method.ReturnType);
if (isAsync)
{
var hasCt = step.Method.Parameters.Any(p => p.Type.Name == "CancellationToken");
var args = hasCt ? "ctx, ct" : "ctx";
sb.AppendLine($" await {step.Method.Name}({args}).ConfigureAwait(false);");
}
else
{
sb.AppendLine($" {step.Method.Name}(ctx);");
}
}
// AfterAll hooks (inside try - only execute on success)
foreach (var hook in afterAllHooks)
{
var isAsync = IsNonGenericValueTask(hook.Method.ReturnType);
if (isAsync)
{
var hasCt = hook.Method.Parameters.Any(p => p.Type.Name == "CancellationToken");
var args = hasCt ? "ctx, ct" : "ctx";
sb.AppendLine($" await {hook.Method.Name}({args}).ConfigureAwait(false);");
}
else
{
sb.AppendLine($" {hook.Method.Name}(ctx);");
}
}
sb.AppendLine(" }");
sb.AppendLine(" catch (System.Exception ex)");
sb.AppendLine(" {");
// OnError hooks
foreach (var hook in onErrorHooks)
{
var isAsync = IsNonGenericValueTask(hook.Method.ReturnType);
if (isAsync)
{
var hasCt = hook.Method.Parameters.Any(p => p.Type.Name == "CancellationToken");
var args = hasCt ? "ctx, ex, ct" : "ctx, ex";
sb.AppendLine($" await {hook.Method.Name}({args}).ConfigureAwait(false);");
}
else
{
sb.AppendLine($" {hook.Method.Name}(ctx, ex);");
}
}
sb.AppendLine(" throw;");
sb.AppendLine(" }");
}
else
{
// Handle-and-continue: wrap each step in its own try-catch so remaining steps still execute
foreach (var step in sortedSteps)
{
var isAsync = IsNonGenericValueTask(step.Method.ReturnType);
if (isAsync)
{
var hasCt = step.Method.Parameters.Any(p => p.Type.Name == "CancellationToken");
var args = hasCt ? "ctx, ct" : "ctx";
sb.AppendLine(" try");
sb.AppendLine(" {");
sb.AppendLine($" await {step.Method.Name}({args}).ConfigureAwait(false);");
sb.AppendLine(" }");
sb.AppendLine(" catch (System.Exception ex)");
sb.AppendLine(" {");
// OnError hooks for this step
foreach (var hook in onErrorHooks)
{
var hookIsAsync = IsNonGenericValueTask(hook.Method.ReturnType);
if (hookIsAsync)
{
var hookHasCt = hook.Method.Parameters.Any(p => p.Type.Name == "CancellationToken");
var hookArgs = hookHasCt ? "ctx, ex, ct" : "ctx, ex";
sb.AppendLine($" await {hook.Method.Name}({hookArgs}).ConfigureAwait(false);");
}
else
{
sb.AppendLine($" {hook.Method.Name}(ctx, ex);");
}
}
// Do not rethrow: continue with next step
sb.AppendLine(" }");
}
else
{
sb.AppendLine(" try");
sb.AppendLine(" {");
sb.AppendLine($" {step.Method.Name}(ctx);");
sb.AppendLine(" }");
sb.AppendLine(" catch (System.Exception ex)");
sb.AppendLine(" {");
// OnError hooks for this step
foreach (var hook in onErrorHooks)
{
var hookIsAsync = IsNonGenericValueTask(hook.Method.ReturnType);
if (hookIsAsync)
{
var hookHasCt = hook.Method.Parameters.Any(p => p.Type.Name == "CancellationToken");
var hookArgs = hookHasCt ? "ctx, ex, ct" : "ctx, ex";
sb.AppendLine($" await {hook.Method.Name}({hookArgs}).ConfigureAwait(false);");
}
else
{
sb.AppendLine($" {hook.Method.Name}(ctx, ex);");
}
}
// Do not rethrow: continue with next step
sb.AppendLine(" }");
}
}
// AfterAll hooks (execute after all steps, even if some failed but were handled)
foreach (var hook in afterAllHooks)
{
var isAsync = IsNonGenericValueTask(hook.Method.ReturnType);
if (isAsync)
{
var hasCt = hook.Method.Parameters.Any(p => p.Type.Name == "CancellationToken");
var args = hasCt ? "ctx, ct" : "ctx";
sb.AppendLine($" await {hook.Method.Name}({args}).ConfigureAwait(false);");
}
else
{
sb.AppendLine($" {hook.Method.Name}(ctx);");
}
}
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated documentation to accurately reflect current implementation. HandleAndContinue suppresses the exception (doesn't rethrow) but doesn't continue execution - the workflow terminates after the catch block. Documentation now states "Exception is suppressed (not rethrown), Workflow terminates" instead of "Execution continues with next step". Fixed in 80c6370

private void OnError(ImportContext ctx, Exception ex)
{
ctx.Log.Add($"[{DateTime.UtcNow:HH:mm:ss}] ERROR: {ex.Message}");
ctx.Log.Add($"[{DateTime.UtcNow:HH:mm:ss}] Import failed at validation stage");
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message hardcodes "Import failed at validation stage" but this OnError hook can be invoked when any step fails, not just the validation step. Consider making this message more generic (e.g., "Import failed") or inspecting the exception/context to determine which step failed.

Suggested change
ctx.Log.Add($"[{DateTime.UtcNow:HH:mm:ss}] Import failed at validation stage");
ctx.Log.Add($"[{DateTime.UtcNow:HH:mm:ss}] Import failed");

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed error message from hardcoded "Import failed at validation stage" to generic "Import failed" since OnError hook can be invoked when any step fails. Fixed in 80c6370

Comment on lines 316 to 333
**HandleAndContinue:**
- OnError hook is invoked
- Execution continues with next step
- Only allowed when remaining steps are all optional

```csharp
[Template(ErrorPolicy = TemplateErrorPolicy.HandleAndContinue)]
public partial class ResilientWorkflow
{
[TemplateStep(0)]
private void Step1(Context ctx) { }

[TemplateStep(1, Optional = true)]
private void Step2(Context ctx) { } // Must be optional

[TemplateStep(2, Optional = true)]
private void Step3(Context ctx) { } // Must be optional
}
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states "Only allowed when remaining steps are all optional" but the example shows Step1 (line 326) is not optional. The actual implementation in TemplateGenerator.cs (line 437) validates that ALL steps must be optional, not just "remaining" ones. Either the documentation should be updated to say "Only allowed when all steps are optional" or the validation logic needs to be changed to only check remaining steps after an error occurs. The current example would fail validation with PKTMP008.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated documentation to match implementation. Changed "remaining steps are all optional" to "all steps are optional" and updated example to mark Step1 as optional. The validation logic requires ALL steps to be optional, not just remaining ones. Fixed in 80c6370

@JerrettDavis
Copy link
Owner

@copilot apply changes based on the comments in this thread

… update docs, refactor validation loops

Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generator: Create Template Method Pattern

2 participants