Skip to content

Commit d80c47c

Browse files
authored
feat(#98): refactor CLI argument handling for improved consistency and scalability (#128)
- Utilize shared declarative parser helpers for `nova` commands - Maintain existing CLI syntax and validation behavior
1 parent 2f38c3b commit d80c47c

10 files changed

Lines changed: 237 additions & 79 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
134134
- Change `Publish-NovaModule -Local` and `% nova publish --local` so a successful local publish also reloads the
135135
published
136136
module from the local install path into the active PowerShell session.
137+
- Refactor the simple routed `nova` parsers to use shared declarative parser helpers while keeping the existing CLI
138+
syntax, validation, and routed command behavior unchanged.
139+
- `nova build`, `nova test`, and `nova bump` now share one switch-parser pattern.
140+
- `nova update`, `nova version`, and `nova notification` now share one mode-parser pattern.
137141

138142
### Fixed
139143

src/private/cli/ConvertFromNovaBuildCliArgument.ps1

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,9 @@ function ConvertFrom-NovaBuildCliArgument {
44
[string[]]$Arguments
55
)
66

7-
$Arguments = ConvertTo-NovaCliArgumentArray -BoundParameters $PSBoundParameters -Arguments $Arguments
8-
$options = @{}
9-
10-
foreach ($token in $Arguments) {
11-
switch -Regex ($token) {
12-
'^(--continuous-integration|-i)$' {
13-
$options.ContinuousIntegration = $true
14-
}
15-
default {
16-
Stop-NovaOperation -Message "Unknown argument: $token" -ErrorId 'Nova.Validation.UnknownCliArgument' -Category InvalidArgument -TargetObject $token
17-
}
18-
}
7+
return ConvertFrom-NovaCliSwitchArgument -Arguments $Arguments -TokenMap @{
8+
'--continuous-integration' = 'ContinuousIntegration'
9+
'-i' = 'ContinuousIntegration'
1910
}
20-
21-
return $options
2211
}
2312

src/private/cli/ConvertFromNovaBumpCliArgument.ps1

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,11 @@ function ConvertFrom-NovaBumpCliArgument {
44
[string[]]$Arguments
55
)
66

7-
$Arguments = ConvertTo-NovaCliArgumentArray -BoundParameters $PSBoundParameters -Arguments $Arguments
8-
$options = @{}
9-
10-
foreach ($token in $Arguments) {
11-
switch -Regex ($token) {
12-
'^(--preview|-p)$' {
13-
$options.Preview = $true
14-
}
15-
'^(--continuous-integration|-i)$' {
16-
$options.ContinuousIntegration = $true
17-
}
18-
default {
19-
Stop-NovaOperation -Message "Unknown argument: $token" -ErrorId 'Nova.Validation.UnknownCliArgument' -Category InvalidArgument -TargetObject $token
20-
}
21-
}
7+
return ConvertFrom-NovaCliSwitchArgument -Arguments $Arguments -TokenMap @{
8+
'--preview' = 'Preview'
9+
'-p' = 'Preview'
10+
'--continuous-integration' = 'ContinuousIntegration'
11+
'-i' = 'ContinuousIntegration'
2212
}
23-
24-
return $options
2513
}
2614

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
function Get-NovaCliModeArgumentValue {
2+
[CmdletBinding()]
3+
param(
4+
[string[]]$Arguments,
5+
[Parameter(Mandatory)][pscustomobject]$Definition
6+
)
7+
8+
$Arguments = ConvertTo-NovaCliArgumentArray -BoundParameters $PSBoundParameters -Arguments $Arguments
9+
if ($Arguments.Count -eq 0) {
10+
return $Definition.EmptyResult
11+
}
12+
13+
if ($Arguments.Count -ne 1) {
14+
Stop-NovaOperation -Message $Definition.Usage.Message -ErrorId $Definition.Usage.ErrorId -Category InvalidArgument -TargetObject $Arguments
15+
}
16+
17+
$value = $Definition.TokenMap[$Arguments[0]]
18+
if ($null -ne $value) {
19+
return $value
20+
}
21+
22+
if ($Definition.UnknownArgumentUsesUsageError) {
23+
Stop-NovaOperation -Message $Definition.Usage.Message -ErrorId $Definition.Usage.ErrorId -Category InvalidArgument -TargetObject $Arguments
24+
}
25+
26+
Stop-NovaOperation -Message "Unknown argument: $( $Arguments[0] )" -ErrorId 'Nova.Validation.UnknownCliArgument' -Category InvalidArgument -TargetObject $Arguments[0]
27+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
function Get-NovaCliSwitchOptionName {
2+
[CmdletBinding()]
3+
param(
4+
[Parameter(Mandatory)][hashtable]$TokenMap,
5+
[Parameter(Mandatory)][string]$Token
6+
)
7+
8+
$optionName = $TokenMap[$Token]
9+
if ( [string]::IsNullOrWhiteSpace($optionName)) {
10+
Stop-NovaOperation -Message "Unknown argument: $Token" -ErrorId 'Nova.Validation.UnknownCliArgument' -Category InvalidArgument -TargetObject $Token
11+
}
12+
13+
return $optionName
14+
}
15+
16+
function ConvertFrom-NovaCliSwitchArgument {
17+
[CmdletBinding()]
18+
param(
19+
[string[]]$Arguments,
20+
[Parameter(Mandatory)][hashtable]$TokenMap
21+
)
22+
23+
$Arguments = ConvertTo-NovaCliArgumentArray -BoundParameters $PSBoundParameters -Arguments $Arguments
24+
$options = @{}
25+
foreach ($token in $Arguments) {
26+
$options[(Get-NovaCliSwitchOptionName -TokenMap $TokenMap -Token $token)] = $true
27+
}
28+
29+
return $options
30+
}

src/private/cli/ConvertFromNovaNotificationCliArgument.ps1

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,18 @@ function ConvertFrom-NovaNotificationCliArgument {
44
[string[]]$Arguments
55
)
66

7-
$Arguments = ConvertTo-NovaCliArgumentArray -BoundParameters $PSBoundParameters -Arguments $Arguments
8-
if ($Arguments.Count -eq 0) {
9-
return 'status'
10-
}
11-
12-
if ($Arguments.Count -ne 1) {
13-
Stop-NovaOperation -Message "Unsupported 'nova notification' usage. Use 'nova notification', 'nova notification --enable'/'nova notification -e', or 'nova notification --disable'/'nova notification -d'." -ErrorId 'Nova.Validation.UnsupportedNotificationCliUsage' -Category InvalidArgument -TargetObject $Arguments
14-
}
15-
16-
switch -Regex ($Arguments[0]) {
17-
'^(--enable|-e)$' {
18-
return 'enable'
19-
}
20-
'^(--disable|-d)$' {
21-
return 'disable'
7+
return Get-NovaCliModeArgumentValue -Arguments $Arguments -Definition ([pscustomobject]@{
8+
EmptyResult = 'status'
9+
TokenMap = @{
10+
'--enable' = 'enable'
11+
'-e' = 'enable'
12+
'--disable' = 'disable'
13+
'-d' = 'disable'
2214
}
23-
default {
24-
Stop-NovaOperation -Message "Unknown argument: $( $Arguments[0] )" -ErrorId 'Nova.Validation.UnknownCliArgument' -Category InvalidArgument -TargetObject $Arguments[0]
15+
Usage = [pscustomobject]@{
16+
Message = "Unsupported 'nova notification' usage. Use 'nova notification', 'nova notification --enable'/'nova notification -e', or 'nova notification --disable'/'nova notification -d'."
17+
ErrorId = 'Nova.Validation.UnsupportedNotificationCliUsage'
2518
}
26-
}
19+
UnknownArgumentUsesUsageError = $false
20+
})
2721
}

src/private/cli/ConvertFromNovaTestCliArgument.ps1

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,9 @@ function ConvertFrom-NovaTestCliArgument {
44
[string[]]$Arguments
55
)
66

7-
$Arguments = ConvertTo-NovaCliArgumentArray -BoundParameters $PSBoundParameters -Arguments $Arguments
8-
$options = @{}
9-
10-
foreach ($token in $Arguments) {
11-
switch -Regex ($token) {
12-
'^(--build|-b)$' {
13-
$options.Build = $true
14-
}
15-
default {
16-
Stop-NovaOperation -Message "Unknown argument: $token" -ErrorId 'Nova.Validation.UnknownCliArgument' -Category InvalidArgument -TargetObject $token
17-
}
18-
}
7+
return ConvertFrom-NovaCliSwitchArgument -Arguments $Arguments -TokenMap @{
8+
'--build' = 'Build'
9+
'-b' = 'Build'
1910
}
20-
21-
return $options
2211
}
2312

src/private/cli/ConvertFromNovaUpdateCliArgument.ps1

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ function ConvertFrom-NovaUpdateCliArgument {
44
[string[]]$Arguments
55
)
66

7-
$Arguments = ConvertTo-NovaCliArgumentArray -BoundParameters $PSBoundParameters -Arguments $Arguments
8-
if ($Arguments.Count -eq 0) {
9-
return @{}
10-
}
11-
12-
Stop-NovaOperation -Message "Unsupported 'nova update' usage. Use 'nova update'." -ErrorId 'Nova.Validation.UnsupportedUpdateCliUsage' -Category InvalidArgument -TargetObject $Arguments
7+
return Get-NovaCliModeArgumentValue -Arguments $Arguments -Definition ([pscustomobject]@{
8+
EmptyResult = @{}
9+
TokenMap = @{}
10+
Usage = [pscustomobject]@{
11+
Message = "Unsupported 'nova update' usage. Use 'nova update'."
12+
ErrorId = 'Nova.Validation.UnsupportedUpdateCliUsage'
13+
}
14+
UnknownArgumentUsesUsageError = $true
15+
})
1316
}

src/private/cli/ConvertFromNovaVersionCliArgument.ps1

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ function ConvertFrom-NovaVersionCliArgument {
44
[string[]]$Arguments
55
)
66

7-
$Arguments = ConvertTo-NovaCliArgumentArray -BoundParameters $PSBoundParameters -Arguments $Arguments
8-
if ($Arguments.Count -eq 0) {
9-
return @{Installed = $false}
10-
}
11-
12-
if ($Arguments.Count -eq 1 -and $Arguments[0] -match '^(--installed|-i)$') {
13-
return @{Installed = $true}
14-
}
15-
16-
Stop-NovaOperation -Message "Unsupported 'nova version' usage. Use 'nova version' or 'nova version --installed'/'nova version -i'." -ErrorId 'Nova.Validation.UnsupportedVersionCliUsage' -Category InvalidArgument -TargetObject $Arguments
7+
return Get-NovaCliModeArgumentValue -Arguments $Arguments -Definition ([pscustomobject]@{
8+
EmptyResult = @{Installed = $false}
9+
TokenMap = @{
10+
'--installed' = @{Installed = $true}
11+
'-i' = @{Installed = $true}
12+
}
13+
Usage = [pscustomobject]@{
14+
Message = "Unsupported 'nova version' usage. Use 'nova version' or 'nova version --installed'/'nova version -i'."
15+
ErrorId = 'Nova.Validation.UnsupportedVersionCliUsage'
16+
}
17+
UnknownArgumentUsesUsageError = $true
18+
})
1719
}

tests/CliSharedParser.Tests.ps1

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
$script:coverageGapsCliTestSupportPath = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot 'CoverageGaps.Cli.TestSupport.ps1')).Path
2+
. $script:coverageGapsCliTestSupportPath
3+
$global:cliSharedParserTestSupportFunctionNameList = @(
4+
'Assert-TestStructuredCliError'
5+
)
6+
7+
foreach ($functionName in $global:cliSharedParserTestSupportFunctionNameList) {
8+
$scriptBlock = (Get-Command -Name $functionName -CommandType Function -ErrorAction Stop).ScriptBlock
9+
Set-Item -Path "function:global:$functionName" -Value $scriptBlock
10+
}
11+
12+
BeforeAll {
13+
$here = Split-Path -Parent $PSCommandPath
14+
$script:repoRoot = Split-Path -Parent $here
15+
$script:moduleName = (Get-Content -LiteralPath (Join-Path $script:repoRoot 'project.json') -Raw | ConvertFrom-Json).ProjectName
16+
$script:distModuleDir = Join-Path $script:repoRoot "dist/$script:moduleName"
17+
$coverageGapsCliTestSupportPath = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot 'CoverageGaps.Cli.TestSupport.ps1')).Path
18+
19+
if (-not (Test-Path -LiteralPath $script:distModuleDir)) {
20+
throw "Expected built $script:moduleName module at: $script:distModuleDir. Run Invoke-NovaBuild in the repo root first."
21+
}
22+
23+
. $coverageGapsCliTestSupportPath
24+
foreach ($functionName in $global:cliSharedParserTestSupportFunctionNameList) {
25+
$scriptBlock = (Get-Command -Name $functionName -CommandType Function -ErrorAction Stop).ScriptBlock
26+
Set-Item -Path "function:global:$functionName" -Value $scriptBlock
27+
}
28+
Remove-Module $script:moduleName -ErrorAction SilentlyContinue
29+
Import-Module $script:distModuleDir -Force
30+
}
31+
32+
Describe 'CLI shared parser helpers' {
33+
It 'ConvertFrom-NovaCliSwitchArgument maps declarative switch schemas into boolean option sets' {
34+
InModuleScope $script:moduleName {
35+
$options = ConvertFrom-NovaCliSwitchArgument -Arguments @('--preview', '-i') -TokenMap @{
36+
'--preview' = 'Preview'
37+
'-p' = 'Preview'
38+
'--continuous-integration' = 'ContinuousIntegration'
39+
'-i' = 'ContinuousIntegration'
40+
}
41+
42+
$options.Preview | Should -BeTrue
43+
$options.ContinuousIntegration | Should -BeTrue
44+
}
45+
}
46+
47+
It 'Get-NovaCliModeArgumentValue supports empty results, mapped values, usage errors, and unknown arguments' {
48+
InModuleScope $script:moduleName {
49+
$notificationDefinition = [pscustomobject]@{
50+
EmptyResult = 'status'
51+
TokenMap = @{
52+
'--enable' = 'enable'
53+
}
54+
Usage = [pscustomobject]@{
55+
Message = "Unsupported 'nova notification' usage."
56+
ErrorId = 'Nova.Validation.UnsupportedNotificationCliUsage'
57+
}
58+
UnknownArgumentUsesUsageError = $false
59+
}
60+
$updateDefinition = [pscustomobject]@{
61+
EmptyResult = @{}
62+
TokenMap = @{}
63+
Usage = [pscustomobject]@{
64+
Message = "Unsupported 'nova update' usage. Use 'nova update'."
65+
ErrorId = 'Nova.Validation.UnsupportedUpdateCliUsage'
66+
}
67+
UnknownArgumentUsesUsageError = $true
68+
}
69+
70+
$emptyResult = Get-NovaCliModeArgumentValue -Arguments @() -Definition $notificationDefinition
71+
$mappedResult = Get-NovaCliModeArgumentValue -Arguments @('--enable') -Definition $notificationDefinition
72+
73+
$usageError = $null
74+
try {
75+
Get-NovaCliModeArgumentValue -Arguments @('--enable', '--disable') -Definition $notificationDefinition
76+
}
77+
catch {
78+
$usageError = $_
79+
}
80+
81+
$unknownArgumentError = $null
82+
try {
83+
Get-NovaCliModeArgumentValue -Arguments @('--bogus') -Definition $notificationDefinition
84+
}
85+
catch {
86+
$unknownArgumentError = $_
87+
}
88+
89+
$updateUsageError = $null
90+
try {
91+
Get-NovaCliModeArgumentValue -Arguments @('--bogus') -Definition $updateDefinition
92+
}
93+
catch {
94+
$updateUsageError = $_
95+
}
96+
97+
$emptyResult | Should -Be 'status'
98+
$mappedResult | Should -Be 'enable'
99+
Assert-TestStructuredCliError -ThrownError $usageError -ExpectedError ([pscustomobject]@{
100+
Message = "Unsupported 'nova notification' usage."
101+
ErrorId = 'Nova.Validation.UnsupportedNotificationCliUsage'
102+
Category = [System.Management.Automation.ErrorCategory]::InvalidArgument
103+
})
104+
Assert-TestStructuredCliError -ThrownError $unknownArgumentError -ExpectedError ([pscustomobject]@{
105+
Message = 'Unknown argument: --bogus'
106+
ErrorId = 'Nova.Validation.UnknownCliArgument'
107+
Category = [System.Management.Automation.ErrorCategory]::InvalidArgument
108+
TargetObject = '--bogus'
109+
})
110+
Assert-TestStructuredCliError -ThrownError $updateUsageError -ExpectedError ([pscustomobject]@{
111+
Message = "Unsupported 'nova update' usage. Use 'nova update'."
112+
ErrorId = 'Nova.Validation.UnsupportedUpdateCliUsage'
113+
Category = [System.Management.Automation.ErrorCategory]::InvalidArgument
114+
})
115+
}
116+
}
117+
118+
It 'the shared parsers preserve the existing routed CLI contracts for bump, version, update, and notification' {
119+
InModuleScope $script:moduleName {
120+
$bumpOptions = ConvertFrom-NovaBumpCliArgument -Arguments @('--preview', '--continuous-integration')
121+
122+
$bumpOptions.Preview | Should -BeTrue
123+
$bumpOptions.ContinuousIntegration | Should -BeTrue
124+
(ConvertFrom-NovaVersionCliArgument).Installed | Should -BeFalse
125+
(ConvertFrom-NovaVersionCliArgument -Arguments @('--installed')).Installed | Should -BeTrue
126+
ConvertFrom-NovaNotificationCliArgument | Should -Be 'status'
127+
ConvertFrom-NovaNotificationCliArgument -Arguments @('--enable') | Should -Be 'enable'
128+
ConvertFrom-NovaNotificationCliArgument -Arguments @('-d') | Should -Be 'disable'
129+
(ConvertFrom-NovaUpdateCliArgument).Count | Should -Be 0
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)