Last active
July 24, 2019 19:39
-
-
Save Seekatar/5bad3a0779679564c48eb6485a33abac to your computer and use it in GitHub Desktop.
Traverse all the properties of a PowerShell object with a visitor scriptblock. The process.json is used as input to the Pester tests.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
Class for passing into Edit-Object | |
#> | |
class EditObjectVisitor | |
{ | |
<# | |
Called by Edit-Object on each member. | |
$Obj the current object visited | |
$Member the current member visited | |
$Path the path to the current member, e.g. obj.array[2].propA | |
#> | |
[void] Visit([object]$Obj, [Microsoft.PowerShell.Commands.MemberDefinition]$Member, [string]$Path) | |
{ | |
} | |
} | |
<# | |
.SYNOPSIS | |
Edit or view all properties of an object | |
.DESCRIPTION | |
Visit every property of an object recursively and call a ScriptBlock or method on a class, if matched | |
.PARAMETER Obj | |
The object to scan | |
.PARAMETER PropertyNames | |
List of names of properties to look for | |
.PARAMETER Visitor | |
EditObjectVisitor-derived class to process the property | |
.PARAMETER VisitorSb | |
A scriptblock that takes ([object]$Obj, [Microsoft.PowerShell.Commands.MemberDefinition]$Member, [object]$SbParam, [string]$Path) | |
.PARAMETER SbParam | |
Parameter passed into VisitorSb, may be null if not needed | |
.PARAMETER PassThru | |
True to emit base-level properties | |
.PARAMETER Name | |
Base object name for verbose logging, and first level of $Path in callbacks, defaults to "obj" | |
.PARAMETER MaxDepth | |
Max depth to stop processing. Currently there is no circular graph detection aside from this. Defaults to 20. | |
.PARAMETER depth | |
Used recursively don't pass in | |
.EXAMPLE | |
$items = new-object "System.Collections.Generic.List[object]" | |
Edit-Object -obj $process -PropertyNames "Id","Name" -VisitorSb { | |
param([object]$Obj, [Microsoft.PowerShell.Commands.MemberDefinition]$Member, [object]$SbParam, [string]$Path) | |
$SbParam.Add($Obj.($Member.Name)) | |
} -SbParam $items | |
$items | |
Use a ScriptBlock to find properties named Id or Name and puts them into an array | |
.EXAMPLE | |
Edit-Object -obj $process -PropertyNames "Id" -VisitorSb { | |
param([object]$Obj, [Microsoft.PowerShell.Commands.MemberDefinition]$Member, [object]$SbParam, [string]$Path) | |
$Obj.PSObject.Properties.Remove("ID") | |
} -SbParam $items | |
Use a ScriptBlock to remove all the ID properties from the object | |
.NOTES | |
See the Pester test file for full examples | |
#> | |
function Edit-Object | |
{ | |
[CmdletBinding()] | |
param( | |
[Parameter(Mandatory,ValueFromPipeline)] | |
[object] $Obj, | |
[ValidateCount(1,100)] | |
[string[]] $PropertyNames, | |
[Parameter(Mandatory,ParameterSetName="ScriptBlock")] | |
[ScriptBlock] $VisitorSb, | |
[Parameter(ParameterSetName="ScriptBlock")] | |
[object] $SbParam, | |
[Parameter(Mandatory,ParameterSetName="Object")] | |
# [EditObjectVisitor]$Visitor, Pester doesn't like this, says can't cast, but this works outside of Pester | |
$Visitor, | |
[switch] $PassThru, | |
[int] $depth = 0, | |
[string] $Name = "obj", | |
[int] $MaxDepth = 20 | |
) | |
Set-StrictMode -Version Latest | |
$parms = $PSBoundParameters | |
$null = $parms.Remove("Obj") | |
$null = $parms.Remove("depth") | |
$null = $parms.Remove("Name") | |
if ( $depth -gt $MaxDepth ) | |
{ | |
throw "Depth is $depth -- probably circular reference, not supported now" | |
} | |
if ( $Obj -is 'Array') | |
{ | |
$i = 0 | |
foreach ( $o in $Obj ) | |
{ | |
Edit-Object -Obj $Obj[$i] -depth ($depth+1) -name "$Name[$i]" @parms | |
$i += 1 | |
} | |
return | |
} | |
foreach ( $m in Get-Member -InputObject $Obj -MemberType NoteProperty) | |
{ | |
if ( $Obj.$($m.Name) -is 'PSCustomObject' ) | |
{ | |
Edit-Object -Obj $Obj.$($m.Name) -depth ($depth+1) -name "$Name.$($m.Name)" @parms | |
} | |
elseif ( $Obj.$($m.Name) -is 'Array' ) | |
{ | |
Edit-Object -Obj $Obj.$($m.Name) -depth ($depth+1) -name "$Name.$($m.Name)" @parms | |
} | |
if ( (-not $PropertyNames) -or $m.Name -in $PropertyNames) | |
{ | |
Write-Verbose "Found $Name.$($m.Name)" | |
if ($VisitorSb) | |
{ | |
Invoke-Command $VisitorSb -ArgumentList $Obj,$m,$SbParam,$Name | |
} | |
else | |
{ | |
$Visitor.Visit($Obj,$m,$Name) | |
} | |
} | |
} | |
if ( $depth -eq 0 -and $PassThru) | |
{ | |
$Obj | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Describe "Test Edit-Object gist" { | |
$process = ConvertFrom-Json (Get-Content (Join-path $PSScriptRoot "process.json") -Raw) | |
It "Tests VisitorSb" { | |
$items = new-object "System.Collections.Generic.List[object]" | |
Edit-Object -obj $process -PropertyNames "ProjectId","Name" -VisitorSb { | |
param([object]$Obj, [Microsoft.PowerShell.Commands.MemberDefinition]$Member, [object]$SbParam, [string]$Path) | |
$SbParam.Add($Obj.($Member.Name)) | |
} -SbParam $items | |
$items.Count | Should -Be 11 | |
} | |
It "Tests VisitorSb Removing" { | |
Get-Member -InputObject $process -Name "ProjectId" | Should -not -BeNullOrEmpty | |
Edit-Object -obj $process -PropertyNames "ProjectId" -VisitorSb { | |
param([object]$Obj, [Microsoft.PowerShell.Commands.MemberDefinition]$Member, [object]$SbParam, [string]$Path) | |
$Obj.PSObject.Properties.Remove("ProjectId") | |
} | |
Get-Member -InputObject $process -Name "ProjectId" | Should -BeNullOrEmpty | |
} | |
It "Tests VisitorClass" { | |
$process = ConvertFrom-Json (Get-Content (Join-path $PSScriptRoot "process.json") -Raw) | |
# if get error about finding EditObjectVisitor, you must dot-source Edit-Object.ps1 first | |
# see this Pester issue https://github.com/pester/Pester/issues/1054 | |
class MyVisitor : EditObjectVisitor | |
{ | |
MyVisitor() | |
{ | |
$this.Items = new-object "System.Collections.Generic.List[object]" | |
} | |
[void] Visit([object]$Obj, [Microsoft.PowerShell.Commands.MemberDefinition]$Member, [string]$Path) | |
{ | |
if ($Member.Name -eq "Name") | |
{ | |
$this.Items.Add($Obj.($Member.Name)) | |
} | |
else | |
{ | |
$this.ProjectId = $Obj.($Member.Name) | |
} | |
} | |
$Items | |
[string] $ProjectId | |
} | |
$visitor = New-Object "MyVisitor" | |
Edit-Object -obj $process -PropertyNames "ProjectId","Name" -Visitor $visitor | |
$visitor.Items.Count | Should -Be 10 | |
$visitor.ProjectId | Should -Be $process.ProjectId | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"ProjectId": "Projects-263", | |
"Steps": [ | |
{ | |
"Name": "Human Approval", | |
"PackageRequirement": "LetOctopusDecide", | |
"Properties": { | |
}, | |
"Condition": "Success", | |
"StartTrigger": "StartAfterPrevious", | |
"Actions": [ | |
{ | |
"Name": "Human Approval", | |
"ActionType": "Octopus.Manual", | |
"IsDisabled": false, | |
"CanBeUsedForProjectVersioning": false, | |
"IsRequired": false, | |
"WorkerPoolId": null, | |
"Environments": [ | |
], | |
"ExcludedEnvironments": [ | |
"Environments-41", | |
"Environments-42", | |
"Environments-43" | |
], | |
"Channels": [ | |
], | |
"TenantTags": [ | |
], | |
"Packages": [ | |
], | |
"Properties": { | |
"Octopus.Action.Manual.ResponsibleTeamIds": "Teams-21", | |
"Octopus.Action.Manual.Instructions": "Please approve before deploying to production." | |
}, | |
"Links": { | |
} | |
} | |
] | |
}, | |
{ | |
"Name": "Application Insights - Annotate Release", | |
"PackageRequirement": "LetOctopusDecide", | |
"Properties": { | |
}, | |
"Condition": "Success", | |
"StartTrigger": "StartAfterPrevious", | |
"Actions": [ | |
{ | |
"Name": "Application Insights - Annotate Release", | |
"ActionType": "Octopus.Script", | |
"IsDisabled": false, | |
"CanBeUsedForProjectVersioning": false, | |
"IsRequired": false, | |
"WorkerPoolId": null, | |
"Environments": [ | |
], | |
"ExcludedEnvironments": [ | |
], | |
"Channels": [ | |
], | |
"TenantTags": [ | |
], | |
"Packages": [ | |
], | |
"Properties": { | |
"Octopus.Action.Script.Syntax": "PowerShell", | |
"Octopus.Action.Script.ScriptBody": "", | |
"Octopus.Action.Template.Id": "ActionTemplates-42", | |
"Octopus.Action.Template.Version": "6", | |
"ReleaseName": "#{Octopus.Release.Number}", | |
"Octopus.Action.RunOnServer": "true", | |
"ApplicationId": "#{AppInsightsApplicationID}", | |
"ApiKey": "#{AppInsightsApiKey}" | |
}, | |
"Links": { | |
} | |
} | |
] | |
}, | |
{ | |
"Name": "Database Migration", | |
"PackageRequirement": "LetOctopusDecide", | |
"Properties": { | |
"Octopus.Action.TargetRoles": "database-server" | |
}, | |
"Condition": "Success", | |
"StartTrigger": "StartAfterPrevious", | |
"Actions": [ | |
{ | |
"Name": "Database Migration", | |
"ActionType": "Octopus.TentaclePackage", | |
"IsDisabled": false, | |
"CanBeUsedForProjectVersioning": true, | |
"IsRequired": false, | |
"WorkerPoolId": null, | |
"Environments": [ | |
], | |
"ExcludedEnvironments": [ | |
], | |
"Channels": [ | |
], | |
"TenantTags": [ | |
], | |
"Packages": [ | |
{ | |
"Name": "", | |
"PackageId": "OnionDevOpsArchitecture.Database", | |
"FeedId": "Feeds-41", | |
"AcquisitionLocation": "ExecutionTarget", | |
"Properties": { | |
} | |
} | |
], | |
"Properties": { | |
"Octopus.Action.EnabledFeatures": "Octopus.Features.CustomScripts", | |
"Octopus.Action.Package.PackageId": "OnionDevOpsArchitecture.Database", | |
"Octopus.Action.Package.FeedId": "Feeds-41", | |
"Octopus.Action.Package.DownloadOnTentacle": "True", | |
"Octopus.Action.CustomScripts.PostDeploy.ps1": "" | |
}, | |
"Links": { | |
} | |
} | |
] | |
}, | |
{ | |
"Name": "Deploy UI", | |
"PackageRequirement": "LetOctopusDecide", | |
"Properties": { | |
"Octopus.Action.TargetRoles": "web-server" | |
}, | |
"Condition": "Success", | |
"StartTrigger": "StartAfterPrevious", | |
"Actions": [ | |
{ | |
"Name": "Deploy UI", | |
"ActionType": "Octopus.IIS", | |
"IsDisabled": false, | |
"CanBeUsedForProjectVersioning": true, | |
"IsRequired": false, | |
"WorkerPoolId": null, | |
"Environments": [ | |
], | |
"ExcludedEnvironments": [ | |
], | |
"Channels": [ | |
], | |
"TenantTags": [ | |
], | |
"Packages": [ | |
{ | |
"Name": "", | |
"PackageId": "OnionDevOpsArchitecture.UI", | |
"FeedId": "Feeds-41", | |
"AcquisitionLocation": "ExecutionTarget", | |
"Properties": { | |
} | |
} | |
], | |
"Properties": { | |
"Octopus.Action.IISWebSite.ApplicationPoolPassword": { | |
"HasValue": true, | |
"NewValue": null | |
}, | |
"Octopus.Action.IISWebSite.DeploymentType": "webSite", | |
"Octopus.Action.IISWebSite.CreateOrUpdateWebSite": "True", | |
"Octopus.Action.Package.FeedId": "Feeds-41", | |
"Octopus.Action.Package.DownloadOnTentacle": "True", | |
"Octopus.Action.IISWebSite.ApplicationPoolUsername": "buildadmin", | |
"Octopus.Action.Package.JsonConfigurationVariablesEnabled": "True", | |
"Octopus.Action.Package.JsonConfigurationVariablesTargets": "appsettings.json", | |
"Octopus.Action.SubstituteInFiles.Enabled": "True", | |
"Octopus.Action.SubstituteInFiles.TargetFiles": "wwwroot\\css\\site.css" | |
}, | |
"Links": { | |
} | |
} | |
] | |
} | |
], | |
"Version": 33, | |
"LastSnapshotId": null, | |
"SpaceId": "Spaces-3", | |
"Links": { | |
"Self": "/api/Spaces-3/deploymentprocesses/deploymentprocess-Projects-202", | |
"Project": "/api/Spaces-3/projects/Projects-202", | |
"Template": "/api/Spaces-3/deploymentprocesses/deploymentprocess-Projects-202/template{?channel,releaseId}" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment