Skip to content

Instantly share code, notes, and snippets.

@Seekatar
Last active July 24, 2019 19:39
Show Gist options
  • Save Seekatar/5bad3a0779679564c48eb6485a33abac to your computer and use it in GitHub Desktop.
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.
<#
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
}
}
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
}
}
{
"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