Skip to content

Commit b3629fc

Browse files
wing328gaurav0107claude
authored
[POWERSHELL] fix: single-quote DTO property names to prevent $-variable interpolation (#23624)
* [POWERSHELL] fix: single-quote DTO property names to prevent $-variable interpolation PowerShell treats `$` inside double-quoted strings as the sigil for variable interpolation, so `"$foo" = ${Foo}` inside a hash literal becomes `<value-of-$foo> = <value-of-Foo>`. When an OpenAPI property name starts with (or contains) `$` — e.g. `$type` from C# polymorphic payloads or `$ref` / `$schema` from JSON Schema — the generated Initialize- and ConvertFrom- commandlets emit invalid PSCustomObject hash keys and empty regex patterns, breaking DTO (de)serialization. Swap all user-property-name emissions in `model_simple.mustache` from double-quoted to single-quoted literals so baseName is preserved verbatim: - `$PSO = [PSCustomObject]@{ '<baseName>' = ${<name>} }` (3 sites) - `$AllProperties = ('<baseName>', ...)` (1 site) - `-match '<baseName>'` and `.Properties['<baseName>']` (4 sites) Fixes: #23535 Co-Authored-By: Claude <noreply@anthropic.com> * [POWERSHELL] test: assert every baseName emission site is single-quoted Extends the regression test to cover every place model_simple.mustache embeds a user-supplied property name: the Initialize- hash literal, the ConvertFrom-…JsonTo… hash literal, the $AllProperties allow-list, the .Properties[…] indexer, and the -match presence check. All five must be single-quoted so PowerShell preserves `$` literally. Co-Authored-By: Claude <noreply@anthropic.com> * update powershell samples --------- Co-authored-by: gaurav0107 <gauravdubey0107@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 85aee00 commit b3629fc

73 files changed

Lines changed: 1023 additions & 924 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

modules/openapi-generator/src/main/resources/powershell/model_simple.mustache

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ function Initialize-{{{apiNamePrefix}}}{{{classname}}} {
122122
$PSO = [PSCustomObject]@{
123123
{{=<< >>=}}
124124
<<#allVars>>
125-
"<<baseName>>" = ${<<name>>}
125+
'<<baseName>>' = ${<<name>>}
126126
<</allVars>>
127127
<<={{ }}=>>
128128
}
@@ -184,7 +184,7 @@ function Initialize-{{{apiNamePrefix}}}{{{classname}}} {
184184
{{=<< >>=}}
185185
<<#allVars>>
186186
<<^isReadOnly>>
187-
"<<baseName>>" = ${<<name>>}
187+
'<<baseName>>' = ${<<name>>}
188188
<</isReadOnly>>
189189
<</allVars>>
190190
<<={{ }}=>>
@@ -228,7 +228,7 @@ function ConvertFrom-{{{apiNamePrefix}}}JsonTo{{{classname}}} {
228228
{{/isAdditionalPropertiesTrue}}
229229

230230
# check if Json contains properties not defined in {{{apiNamePrefix}}}{{{classname}}}
231-
$AllProperties = ({{#allVars}}"{{{baseName}}}"{{^-last}}, {{/-last}}{{/allVars}})
231+
$AllProperties = ({{#allVars}}'{{{baseName}}}'{{^-last}}, {{/-last}}{{/allVars}})
232232
foreach ($name in $JsonParameters.PsObject.Properties.Name) {
233233
{{^isAdditionalPropertiesTrue}}
234234
if (!($AllProperties.Contains($name))) {
@@ -250,29 +250,29 @@ function ConvertFrom-{{{apiNamePrefix}}}JsonTo{{{classname}}} {
250250
}
251251

252252
{{/-first}}
253-
if (!([bool]($JsonParameters.PSobject.Properties.name -match "{{{baseName}}}"))) {
253+
if (!([bool]($JsonParameters.PSobject.Properties.name -match '{{{baseName}}}'))) {
254254
throw "Error! JSON cannot be serialized due to the required property '{{{baseName}}}' missing."
255255
} else {
256-
${{name}} = $JsonParameters.PSobject.Properties["{{{baseName}}}"].value
256+
${{name}} = $JsonParameters.PSobject.Properties['{{{baseName}}}'].value
257257
}
258258

259259
{{/requiredVars}}
260260
{{#optionalVars}}
261-
if (!([bool]($JsonParameters.PSobject.Properties.name -match "{{{baseName}}}"))) { #optional property not found
261+
if (!([bool]($JsonParameters.PSobject.Properties.name -match '{{{baseName}}}'))) { #optional property not found
262262
${{name}} = $null
263263
} else {
264-
${{name}} = $JsonParameters.PSobject.Properties["{{{baseName}}}"].value
264+
${{name}} = $JsonParameters.PSobject.Properties['{{{baseName}}}'].value
265265
}
266266

267267
{{/optionalVars}}
268268
$PSO = [PSCustomObject]@{
269269
{{=<< >>=}}
270270
<<#allVars>>
271-
"<<baseName>>" = ${<<name>>}
271+
'<<baseName>>' = ${<<name>>}
272272
<</allVars>>
273273
<<={{ }}=>>
274274
{{#isAdditionalPropertiesTrue}}
275-
"AdditionalProperties" = ${{{apiNamePrefix}}}{{{classname}}}AdditionalProperties
275+
'AdditionalProperties' = ${{{apiNamePrefix}}}{{{classname}}}AdditionalProperties
276276
{{/isAdditionalPropertiesTrue}}
277277
}
278278

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.openapitools.codegen.powershell;
18+
19+
import io.swagger.parser.OpenAPIParser;
20+
import io.swagger.v3.oas.models.OpenAPI;
21+
import io.swagger.v3.parser.core.models.ParseOptions;
22+
import org.openapitools.codegen.ClientOptInput;
23+
import org.openapitools.codegen.CodegenConstants;
24+
import org.openapitools.codegen.DefaultGenerator;
25+
import org.openapitools.codegen.languages.PowerShellClientCodegen;
26+
import org.testng.annotations.Test;
27+
28+
import java.io.File;
29+
import java.io.IOException;
30+
import java.nio.file.Files;
31+
import java.nio.file.Paths;
32+
33+
import static org.openapitools.codegen.TestUtils.assertFileContains;
34+
import static org.openapitools.codegen.TestUtils.assertFileNotContains;
35+
36+
public class PowerShellClientCodegenTest {
37+
38+
/**
39+
* Regression test for <a href="https://github.com/OpenAPITools/openapi-generator/issues/23535">#23535</a>.
40+
*
41+
* PowerShell treats {@code $} inside double-quoted strings as the sigil for
42+
* variable interpolation, so hash-table keys emitted as {@code "$foo"} get
43+
* rewritten at runtime to the value of {@code $foo} (usually empty). This
44+
* produces invalid DTO commandlets whenever an OpenAPI property name starts
45+
* with (or contains) {@code $}. The {@code model_simple.mustache} template
46+
* now emits single-quoted keys so the literal baseName is preserved.
47+
*/
48+
@Test
49+
public void dollarSignPropertyNamesAreSingleQuoted() throws IOException {
50+
File output = Files.createTempDirectory("test-powershell-23535").toFile().getCanonicalFile();
51+
output.deleteOnExit();
52+
String outputPath = output.getAbsolutePath().replace('\\', '/');
53+
54+
OpenAPI openAPI = new OpenAPIParser()
55+
.readLocation("src/test/resources/3_0/dollar-in-names-pull14359.yaml", null, new ParseOptions())
56+
.getOpenAPI();
57+
58+
PowerShellClientCodegen codegen = new PowerShellClientCodegen();
59+
codegen.setOutputDir(output.getAbsolutePath());
60+
61+
ClientOptInput input = new ClientOptInput();
62+
input.openAPI(openAPI);
63+
input.config(codegen);
64+
65+
DefaultGenerator generator = new DefaultGenerator();
66+
generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true");
67+
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false");
68+
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false");
69+
generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false");
70+
generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false");
71+
generator.opts(input).generate();
72+
73+
// The PowerShell codegen sanitises "$DollarModel$" to "DollarModel",
74+
// so the emitted model lives at src/<package>/Model/DollarModel.ps1.
75+
java.nio.file.Path dollarModelPs1 = Paths.get(outputPath + "/src/PSOpenAPITools/Model/DollarModel.ps1");
76+
77+
// Every site where model_simple.mustache emits a user-supplied baseName
78+
// must use single-quoted PowerShell strings so `$` is treated literally:
79+
//
80+
// 1. The `Initialize-…` hash literal: `'$dollarValue$' = ${DollarValue}`
81+
// 2. The JSON round-trip hash literal in `ConvertFrom-…JsonTo…`
82+
// 3. The property-allowlist array: `$AllProperties = ('$dollarValue$')`
83+
// 4. The property-indexer lookup: `$JsonParameters.PSobject.Properties['$dollarValue$'].value`
84+
// 5. The presence-check regex: `-match '$dollarValue$'`
85+
assertFileContains(dollarModelPs1,
86+
"'$dollarValue$' = ${DollarValue}",
87+
"$AllProperties = ('$dollarValue$')",
88+
"$JsonParameters.PSobject.Properties['$dollarValue$'].value",
89+
"-match '$dollarValue$'");
90+
91+
// The previous double-quoted emissions must no longer appear anywhere in
92+
// the generated model file.
93+
assertFileNotContains(dollarModelPs1,
94+
"\"$dollarValue$\" = ",
95+
"$AllProperties = (\"$dollarValue$\")",
96+
"$JsonParameters.PSobject.Properties[\"$dollarValue$\"]",
97+
"-match \"$dollarValue$\"");
98+
}
99+
}

samples/client/echo_api/powershell/src/PSOpenAPITools/Model/Bird.ps1

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ function Initialize-Bird {
4141

4242

4343
$PSO = [PSCustomObject]@{
44-
"size" = ${Size}
45-
"color" = ${Color}
44+
'size' = ${Size}
45+
'color' = ${Color}
4646
}
4747

4848

@@ -80,28 +80,28 @@ function ConvertFrom-JsonToBird {
8080
$JsonParameters = ConvertFrom-Json -InputObject $Json
8181

8282
# check if Json contains properties not defined in Bird
83-
$AllProperties = ("size", "color")
83+
$AllProperties = ('size', 'color')
8484
foreach ($name in $JsonParameters.PsObject.Properties.Name) {
8585
if (!($AllProperties.Contains($name))) {
8686
throw "Error! JSON key '$name' not found in the properties: $($AllProperties)"
8787
}
8888
}
8989

90-
if (!([bool]($JsonParameters.PSobject.Properties.name -match "size"))) { #optional property not found
90+
if (!([bool]($JsonParameters.PSobject.Properties.name -match 'size'))) { #optional property not found
9191
$Size = $null
9292
} else {
93-
$Size = $JsonParameters.PSobject.Properties["size"].value
93+
$Size = $JsonParameters.PSobject.Properties['size'].value
9494
}
9595

96-
if (!([bool]($JsonParameters.PSobject.Properties.name -match "color"))) { #optional property not found
96+
if (!([bool]($JsonParameters.PSobject.Properties.name -match 'color'))) { #optional property not found
9797
$Color = $null
9898
} else {
99-
$Color = $JsonParameters.PSobject.Properties["color"].value
99+
$Color = $JsonParameters.PSobject.Properties['color'].value
100100
}
101101

102102
$PSO = [PSCustomObject]@{
103-
"size" = ${Size}
104-
"color" = ${Color}
103+
'size' = ${Size}
104+
'color' = ${Color}
105105
}
106106

107107
return $PSO

samples/client/echo_api/powershell/src/PSOpenAPITools/Model/Category.ps1

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ function Initialize-Category {
4141

4242

4343
$PSO = [PSCustomObject]@{
44-
"id" = ${Id}
45-
"name" = ${Name}
44+
'id' = ${Id}
45+
'name' = ${Name}
4646
}
4747

4848

@@ -80,28 +80,28 @@ function ConvertFrom-JsonToCategory {
8080
$JsonParameters = ConvertFrom-Json -InputObject $Json
8181

8282
# check if Json contains properties not defined in Category
83-
$AllProperties = ("id", "name")
83+
$AllProperties = ('id', 'name')
8484
foreach ($name in $JsonParameters.PsObject.Properties.Name) {
8585
if (!($AllProperties.Contains($name))) {
8686
throw "Error! JSON key '$name' not found in the properties: $($AllProperties)"
8787
}
8888
}
8989

90-
if (!([bool]($JsonParameters.PSobject.Properties.name -match "id"))) { #optional property not found
90+
if (!([bool]($JsonParameters.PSobject.Properties.name -match 'id'))) { #optional property not found
9191
$Id = $null
9292
} else {
93-
$Id = $JsonParameters.PSobject.Properties["id"].value
93+
$Id = $JsonParameters.PSobject.Properties['id'].value
9494
}
9595

96-
if (!([bool]($JsonParameters.PSobject.Properties.name -match "name"))) { #optional property not found
96+
if (!([bool]($JsonParameters.PSobject.Properties.name -match 'name'))) { #optional property not found
9797
$Name = $null
9898
} else {
99-
$Name = $JsonParameters.PSobject.Properties["name"].value
99+
$Name = $JsonParameters.PSobject.Properties['name'].value
100100
}
101101

102102
$PSO = [PSCustomObject]@{
103-
"id" = ${Id}
104-
"name" = ${Name}
103+
'id' = ${Id}
104+
'name' = ${Name}
105105
}
106106

107107
return $PSO

samples/client/echo_api/powershell/src/PSOpenAPITools/Model/DataQuery.ps1

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,11 @@ function Initialize-DataQuery {
5757

5858

5959
$PSO = [PSCustomObject]@{
60-
"id" = ${Id}
61-
"outcomes" = ${Outcomes}
62-
"suffix" = ${Suffix}
63-
"text" = ${Text}
64-
"date" = ${Date}
60+
'id' = ${Id}
61+
'outcomes' = ${Outcomes}
62+
'suffix' = ${Suffix}
63+
'text' = ${Text}
64+
'date' = ${Date}
6565
}
6666

6767

@@ -99,49 +99,49 @@ function ConvertFrom-JsonToDataQuery {
9999
$JsonParameters = ConvertFrom-Json -InputObject $Json
100100

101101
# check if Json contains properties not defined in DataQuery
102-
$AllProperties = ("id", "outcomes", "suffix", "text", "date")
102+
$AllProperties = ('id', 'outcomes', 'suffix', 'text', 'date')
103103
foreach ($name in $JsonParameters.PsObject.Properties.Name) {
104104
if (!($AllProperties.Contains($name))) {
105105
throw "Error! JSON key '$name' not found in the properties: $($AllProperties)"
106106
}
107107
}
108108

109-
if (!([bool]($JsonParameters.PSobject.Properties.name -match "id"))) { #optional property not found
109+
if (!([bool]($JsonParameters.PSobject.Properties.name -match 'id'))) { #optional property not found
110110
$Id = $null
111111
} else {
112-
$Id = $JsonParameters.PSobject.Properties["id"].value
112+
$Id = $JsonParameters.PSobject.Properties['id'].value
113113
}
114114

115-
if (!([bool]($JsonParameters.PSobject.Properties.name -match "outcomes"))) { #optional property not found
115+
if (!([bool]($JsonParameters.PSobject.Properties.name -match 'outcomes'))) { #optional property not found
116116
$Outcomes = $null
117117
} else {
118-
$Outcomes = $JsonParameters.PSobject.Properties["outcomes"].value
118+
$Outcomes = $JsonParameters.PSobject.Properties['outcomes'].value
119119
}
120120

121-
if (!([bool]($JsonParameters.PSobject.Properties.name -match "suffix"))) { #optional property not found
121+
if (!([bool]($JsonParameters.PSobject.Properties.name -match 'suffix'))) { #optional property not found
122122
$Suffix = $null
123123
} else {
124-
$Suffix = $JsonParameters.PSobject.Properties["suffix"].value
124+
$Suffix = $JsonParameters.PSobject.Properties['suffix'].value
125125
}
126126

127-
if (!([bool]($JsonParameters.PSobject.Properties.name -match "text"))) { #optional property not found
127+
if (!([bool]($JsonParameters.PSobject.Properties.name -match 'text'))) { #optional property not found
128128
$Text = $null
129129
} else {
130-
$Text = $JsonParameters.PSobject.Properties["text"].value
130+
$Text = $JsonParameters.PSobject.Properties['text'].value
131131
}
132132

133-
if (!([bool]($JsonParameters.PSobject.Properties.name -match "date"))) { #optional property not found
133+
if (!([bool]($JsonParameters.PSobject.Properties.name -match 'date'))) { #optional property not found
134134
$Date = $null
135135
} else {
136-
$Date = $JsonParameters.PSobject.Properties["date"].value
136+
$Date = $JsonParameters.PSobject.Properties['date'].value
137137
}
138138

139139
$PSO = [PSCustomObject]@{
140-
"id" = ${Id}
141-
"outcomes" = ${Outcomes}
142-
"suffix" = ${Suffix}
143-
"text" = ${Text}
144-
"date" = ${Date}
140+
'id' = ${Id}
141+
'outcomes' = ${Outcomes}
142+
'suffix' = ${Suffix}
143+
'text' = ${Text}
144+
'date' = ${Date}
145145
}
146146

147147
return $PSO

0 commit comments

Comments
 (0)