Skip to content

Commit ecc3fb8

Browse files
feat: enum null-clearing on Set-NBDCIMInterface (#398 follow-up) (#401)
* docs: design spec for Set-NBDCIMInterface enum null-clearing Follow-up to PR #398, which left 5 enum-string parameters on Set-NBDCIMInterface without null-clearing support. The initially-preferred [AllowNull()] pattern was empirically disproven — PowerShell coerces \$null to '' before ValidateSet runs, so null values are rejected. Approved approach: empty-string sentinel in ValidateSet + AllowEmptyString + translate '' to \$null before JSON serialization (same end result as the numeric [Nullable[T]] fields from PR #398). Scope is deliberately narrow: one function, five parameters, one new translation loop in process {}. * feat: enum null-clearing on Set-NBDCIMInterface (#398 follow-up) Allows callers to clear any of the five enum-string fields (Duplex, POE_Mode, POE_Type, RF_Role, Mode) on an existing interface by passing '' (empty string) as the parameter value. The PATCH body emits "<field>": null, which NetBox accepts as clear-the-field. Pattern: [AllowEmptyString()] + '' added to each ValidateSet, then a new 7-line translation loop at the top of process {} converts empty-string sentinel values to \$null so BuildURIComponents + ConvertTo-Json produce a JSON null. Same end result as the 9 numeric [Nullable[T]] parameters from PR #398. The initially-preferred [AllowNull()] pattern was empirically disproven — PowerShell coerces \$null to '' before ValidateSet runs, so '-Duplex \$null' throws 'argument does not belong to set'. Option B (empty-string sentinel) works reliably on both PS 5.1 and PS 7. Adds 5 null-clearing tests in Tests/DCIM.Interfaces.Tests.ps1 in a new Context nested inside Context "Set-NBDCIMInterface". Full unit regression: 2291 baseline + 5 new = 2296 passed / 0 failed. Closes the scope-limitation explicitly deferred in PR #398. * docs: align spec loop variable name with implementation (/Users/elvis/.nix-profile -> $clearable) Per Gemini review on PR #401 — trivial documentation consistency fix.
1 parent 917bf16 commit ecc3fb8

3 files changed

Lines changed: 160 additions & 5 deletions

File tree

Functions/DCIM/Interfaces/Set-NBDCIMInterface.ps1

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ function Set-NBDCIMInterface {
158158

159159
[Nullable[uint64]]$Speed,
160160

161-
[ValidateSet('full', 'half', 'auto', IgnoreCase = $true)]
161+
[AllowEmptyString()]
162+
[ValidateSet('full', 'half', 'auto', '', IgnoreCase = $true)]
162163
[string]$Duplex,
163164

164165
[bool]$Mark_Connected,
@@ -168,10 +169,12 @@ function Set-NBDCIMInterface {
168169

169170
[uint64[]]$VDCS,
170171

171-
[ValidateSet('pd', 'pse', IgnoreCase = $true)]
172+
[AllowEmptyString()]
173+
[ValidateSet('pd', 'pse', '', IgnoreCase = $true)]
172174
[string]$POE_Mode,
173175

174-
[ValidateSet('type1-ieee802.3af', 'type2-ieee802.3at', 'type3-ieee802.3bt', 'type4-ieee802.3bt', 'passive-24v-2pair', 'passive-24v-4pair', 'passive-48v-2pair', 'passive-48v-4pair', IgnoreCase = $true)]
176+
[AllowEmptyString()]
177+
[ValidateSet('type1-ieee802.3af', 'type2-ieee802.3at', 'type3-ieee802.3bt', 'type4-ieee802.3bt', 'passive-24v-2pair', 'passive-24v-4pair', 'passive-48v-2pair', 'passive-48v-4pair', '', IgnoreCase = $true)]
175178
[string]$POE_Type,
176179

177180
[uint64]$Vlan_Group,
@@ -180,7 +183,8 @@ function Set-NBDCIMInterface {
180183

181184
[uint64]$VRF,
182185

183-
[ValidateSet('ap', 'station', IgnoreCase = $true)]
186+
[AllowEmptyString()]
187+
[ValidateSet('ap', 'station', '', IgnoreCase = $true)]
184188
[string]$RF_Role,
185189

186190
[string]$RF_Channel,
@@ -207,7 +211,8 @@ function Set-NBDCIMInterface {
207211

208212
[string]$Description,
209213

210-
[ValidateSet('Access', 'Tagged', 'Tagged All', 'Q-in-Q', 'q-in-q', '100', '200', '300', '400', IgnoreCase = $true)]
214+
[AllowEmptyString()]
215+
[ValidateSet('Access', 'Tagged', 'Tagged All', 'Q-in-Q', 'q-in-q', '100', '200', '300', '400', '', IgnoreCase = $true)]
211216
[string]$Mode,
212217

213218
[uint64]$Untagged_VLAN,
@@ -274,6 +279,17 @@ function Set-NBDCIMInterface {
274279

275280
process {
276281
Write-Verbose "Updating DCIM Interface"
282+
283+
# Translate empty-string sentinel to $null for the 5 clearable enum parameters.
284+
# Users pass '' to clear a field server-side; BuildURIComponents +
285+
# ConvertTo-Json emit "field": null on the wire, which NetBox PATCH accepts.
286+
$clearableEnums = @('Duplex', 'POE_Mode', 'POE_Type', 'RF_Role', 'Mode')
287+
foreach ($clearable in $clearableEnums) {
288+
if ($PSBoundParameters.ContainsKey($clearable) -and $PSBoundParameters[$clearable] -eq '') {
289+
$PSBoundParameters[$clearable] = $null
290+
}
291+
}
292+
277293
foreach ($InterfaceId in $Id) {
278294

279295
$Segments = [System.Collections.ArrayList]::new(@('dcim', 'interfaces', $InterfaceId))

Tests/DCIM.Interfaces.Tests.ps1

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,33 @@ Describe "DCIM Interfaces Tests" -Tag 'DCIM', 'Interfaces' {
647647
($Result.Body | ConvertFrom-Json).changelog_message | Should -Be 'Updated during maintenance'
648648
}
649649
}
650+
651+
Context "Set-NBDCIMInterface enum null-clearing (#398 follow-up)" {
652+
It "Should send null when -Duplex '' is passed" {
653+
$Result = Set-NBDCIMInterface -Id 42 -Duplex ''
654+
$Result.Body | Should -Match '"duplex"\s*:\s*null'
655+
}
656+
657+
It "Should send null when -POE_Mode '' is passed" {
658+
$Result = Set-NBDCIMInterface -Id 42 -POE_Mode ''
659+
$Result.Body | Should -Match '"poe_mode"\s*:\s*null'
660+
}
661+
662+
It "Should send null when -POE_Type '' is passed" {
663+
$Result = Set-NBDCIMInterface -Id 42 -POE_Type ''
664+
$Result.Body | Should -Match '"poe_type"\s*:\s*null'
665+
}
666+
667+
It "Should send null when -RF_Role '' is passed" {
668+
$Result = Set-NBDCIMInterface -Id 42 -RF_Role ''
669+
$Result.Body | Should -Match '"rf_role"\s*:\s*null'
670+
}
671+
672+
It "Should send null when -Mode '' is passed" {
673+
$Result = Set-NBDCIMInterface -Id 42 -Mode ''
674+
$Result.Body | Should -Match '"mode"\s*:\s*null'
675+
}
676+
}
650677
}
651678

652679
Context "Remove-NBDCIMInterface" {
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
---
2+
title: Null-clearing for enum string parameters on Set-NBDCIMInterface
3+
status: approved
4+
date: 2026-04-17
5+
predecessor: PR #398 (Interface parameters — deferred this scope item)
6+
---
7+
8+
# Set-NBDCIMInterface — null-clearing for enum string parameters
9+
10+
## Context
11+
12+
PR #398 added 9 nullable numeric parameters to `Set-NBDCIMInterface` using the `[Nullable[T]]` pattern so callers can pass `$null` to clear the server-side value via PATCH. That pattern doesn't extend to string parameters with `[ValidateSet]` — PowerShell coerces `$null` to `""` at bind time and then ValidateSet rejects the empty string.
13+
14+
This spec closes that gap for the five enum-string parameters on `Set-NBDCIMInterface`:
15+
16+
- `-Duplex`
17+
- `-POE_Mode`
18+
- `-POE_Type`
19+
- `-RF_Role`
20+
- `-Mode`
21+
22+
## Decision — Option B (empty-string sentinel)
23+
24+
During the brainstorm, an initial preference for `[AllowNull()]` (Option A) was empirically disproven: `[AllowNull()] [ValidateSet(...)] [string]$X` + `-X $null` throws `"" does not belong to the set`. PowerShell coerces `$null` to `""` before ValidateSet runs.
25+
26+
**Approved pattern (Option B):**
27+
28+
```powershell
29+
[AllowEmptyString()]
30+
[ValidateSet('full', 'half', 'auto', '', IgnoreCase = $true)]
31+
[string]$Duplex
32+
```
33+
34+
- User writes `Set-NBDCIMInterface -Id 42 -Duplex ''` to clear the server-side value.
35+
- In the function's `process {}` block, a single-pass loop translates empty-string sentinel to `$null` for the 5 enum parameters.
36+
- `BuildURIComponents` then writes `$null` to the body hashtable; `ConvertTo-Json` serializes it as `null` correctly on both PS 5.1 and PS 7 (verified empirically — same mechanism as the 9 numeric nullable parameters from PR #398).
37+
38+
The result on the wire: `PATCH /api/dcim/interfaces/42/` with body `{"duplex": null}` — which NetBox accepts as "clear this field".
39+
40+
### Rejected alternatives
41+
42+
- **Option A** (`[AllowNull()]`): empirically doesn't work with `[string]` + `[ValidateSet]` combination; PowerShell binding layer rejects it.
43+
- **Option E** (drop `[ValidateSet]` entirely): breaks with project convention that every enum parameter has client-side validation; removes Get-Help discoverability and tab completion for valid values.
44+
- **Option F** (defer indefinitely): acceptable but leaves the gap documented-not-fixed; Option B is cheap enough that shipping it is preferable.
45+
46+
## Scope
47+
48+
**Single function modified:** `Functions/DCIM/Interfaces/Set-NBDCIMInterface.ps1`
49+
50+
**Five parameters updated:**
51+
52+
| Parameter | Current ValidateSet | New ValidateSet |
53+
|---|---|---|
54+
| `Duplex` | `'full','half','auto'` | `'full','half','auto',''` + `[AllowEmptyString()]` |
55+
| `POE_Mode` | `'pd','pse'` | `'pd','pse',''` + `[AllowEmptyString()]` |
56+
| `POE_Type` | 8-value set | 8 values + `''` + `[AllowEmptyString()]` |
57+
| `RF_Role` | `'ap','station'` | `'ap','station',''` + `[AllowEmptyString()]` |
58+
| `Mode` | 9-value set (title-case + lower + legacy numeric) | 9 values + `''` + `[AllowEmptyString()]` |
59+
60+
All `IgnoreCase = $true` attributes preserved.
61+
62+
**One new code block in `process {}`:**
63+
64+
Inserted after the existing Mode translation switch, before the call to `BuildURIComponents`:
65+
66+
```powershell
67+
# Translate empty-string sentinel to $null for the 5 clearable enum params.
68+
# Users pass '' to clear a field server-side; BuildURIComponents +
69+
# ConvertTo-Json emit "field": null on the wire, which NetBox PATCH accepts.
70+
$clearableEnums = @('Duplex', 'POE_Mode', 'POE_Type', 'RF_Role', 'Mode')
71+
foreach ($clearable in $clearableEnums) {
72+
if ($PSBoundParameters.ContainsKey($clearable) -and $PSBoundParameters[$clearable] -eq '') {
73+
$PSBoundParameters[$clearable] = $null
74+
}
75+
}
76+
```
77+
78+
Placement matters: Mode has an existing translation switch (PR #398) that skips when Mode is `IsNullOrWhiteSpace`, so an empty-string Mode isn't translated to `'access'`. The new block runs after that switch, so Mode's `''` passes through and becomes `$null` in the body.
79+
80+
## Tests
81+
82+
5 new tests in `Tests/DCIM.Interfaces.Tests.ps1`, in a new `Context "Set-NBDCIMInterface enum null-clearing (#398 follow-up)"` nested inside the existing `Context "Set-NBDCIMInterface"`:
83+
84+
```powershell
85+
It "Should send null when -Duplex '' is passed" {
86+
$Result = Set-NBDCIMInterface -Id 42 -Duplex ''
87+
$Result.Body | Should -Match '"duplex"\s*:\s*null'
88+
}
89+
# ... same pattern for POE_Mode, POE_Type, RF_Role, Mode
90+
```
91+
92+
Asserts the JSON body contains the literal `"<field>": null` pattern. Matches the regex style already used by the 9 numeric null-clearing tests added in PR #398.
93+
94+
## Verification checklist
95+
96+
1. `Invoke-Pester ./Tests/DCIM.Interfaces.Tests.ps1` → all green (baseline + 5 new)
97+
2. `Invoke-Pester ./Tests/ -ExcludeTagFilter Integration,Live,Scenario` → full regression green
98+
3. Filter-exclusion auditor → 0 findings (no ValidateSet is drift-relevant here — we're ADDING `''`, not a NetBox enum value)
99+
4. PSScriptAnalyzer on the modified file → 0 new findings
100+
5. Existing positive enum tests (`-Duplex 'auto'`, `-POE_Mode 'pse'`, etc.) still pass — adding `''` to ValidateSet doesn't affect existing valid values
101+
102+
## Release impact
103+
104+
Non-breaking. `-Duplex ''` previously threw `ValidateSet` error; now clears the field. Users who relied on that error to detect invalid input should migrate to catching `-Duplex $null` (which still throws) or pre-validating.
105+
106+
Candidate for next patch release (v4.5.8.2 or bundle with other work into v4.5.9.0).
107+
108+
## Out of scope
109+
110+
- Null-clearing for enum string parameters on **other** Set functions (only `Set-NBDCIMInterface` here). If demand surfaces, extend in a follow-up.
111+
- Refactoring the sentinel logic into a shared helper — 5-line loop per function is simpler than a helper for now; if more Set functions adopt this, extract later.
112+
- Client-side detection of "`-Duplex $null` was meant" — PowerShell's string coercion makes this unreachable from the cmdlet; document the `''` idiom in `.NOTES`.

0 commit comments

Comments
 (0)