config editor
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
function Add-PersonaConditionNode {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Appends a condition or nested group to a group in a rule's condition tree (FR-028).
|
||||
|
||||
.DESCRIPTION
|
||||
Locates the group at ParentPath and appends Node to its `conditions`
|
||||
collection. Structural checks only - id/priority-style collisions do not apply
|
||||
to conditions, and nesting-depth enforcement is deliberately not duplicated
|
||||
here.
|
||||
|
||||
Depth is enforced by re-running the real validator (VR-001) against the whole
|
||||
candidate document after the edit, the same way a hand-edited file would be
|
||||
checked - see Edit-PersonaEngineConfig.ps1's candidate-edit wrapper. A second,
|
||||
local depth calculation here would risk drifting from Test-PersonaConditionGroup's
|
||||
engine semantics (RE-004), which count nesting by group levels, not by leaf
|
||||
conditions - the exact mismatch VR-002's own depth check is careful to avoid
|
||||
(see Test-PersonaConfigurationSemantic.ps1).
|
||||
|
||||
.PARAMETER Group
|
||||
The rule's `match` condition group (the root).
|
||||
|
||||
.PARAMETER ParentPath
|
||||
Path (see Get-PersonaConditionNode) to the group to append into. Empty selects
|
||||
the root.
|
||||
|
||||
.PARAMETER Node
|
||||
The condition or condition group to append.
|
||||
|
||||
.OUTPUTS
|
||||
System.Object - the same Group, mutated in place, returned for convenience.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
[OutputType([object])]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[object] $Group,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[AllowEmptyCollection()]
|
||||
[int[]] $ParentPath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[object] $Node
|
||||
)
|
||||
|
||||
$parent = Get-PersonaConditionNode -Group $Group -Path $ParentPath
|
||||
|
||||
if ($null -eq $parent.PSObject.Properties['conditions']) {
|
||||
throw 'The target node is a leaf condition, not a group. Choose a group (all/any) to add into.'
|
||||
}
|
||||
|
||||
$parent.conditions = @($parent.conditions) + $Node
|
||||
|
||||
$Group
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
function Add-PersonaConfigRule {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Appends a new rule to a rules collection, rejecting an id/priority collision (FR-027).
|
||||
|
||||
.DESCRIPTION
|
||||
Pure: takes the existing rules and a candidate rule, returns a new array. Never
|
||||
mutates the input collection, so a caller that discards the result on a
|
||||
validation failure has changed nothing.
|
||||
|
||||
The id/priority check is duplicated with PE-SEM-001/PE-SEM-002 by design - the
|
||||
interactive editor should not need a full validation round trip just to tell an
|
||||
operator they typed an id that already exists. Anything that reaches the
|
||||
semantic layer anyway (for example a collision introduced by two concurrent
|
||||
edits) is still caught there.
|
||||
|
||||
.PARAMETER Rules
|
||||
The rule collection to append to. Not mutated.
|
||||
|
||||
.PARAMETER Rule
|
||||
The candidate rule. Must carry non-blank `id` and a `priority`.
|
||||
|
||||
.OUTPUTS
|
||||
System.Object[] - the new rules collection, existing rules first.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
[OutputType([object[]])]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[AllowEmptyCollection()]
|
||||
[object[]] $Rules,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[object] $Rule
|
||||
)
|
||||
|
||||
$id = [string]$Rule.id
|
||||
if ([string]::IsNullOrWhiteSpace($id)) {
|
||||
throw 'The new rule requires a non-blank id.'
|
||||
}
|
||||
|
||||
$existing = @($Rules)
|
||||
|
||||
$byId = @($existing | Where-Object { [string]$_.id -eq $id })
|
||||
if ($byId.Count -gt 0) {
|
||||
throw "Rule id '$id' already exists (priority $($byId[0].priority)). Rule ids must be unique (RE-002)."
|
||||
}
|
||||
|
||||
$priority = $Rule.priority
|
||||
$byPriority = @($existing | Where-Object { [int]$_.priority -eq [int]$priority })
|
||||
if ($byPriority.Count -gt 0) {
|
||||
throw "Priority $priority is already used by rule '$($byPriority[0].id)'. Priorities must be unique (RE-002)."
|
||||
}
|
||||
|
||||
, ($existing + $Rule)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
function Get-PersonaConditionNode {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Navigates a rule's condition tree to the node at a path (FR-028).
|
||||
|
||||
.DESCRIPTION
|
||||
A path is a sequence of zero-based indices into successive `.conditions`
|
||||
collections, root first. An empty path returns the root group itself.
|
||||
|
||||
The returned node is the same object reference that lives inside the tree -
|
||||
deliberately. `ConvertFrom-Json` produces `PSCustomObject`/array trees of
|
||||
reference types, so a caller that sets a property on the returned leaf (for
|
||||
example `$node.value = 'x'`) mutates the tree in place. Editing a leaf's
|
||||
fields is therefore a plain property assignment, not a separate setter
|
||||
function; see Set-PersonaConditionLeaf for the one case (switching a
|
||||
condition's operator/type) where stale fields must also be cleared.
|
||||
|
||||
.PARAMETER Group
|
||||
The rule's `match` condition group (the root).
|
||||
|
||||
.PARAMETER Path
|
||||
Zero-based indices from the root, one per nesting level. Empty selects the root.
|
||||
|
||||
.OUTPUTS
|
||||
System.Object - the condition or condition group at that path.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
[OutputType([object])]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[object] $Group,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[AllowEmptyCollection()]
|
||||
[int[]] $Path
|
||||
)
|
||||
|
||||
$node = $Group
|
||||
|
||||
foreach ($index in $Path) {
|
||||
if ($null -eq $node.PSObject.Properties['conditions'] -or $null -eq $node.conditions) {
|
||||
throw "Path segment $index does not resolve: the node above it is a leaf condition, not a group."
|
||||
}
|
||||
|
||||
$children = @($node.conditions)
|
||||
if ($index -lt 0 -or $index -ge $children.Count) {
|
||||
throw "Path segment $index is out of range - this group has $($children.Count) condition(s)."
|
||||
}
|
||||
|
||||
$node = $children[$index]
|
||||
}
|
||||
|
||||
$node
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
function Remove-PersonaConditionNode {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Removes a condition or nested group from a rule's condition tree (FR-028).
|
||||
|
||||
.DESCRIPTION
|
||||
Two structural rules are enforced here rather than left to schema validation,
|
||||
because both would otherwise produce a document the schema calls invalid with
|
||||
no indication which editor action caused it:
|
||||
|
||||
- The root group itself cannot be removed - a rule's `match` is required
|
||||
(RE-001). Delete the rule instead (Remove-PersonaConfigRule).
|
||||
- A group's last remaining condition cannot be removed - an empty
|
||||
`conditions` array violates the schema's `minItems: 1` on conditionGroup.
|
||||
Remove the parent group instead, or add a replacement first.
|
||||
|
||||
.PARAMETER Group
|
||||
The rule's `match` condition group (the root).
|
||||
|
||||
.PARAMETER Path
|
||||
Path (see Get-PersonaConditionNode) to the node to remove. Must not be empty.
|
||||
|
||||
.OUTPUTS
|
||||
System.Object - the same Group, mutated in place, returned for convenience.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
[OutputType([object])]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[object] $Group,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[AllowEmptyCollection()]
|
||||
[int[]] $Path
|
||||
)
|
||||
|
||||
if (@($Path).Count -eq 0) {
|
||||
throw "The root condition group cannot be removed this way - delete the rule instead."
|
||||
}
|
||||
|
||||
# Special-cased at length 1 because PowerShell's range operator treats 0..-1 as
|
||||
# the two-element descending range (0, -1), not an empty range - the naive slice
|
||||
# would silently pick up the wrong parent for a top-level path.
|
||||
$parentPath = (@($Path).Count -eq 1) ? @() : @($Path)[0..($Path.Count - 2)]
|
||||
$index = @($Path)[-1]
|
||||
|
||||
$parent = Get-PersonaConditionNode -Group $Group -Path $parentPath
|
||||
$children = @($parent.conditions)
|
||||
|
||||
if ($index -lt 0 -or $index -ge $children.Count) {
|
||||
throw "Path segment $index is out of range - this group has $($children.Count) condition(s)."
|
||||
}
|
||||
|
||||
if ($children.Count -eq 1) {
|
||||
throw 'This is the only condition in its group. A group requires at least one condition - remove the group itself instead, or add a replacement first.'
|
||||
}
|
||||
|
||||
# Removed by position, not by value or reference - two structurally identical
|
||||
# sibling conditions are otherwise indistinguishable.
|
||||
$remaining = [System.Collections.Generic.List[object]]::new()
|
||||
for ($i = 0; $i -lt $children.Count; $i++) {
|
||||
if ($i -ne $index) { $remaining.Add($children[$i]) }
|
||||
}
|
||||
$parent.conditions = $remaining.ToArray()
|
||||
|
||||
$Group
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
function Remove-PersonaConfigRule {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Removes a rule from a rules collection by id (FR-029).
|
||||
|
||||
.DESCRIPTION
|
||||
Pure: takes the existing rules and an id, returns a new array with that rule
|
||||
absent. Throws if no rule carries the id, because a silent no-op would let an
|
||||
operator believe a delete happened when it did not.
|
||||
|
||||
.PARAMETER Rules
|
||||
The rule collection to remove from. Not mutated.
|
||||
|
||||
.PARAMETER RuleId
|
||||
The id of the rule to remove.
|
||||
|
||||
.OUTPUTS
|
||||
System.Object[] - the remaining rules.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
[OutputType([object[]])]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[AllowEmptyCollection()]
|
||||
[object[]] $Rules,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string] $RuleId
|
||||
)
|
||||
|
||||
$existing = @($Rules)
|
||||
$match = @($existing | Where-Object { [string]$_.id -eq $RuleId })
|
||||
|
||||
if ($match.Count -eq 0) {
|
||||
throw "No rule with id '$RuleId'."
|
||||
}
|
||||
|
||||
, @($existing | Where-Object { [string]$_.id -ne $RuleId })
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
function Set-PersonaConditionLeaf {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Replaces a leaf condition's fields in place (FR-028).
|
||||
|
||||
.DESCRIPTION
|
||||
Every condition-specific field (`property`, `value`, `values`,
|
||||
`groupObjectIds`, `roleIds`, `membershipMode`) is cleared before Fields is
|
||||
applied, then only the keys in Fields are set. This is not tidiness - the
|
||||
schema's `additionalProperties: false` on `condition` (VR-001 layer 2) rejects
|
||||
a stray field outright, and switching, say, a condition from `equals` (which
|
||||
carries `value`) to `in` (which carries `values`) would otherwise leave the
|
||||
old `value` behind and fail schema validation for a reason the editor caused
|
||||
but did not explain.
|
||||
|
||||
`type` and `operator` are cleared and reset the same way, since a caller
|
||||
editing a condition typically passes both.
|
||||
|
||||
.PARAMETER Node
|
||||
The leaf condition to edit (from Get-PersonaConditionNode). Mutated in place.
|
||||
|
||||
.PARAMETER Fields
|
||||
The complete replacement field set for this leaf, for example:
|
||||
`@{ type = 'property'; property = 'Department'; operator = 'isNotNull' }`.
|
||||
|
||||
.OUTPUTS
|
||||
System.Object - the same Node, mutated in place, returned for convenience.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
[OutputType([object])]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[object] $Node,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[hashtable] $Fields
|
||||
)
|
||||
|
||||
if ($null -ne $Node.PSObject.Properties['conditions']) {
|
||||
throw 'The target node is a condition group, not a leaf condition.'
|
||||
}
|
||||
|
||||
$knownFields = @('type', 'property', 'operator', 'value', 'values', 'groupObjectIds', 'roleIds', 'membershipMode', 'caseSensitive')
|
||||
|
||||
foreach ($key in $knownFields) {
|
||||
if ($null -ne $Node.PSObject.Properties[$key]) {
|
||||
$Node.PSObject.Properties.Remove($key)
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($entry in $Fields.GetEnumerator()) {
|
||||
$Node | Add-Member -NotePropertyName $entry.Key -NotePropertyValue $entry.Value -Force
|
||||
}
|
||||
|
||||
$Node
|
||||
}
|
||||
Reference in New Issue
Block a user