Public/Get-HDTConsoleDetectionForm.ps1
|
function Get-HDTConsoleDetectionForm { <# .SYNOPSIS The detection rule as a form: which type, which boxes, what is in them, and whether it can be written. .DESCRIPTION A DETECTION RULE IS A TYPE AND THE KEYS THAT TYPE TAKES, and which keys those are is the type's decision - msiProduct wants a product code, registry wants a key and optionally a value and its data. Typing that as YAML into a box means knowing the key list and the indentation, and getting either wrong is a document the validator refuses after the fact. SO THE TYPE IS CHOSEN AND THE BOXES FOLLOW FROM IT. This says which boxes, what is already in them, and whether what is in them is a rule that can be written. THE KEY LIST IS Get-HDTApplicationDetectKey'S, which the validator and the projector already read. A second list here would be a second place for the window and the engine to disagree about what a registry rule is - and the window would be the one that was wrong. NO RULE IS ONE OF THE CHOICES, not the absence of one. DESIGN 8 says an application that declares no detection installs every time, which is a decision somebody makes deliberately for a package that has no way to be detected. CHANGING THE TYPE EMPTIES THE BOXES. A product code is not a registry key, and carrying one into the other's box writes a rule that parses and detects nothing. AN EMPTY OPTIONAL BOX IS LEFT OUT OF THE DOCUMENT. An absent key and a blank one are different things: the validator accepts the absence, and the engine would compare against the blank. .PARAMETER Type The type to build the form for. Omitted, the type the rule already declares - or none. .PARAMETER Detect The rule as the document holds it, or nothing. .INPUTS None. This command does not accept pipeline input. .OUTPUTS System.Management.Automation.PSCustomObject Type the type the form is showing Choice every type, with the words a technician reads Field one row per box: Key, Label, Value, Required, Hint Complete the boxes hold a rule that can be written Message which box is missing, when one is Rule what to pass to Set-HDTApplication -Detect, or $null CommandText that same rule as it would be TYPED, so the footer shows a line somebody could run rather than a quoted blob - a command that would not run teaches nothing .EXAMPLE Get-HDTConsoleDetectionForm -Detect $application.Detect .EXAMPLE Get-HDTConsoleDetectionForm -Type 'registry' -Detect $current The form after somebody changed the type in the dropdown. .LINK Set-HDTApplication .LINK Test-HDTApplicationDetection #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Position = 0)] [AllowEmptyString()] [string] $Type, [Parameter(Position = 1)] [AllowNull()] [object] $Detect = $null ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $schema = Get-HDTApplicationDetectKey # WHAT EACH TYPE AND EACH KEY IS, IN WORDS. The key list is the engine's; # this is only what to call it on screen, which is the one thing the engine # has no opinion about. $said = @{ msiProduct = 'An MSI product code is registered' file = 'A file is there' registry = 'A registry key is there' script = 'A script says so' } $label = @{ productCode = 'Product code' path = 'Path' version = 'Version' key = 'Registry key' value = 'Value name' data = 'Value data' } $hint = @{ productCode = "The GUID the .msi registers itself under, braces and all - Get-HDTApplicationDetection reads it off an installed machine, or the .msi's ProductCode property has it." path = 'A path on the deployed machine, not on the share. Environment variables are expanded when it is checked.' version = 'Optional. The file version this counts as installed at or above; omitted, the file merely being there is enough.' key = 'A registry key on the deployed machine, for example HKLM:\SOFTWARE\Contoso\Suite.' value = 'Optional. A value under that key; omitted, the key existing is enough.' data = 'Optional. What that value has to hold; omitted, the value existing is enough.' } # WHICH TYPE THE FORM IS SHOWING: the one asked for, or the one the document # declares, or none. $declared = '' if ($null -ne $Detect) { if ($Detect -is [System.Collections.IDictionary]) { if ($Detect.Contains('type')) { $declared = [string] $Detect['type'] } } elseif ($null -ne $Detect.PSObject.Properties['Type']) { $declared = [string] $Detect.Type } } $showing = $declared if ($PSBoundParameters.ContainsKey('Type')) { $showing = $Type } $choice = New-Object -TypeName System.Collections.ArrayList [void] $choice.Add([pscustomobject] @{ Type = '' Display = 'No rule - it installs every time the sequence runs' }) foreach ($current in @($schema.Keys)) { [void] $choice.Add([pscustomobject] @{ Type = [string] $current Display = [string] $said[[string] $current] }) } $field = New-Object -TypeName System.Collections.ArrayList $rule = $null $complete = $true $message = '' if (-not [string]::IsNullOrWhiteSpace($showing)) { if (-not $schema.Contains($showing)) { # A TYPE THIS BUILD DOES NOT KNOW can only come from a document # written against a newer schema, and the window must not silently # replace it with one it does understand. return [pscustomobject] @{ Type = $showing Choice = [pscustomobject[]] @($choice) Field = [pscustomobject[]] @() Complete = $false Message = ("'{0}' is not a detection type this build can run. The types are {1}." -f $showing, (@($schema.Keys) -join ', ')) Rule = $null CommandText = '@{ }' } } # THE VALUES ONLY SURVIVE A TYPE THAT DID NOT CHANGE. $keep = ($declared -eq $showing) $read = { param([string] $Key) if (-not $keep -or $null -eq $Detect) { return '' } if ($Detect -is [System.Collections.IDictionary]) { if ($Detect.Contains($Key)) { return [string] $Detect[$Key] } return '' } if ($null -eq $Detect.PSObject.Properties[$Key]) { return '' } return [string] $Detect.$Key } $rule = [System.Collections.Specialized.OrderedDictionary]::new() $rule['type'] = $showing $missing = New-Object -TypeName System.Collections.ArrayList foreach ($key in @($schema[$showing].Required + $schema[$showing].Optional)) { $keyName = [string] $key $value = [string] (& $read $keyName) $required = ($schema[$showing].Required -contains $keyName) [void] $field.Add([pscustomobject] @{ Key = $keyName Label = [string] $label[$keyName] Value = $value Required = $required Hint = [string] $hint[$keyName] }) if ($required -and [string]::IsNullOrWhiteSpace($value)) { [void] $missing.Add([string] $label[$keyName]) continue } # AN EMPTY OPTIONAL BOX IS NOT A KEY. The validator accepts the # absence; the engine would compare against the blank. if (-not [string]::IsNullOrWhiteSpace($value)) { $rule[$keyName] = $value } } if (@($missing).Count -gt 0) { $complete = $false $rule = $null $message = "this rule needs {0} before it can be saved." -f ((@($missing) -join ' and ').ToLowerInvariant()) } } # THE RULE AS IT WOULD BE TYPED. @{ } is how the rule is REMOVED - see # Set-HDTApplication - so it is the honest text for "no rule" as well as the # thing the window passes. $commandText = '@{ }' if ($null -ne $rule) { $pair = @(@($rule.Keys) | ForEach-Object { "{0} = '{1}'" -f [string] $_, ([string] $rule[$_] -replace "'", "''") }) $commandText = '@{{ {0} }}' -f ($pair -join '; ') } return [pscustomobject] @{ Type = $showing Choice = [pscustomobject[]] @($choice) Field = [pscustomobject[]] @($field) Complete = $complete Message = $message Rule = $rule CommandText = $commandText } } |