Skip to content

Commit eace817

Browse files
committed
WIP: Tests to show it works and how to use it
1 parent be8a9ae commit eace817

18 files changed

Lines changed: 357 additions & 136 deletions

Source/Classes/10. ParameterPosition.ps1

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
class TextReplace {
1+
class TextReplacement {
22
[int]$StartOffset = 0
33
[int]$EndOffset = 0
44
[string]$Text = ''
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using namespace System.Management.Automation.Language
2+
using namespace System.Collections.Generic
3+
4+
class ParameterExtractor : AstVisitor {
5+
[System.Collections.Specialized.OrderedDictionary]$Parameters = @{}
6+
[int]$InsertLineNumber = -1
7+
[int]$InsertColumnNumber = -1
8+
[int]$InsertOffset = -1
9+
10+
ParameterExtractor([Ast]$Ast) {
11+
$ast.Visit($this)
12+
}
13+
14+
[AstVisitAction] VisitParamBlock([ParamBlockAst]$ast) {
15+
if ($Ast.Parameters) {
16+
$Text = $ast.Extent.Text -split "\r?\n"
17+
18+
$FirstLine = $ast.Extent.StartLineNumber
19+
$NextLine = 1
20+
foreach ($parameter in $ast.Parameters | Select-Object Name -Expand Extent) {
21+
$this.Parameters.Add($parameter.Name, ([TextReplacement]@{
22+
StartOffset = $parameter.StartOffset
23+
Text = if (($parameter.StartLineNumber - $FirstLine) -ge $NextLine) {
24+
Write-Debug "Extracted parameter $($Parameter.Name) with surrounding lines"
25+
# Take lines after the last parameter
26+
$Lines = @($Text[$NextLine..($parameter.EndLineNumber - $FirstLine)].Where{ ![string]::IsNullOrWhiteSpace($_) })
27+
# If the last line extends past the end of the parameter, trim that line
28+
if ($Lines.Length -gt 0 -and $parameter.EndColumnNumber -lt $Lines[-1].Length) {
29+
$Lines[-1] = $Lines[-1].SubString($parameter.EndColumnNumber)
30+
}
31+
# Don't return the commas, we'll add them back later
32+
($Lines -join "`n").TrimEnd(",")
33+
} else {
34+
Write-Debug "Extracted parameter $($Parameter.Name) text exactly"
35+
$parameter.Text.TrimEnd(",")
36+
}
37+
}))
38+
$NextLine = 1 + $parameter.EndLineNumber - $FirstLine
39+
}
40+
41+
$this.InsertLineNumber = $ast.Parameters[-1].Extent.EndLineNumber
42+
$this.InsertColumnNumber = $ast.Parameters[-1].Extent.EndColumnNumber
43+
$this.InsertOffset = $ast.Parameters[-1].Extent.EndOffset
44+
} else {
45+
$this.InsertLineNumber = $ast.Extent.EndLineNumber
46+
$this.InsertColumnNumber = $ast.Extent.EndColumnNumber - 1
47+
$this.InsertOffset = $ast.Extent.EndOffset - 1
48+
}
49+
return [AstVisitAction]::StopVisit
50+
}
51+
}

Source/Classes/21. ParameterExtractor.ps1

Lines changed: 0 additions & 60 deletions
This file was deleted.

Source/Classes/20. ModuleBuilderGenerator.ps1 renamed to Source/Classes/30. ModuleBuilderGenerator.ps1

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,19 @@
11
using namespace System.Management.Automation.Language
22
using namespace System.Collections.Generic
3-
class TextReplace {
4-
[int]$StartOffset = 0
5-
[int]$EndOffset = 0
6-
[string]$Text = ''
7-
}
83

94
class ModuleBuilderGenerator {
10-
hidden [List[TextReplace]]$Replacements = @()
5+
hidden [List[TextReplacement]]$Replacements = @()
116

127
[void] Replace($StartOffset, $EndOffset, $Text) {
13-
$this.Replacements.Add([TextReplace]@{
8+
$this.Replacements.Add([TextReplacement]@{
149
StartOffset = $StartOffset
1510
EndOffset = $EndOffset
1611
Text = $Text
1712
})
1813
}
1914

2015
[void] Insert($StartOffset, $Text) {
21-
$this.Replacements.Add([TextReplace]@{
16+
$this.Replacements.Add([TextReplacement]@{
2217
StartOffset = $StartOffset
2318
EndOffset = $StartOffset
2419
Text = $Text
@@ -43,7 +38,7 @@ class ModuleBuilderGenerator {
4338

4439
$Additional = $AdditionalParameters.Parameters.Where{ $_.Name -notin $ExistingParameters.Parameters.Name }
4540
if (($Text = $Additional.Text -join ",`n`n")) {
46-
$Replacement = [TextReplace]@{
41+
$Replacement = [TextReplacement]@{
4742
StartOffset = $ExistingParameters.InsertOffset
4843
EndOffset = $ExistingParameters.InsertOffset
4944
Text = if ($ExistingParameters.Parameters.Count -gt 0) {
@@ -60,7 +55,7 @@ class ModuleBuilderGenerator {
6055

6156

6257

63-
[List[TextReplace]]Generate([Ast]$ast) {
58+
[List[TextReplacement]]Generate([Ast]$ast) {
6459
$ast.Visit($this)
6560
return $this.Replacements
6661
}
Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,26 @@ class ParameterGenerator : ModuleBuilderGenerator {
55
[System.Management.Automation.HiddenAttribute()]
66
[ParameterExtractor]$AdditionalParameterCache
77

8-
[ParameterExtractor]GetAdditional() {
8+
ParameterGenerator($Path) : base($Path) {}
9+
10+
[System.Collections.Generic.Dictionary[string, TextReplacement]]GetAdditional() {
911
if (!$this.AdditionalParameterCache) {
10-
$this.AdditionalParameterCache = $this.Aspect
12+
$this.AdditionalParameterCache = [ParameterExtractor]$this.Ast
1113
}
12-
return $this.AdditionalParameterCache
14+
return $this.AdditionalParameterCache.Parameters
1315
}
1416

1517
[AstVisitAction] VisitFunctionDefinition([FunctionDefinitionAst]$ast) {
1618
if (!$ast.Where($this.Where)) {
1719
return [AstVisitAction]::SkipChildren
1820
}
21+
1922
$Existing = [ParameterExtractor]$ast
20-
$Additional = $this.GetAdditional().Parameters.Where{ $_.Name -notin $Existing.Parameters.Name }
21-
if (($Text = $Additional.Text -join ",`n`n")) {
22-
$Replacement = [TextReplace]@{
23+
24+
$AdditionalParameters = $this.GetAdditional()
25+
$Additional = $AdditionalParameters.Keys.Where{ $_.Name -notin $Existing.Parameters.Name }
26+
if (($Text = $AdditionalParameters.Values.Text -join ",`n`n")) {
27+
$Replacement = [TextReplacement]@{
2328
StartOffset = $Existing.InsertOffset
2429
EndOffset = $Existing.InsertOffset
2530
Text = if ($Existing.Parameters.Count -gt 0) {
@@ -29,7 +34,7 @@ class ParameterGenerator : ModuleBuilderGenerator {
2934
}
3035
}
3136

32-
Write-Debug "Adding parameters to $($ast.name): $($Additional.Name -join ', ')"
37+
Write-Debug "Adding parameters to $($ast.name): $($Additional.Keys -join ', ')"
3338
$this.Replacements.Add($Replacement)
3439
}
3540
return [AstVisitAction]::SkipChildren
Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,23 @@ class BlockGenerator : ModuleBuilderGenerator {
1111
[System.Management.Automation.HiddenAttribute()]
1212
[NamedBlockAst]$EndBlockTemplate
1313

14-
[List[TextReplace]]Generate([Ast]$ast) {
15-
if (!($this.BeginBlockTemplate = $this.Aspect.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "Begin" }, $false))) {
16-
Write-Debug "No Aspect for BeginBlock"
14+
BlockGenerator($Path) : base($Path) {}
15+
16+
[List[TextReplacement]]Generate([Ast]$ast) {
17+
if (!($this.BeginBlockTemplate = $this.Ast.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "Begin" }, $false))) {
18+
Write-Debug "No Generator for BeginBlock"
1719
} else {
18-
Write-Debug "BeginBlock Aspect: $($this.BeginBlockTemplate)"
20+
Write-Debug "BeginBlock Generator: $($this.BeginBlockTemplate)"
1921
}
20-
if (!($this.ProcessBlockTemplate = $this.Aspect.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "Process" }, $false))) {
21-
Write-Debug "No Aspect for ProcessBlock"
22+
if (!($this.ProcessBlockTemplate = $this.Ast.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "Process" }, $false))) {
23+
Write-Debug "No Generator for ProcessBlock"
2224
} else {
23-
Write-Debug "ProcessBlock Aspect: $($this.ProcessBlockTemplate)"
25+
Write-Debug "ProcessBlock Generator: $($this.ProcessBlockTemplate)"
2426
}
25-
if (!($this.EndBlockTemplate = $this.Aspect.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "End" }, $false))) {
26-
Write-Debug "No Aspect for EndBlock"
27+
if (!($this.EndBlockTemplate = $this.Ast.Find({ $args[0] -is [NamedBlockAst] -and $args[0].BlockKind -eq "End" }, $false))) {
28+
Write-Debug "No Generator for EndBlock"
2729
} else {
28-
Write-Debug "EndBlock Aspect: $($this.EndBlockTemplate)"
30+
Write-Debug "EndBlock Generator: $($this.EndBlockTemplate)"
2931
}
3032

3133
$ast.Visit($this)
@@ -44,7 +46,7 @@ class BlockGenerator : ModuleBuilderGenerator {
4446
$BeginBlockText = ($BeginExtent.Text -replace "^begin[\s\r\n]*{|}[\s\r\n]*$", "`n").Trim("`r`n").TrimEnd("`r`n ")
4547

4648

47-
$Replacement = [TextReplace]@{
49+
$Replacement = [TextReplacement]@{
4850
StartOffset = $BeginExtent.StartOffset
4951
EndOffset = $BeginExtent.EndOffset
5052
Text = $this.BeginBlockTemplate.Extent.Text.Replace("existingcode", $BeginBlockText)
@@ -73,7 +75,7 @@ class BlockGenerator : ModuleBuilderGenerator {
7375
$StartOffset = $ProcessBlockExtent.StartOffset
7476
}
7577

76-
$Replacement = [TextReplace]@{
78+
$Replacement = [TextReplacement]@{
7779
StartOffset = $StartOffset
7880
EndOffset = $ProcessBlockExtent.EndOffset
7981
Text = $this.ProcessBlockTemplate.Extent.Text.Replace("existingcode", $ProcessBlockText)
@@ -103,7 +105,7 @@ class BlockGenerator : ModuleBuilderGenerator {
103105
$EndBlockText = ($EndBlockExtent.Text -replace "^end[\s\r\n]*{|}[\s\r\n]*$", "`n").Trim("`r`n").TrimEnd("`r`n ")
104106
}
105107

106-
$Replacement = [TextReplace]@{
108+
$Replacement = [TextReplacement]@{
107109
StartOffset = $StartOffset
108110
EndOffset = $EndBlockExtent.EndOffset
109111
Text = $this.EndBlockTemplate.Extent.Text.Replace("existingcode", $EndBlockText)

Source/ModuleBuilder.psd1

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111

1212
# Release Notes have to be here, so we can update them
1313
ReleaseNotes = '
14-
Add support for Aspect Oriented Programming (AOP) with the new `Aspects` parameter.
14+
Add support for Generators, so you can weave in solutions for cross-cutting concerns at "build" time.
15+
Specifically targeting support for module-wide common parameters, standardized fall-back error handling, etc.
1516
'
1617

1718
# Tags applied to this module. These help with module discovery in online galleries.

Source/Private/GetBuildInfo.ps1

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,9 @@ function GetBuildInfo {
111111
}
112112
}
113113

114-
# Make sure Aspects is an array of objects (instead of hashtables)
115-
if ($BuildInfo.Aspects) {
116-
$BuildInfo.Aspects = $BuildInfo.Aspects | ForEach-Object {
114+
# Make sure Generators is an array of objects (instead of hashtables)
115+
if ($BuildInfo.Generators) {
116+
$BuildInfo.Generators = $BuildInfo.Generators | ForEach-Object {
117117
if ($_ -is [hashtable]) {
118118
[PSCustomObject]$_
119119
} else {

Source/Private/InvokeGenerator.ps1

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,53 +5,54 @@ function InvokeGenerator {
55
.DESCRIPTION
66
This is an aspect-oriented programming approach for adding cross-cutting features to functions in a module.
77
8-
The [ModuleBuilderGenerator] implementations are [AstVisitors] that return [TextReplace] object representing modifications to be performed on the source.
8+
The [ModuleBuilderGenerator] implementations are [AstVisitors] that return [TextReplacement] objects representing modifications to be performed on the source.
99
#>
10+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Function', Justification = 'PSSA reads the AST wrong')]
1011
[CmdletBinding()]
1112
param(
12-
# The path to the RootModule psm1 to merge the aspect into
13+
# The path to the RootModule psm1 to apply the Generator to
1314
[Parameter(Mandatory, Position = 0)]
1415
[string]$RootModule,
1516

1617
# The name of the ModuleBuilder Generator to invoke.
17-
# There are two built in:
18-
# - MergeBlocks. Supports Before/After/Around blocks for aspects like error handling or authentication.
19-
# - AddParameter. Supports adding common parameters to functions (usually in conjunction with MergeBlock that use those parameters)
18+
# There are only two Generators built in:
19+
# - ParameterGenerator. Supports adding parameters to functions in the module. Parameters come from a template file, which must be a script with a param block.
20+
# - BlockGenerator. Supports adding code before, after, and around existing blocks for generators like error handling, authentication, and implementations for common parameters. The added blocks come from a template file, which must be a script with named begin/process/end blocks.
2021
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
21-
[ValidateScript({ (($_ -As [Type]), ("${_}Aspect" -As [Type])).BaseType -eq [ModuleBuilderGenerator] })]
22-
[string]$Action,
22+
[ValidateScript({ (($_ -As [Type]), ("${_}Generator" -As [Type])).BaseType -eq [ModuleBuilderGenerator] })]
23+
[string]$Generator,
2324

24-
# The name(s) of functions in the module to run the generator against. Supports wildcards.
25+
# The name(s) of functions in the RootModule to run the generator against. Supports wildcards.
2526
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
2627
[string[]]$Function,
2728

28-
# The name of the script path or function that contains the base which drives the generator
29+
# The name of a template file that customizes the generator
2930
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
30-
[string]$Source
31+
[string]$Template
3132
)
3233
process {
3334
#! We can't reuse the AST because it needs to be updated after we change it
3435
#! But we can handle this in a wrapper
35-
Write-Verbose "Parsing $RootModule for $Action with $Source"
36+
Write-Verbose "Parsing $RootModule for $Generator with $Template"
3637
$Ast = ConvertToAst $RootModule
3738

38-
$Action = if ($Action -As [Type]) {
39-
$Action
40-
} elseif ("${Action}Aspect" -As [Type]) {
41-
"${Action}Aspect"
39+
$Generator = if ($Generator -As [Type]) {
40+
$Generator
41+
} elseif ("${Generator}Generator" -As [Type]) {
42+
"${Generator}Generator"
4243
} else {
43-
throw "Can't find $Action ModuleBuilderGenerator"
44+
throw "Can't find $Generator ModuleBuilderGenerator"
4445
}
4546

46-
$Aspect = New-Object $Action -Property @{
47+
$Generator = New-Object $Generator -Property @{
4748
Where = { $Func = $_; $Function.ForEach({ $Func.Name -like $_ }) -contains $true }.GetNewClosure()
48-
Aspect = @(Get-Command (Join-Path $AspectDirectory $Source), $Source -ErrorAction Ignore)[0].ScriptBlock.Ast
49+
Generator = @(Get-Command (Join-Path $GeneratorDirectory $Template), $Template -ErrorAction Ignore)[0].ScriptBlock.Ast
4950
}
5051

5152
#! Process replacements from the bottom up, so the line numbers work
5253
$Content = Get-Content $RootModule -Raw
53-
Write-Verbose "Generating $Action in $RootModule"
54-
foreach ($replacement in $Aspect.Generate($Ast.Ast) | Sort-Object StartOffset -Descending) {
54+
Write-Verbose "Generating $Generator in $RootModule"
55+
foreach ($replacement in $Generator.Generate($Ast.Ast) | Sort-Object StartOffset -Descending) {
5556
$Content = $Content.Remove($replacement.StartOffset, ($replacement.EndOffset - $replacement.StartOffset)).Insert($replacement.StartOffset, $replacement.Text)
5657
}
5758
Set-Content $RootModule $Content

0 commit comments

Comments
 (0)