Skip to content

Commit a286059

Browse files
committed
Merge branch 'master' into feature/inlineOneOfWithProperties
2 parents c132be4 + 59c67f2 commit a286059

71 files changed

Lines changed: 2692 additions & 1514 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/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,6 +1401,7 @@ public ExtendedCodegenProperty(CodegenProperty cp) {
14011401
this.xmlName = cp.xmlName;
14021402
this.xmlNamespace = cp.xmlNamespace;
14031403
this.isXmlWrapped = cp.isXmlWrapped;
1404+
this.setHasSanitizedName(cp.getHasSanitizedName());
14041405
}
14051406

14061407
@Override

modules/openapi-generator/src/main/resources/csharp/HttpSigningConfiguration.mustache

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ namespace {{packageName}}.Client
2525
{
2626
HashAlgorithm = HashAlgorithmName.SHA256;
2727
SigningAlgorithm = "PKCS1-v15";
28+
_skipUrlEncode = RuntimeInformation.FrameworkDescription.StartsWith(".NET ") &&
29+
int.TryParse(RuntimeInformation.FrameworkDescription.Substring(5).Split('.')[0], out int fwMajor) && fwMajor >= 9;
2830
}
2931

3032
/// <summary>
@@ -67,6 +69,14 @@ namespace {{packageName}}.Client
6769
/// </summary>
6870
public int SignatureValidityPeriod { get; set; }
6971

72+
// On .NET 9+, HttpUtility.ParseQueryString already URL-encodes keys internally,
73+
// so calling UrlEncode again would cause double-encoding and produce a signature
74+
// that does not match the actual request sent by RestSharp 112+.
75+
// On .NET 8 and earlier, keys must be explicitly URL-encoded so that special
76+
// characters (e.g. '$' in OData params like $filter) are encoded the same way
77+
// in the signature as they are in the outgoing HTTP request.
78+
private readonly bool _skipUrlEncode;
79+
7080
private enum PrivateKeyType
7181
{
7282
None = 0,
@@ -133,8 +143,7 @@ namespace {{packageName}}.Client
133143
foreach (var parameter in requestOptions.QueryParameters)
134144
{
135145
#if (NETCOREAPP)
136-
string framework = RuntimeInformation.FrameworkDescription;
137-
string key = framework.StartsWith(".NET 9") ? parameter.Key : {{#net90OrLater}}HttpUtility.UrlEncode({{/net90OrLater}}parameter.Key{{#net90OrLater}}){{/net90OrLater}};
146+
string key = _skipUrlEncode ? parameter.Key : HttpUtility.UrlEncode(parameter.Key);
138147
if (parameter.Value.Count > 1)
139148
{ // array
140149
foreach (var value in parameter.Value)

modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/infrastructure/ApiClient.kt.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ import {{packageName}}.auth.*
196196
}
197197
this.method = requestConfig.method.httpMethod
198198
headers.filter { header -> !UNSAFE_HEADERS.contains(header.key) }.forEach { header -> this.header(header.key, header.value) }
199-
if (requestConfig.method in listOf(RequestMethod.PUT, RequestMethod.POST, RequestMethod.PATCH)) {
199+
if (requestConfig.method in listOf(RequestMethod.PUT, RequestMethod.POST, RequestMethod.PATCH, RequestMethod.DELETE)) {
200200
val contentType = (requestConfig.headers[HttpHeaders.ContentType]?.let { ContentType.parse(it) }
201201
?: ContentType.Application.Json)
202202
this.contentType(contentType)

modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/infrastructure/ApiClient.kt.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ import {{packageName}}.auth.*
166166
}
167167
this.method = requestConfig.method.httpMethod
168168
headers.filter { header -> !UNSAFE_HEADERS.contains(header.key) }.forEach { header -> this.header(header.key, header.value) }
169-
if (requestConfig.method in listOf(RequestMethod.PUT, RequestMethod.POST, RequestMethod.PATCH)) {
169+
if (requestConfig.method in listOf(RequestMethod.PUT, RequestMethod.POST, RequestMethod.PATCH, RequestMethod.DELETE)) {
170170
val contentType = (requestConfig.headers[HttpHeaders.ContentType]?.let { ContentType.parse(it) }
171171
?: ContentType.Application.Json)
172172
this.contentType(contentType)

modules/openapi-generator/src/main/resources/typescript-fetch/modelGeneric.mustache

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,30 @@ import { type {{modelName}}, {{modelName}}FromJSONTyped, {{modelName}}ToJSON, {{
2525
export function instanceOf{{classname}}(value: object): value is {{classname}} {
2626
{{#vars}}
2727
{{#required}}
28+
{{#hasSanitizedName}}
29+
if ((!('{{name}}' in value) && !('{{baseName}}' in value)) || (value['{{name}}'] === undefined && value['{{baseName}}'] === undefined)) return false;
30+
{{/hasSanitizedName}}
31+
{{^hasSanitizedName}}
2832
if (!('{{name}}' in value) || value['{{name}}'] === undefined) return false;
33+
{{/hasSanitizedName}}
34+
{{#isEnum}}
35+
{{#allowableValues}}
36+
{{#values}}
37+
{{#-first}}
38+
{{#-last}}
39+
{{#hasSanitizedName}}
40+
{{#isString}}if (value['{{name}}'] !== '{{.}}' && value['{{baseName}}'] !== '{{.}}') return false;{{/isString}}
41+
{{^isString}}if (value['{{name}}'] !== {{.}} && value['{{baseName}}'] !== {{.}}) return false;{{/isString}}
42+
{{/hasSanitizedName}}
43+
{{^hasSanitizedName}}
44+
{{#isString}}if (value['{{name}}'] !== '{{.}}') return false;{{/isString}}
45+
{{^isString}}if (value['{{name}}'] !== {{.}}) return false;{{/isString}}
46+
{{/hasSanitizedName}}
47+
{{/-last}}
48+
{{/-first}}
49+
{{/values}}
50+
{{/allowableValues}}
51+
{{/isEnum}}
2952
{{/required}}
3053
{{/vars}}
3154
return true;

modules/openapi-generator/src/main/resources/typescript-fetch/modelOneOf.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export function {{classname}}ToJSONTyped(value?: {{classname}} | null, ignoreDis
151151
switch (value['{{discriminator.propertyName}}']) {
152152
{{#discriminator.mappedModels}}
153153
case '{{mappingName}}':
154-
return Object.assign({}, {{modelName}}ToJSON(value), { {{discriminator.propertyName}}: '{{mappingName}}' } as const);
154+
return Object.assign({}, {{modelName}}ToJSON(value), { '{{discriminator.propertyBaseName}}': '{{mappingName}}' } as const);
155155
{{/discriminator.mappedModels}}
156156
default:
157157
return value;

modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,66 @@ public void testOneOfModelsImportNonPrimitiveTypes() throws IOException {
445445
TestUtils.assertFileContains(testResponse, "import type { OptionThree } from './OptionThree'");
446446
}
447447

448+
@Test(description = "Verify instanceOf checks discriminator value for single-value enums")
449+
public void testInstanceOfChecksDiscriminatorValue() throws IOException {
450+
File output = generate(Collections.emptyMap(), "src/test/resources/3_0/typescript-fetch/oneOf.yaml");
451+
452+
// OptionOne should check discriminator value
453+
Path optionOne = Paths.get(output + "/models/OptionOne.ts");
454+
TestUtils.assertFileExists(optionOne);
455+
TestUtils.assertFileContains(optionOne, "value['discriminatorField'] !== 'optionOne'");
456+
457+
// OptionTwo should check discriminator value
458+
Path optionTwo = Paths.get(output + "/models/OptionTwo.ts");
459+
TestUtils.assertFileExists(optionTwo);
460+
TestUtils.assertFileContains(optionTwo, "value['discriminatorField'] !== 'optionTwo'");
461+
462+
// TestA should NOT have a value check (foo is a plain string, not a single-value enum)
463+
Path testA = Paths.get(output + "/models/TestA.ts");
464+
TestUtils.assertFileExists(testA);
465+
TestUtils.assertFileNotContains(testA, "value['foo'] !==");
466+
467+
// SnakeOptionOne: discriminator_field (snake_case baseName) vs discriminatorField (camelCase name)
468+
// instanceOf should check both casings for field presence and discriminator value
469+
Path snakeOptionOne = Paths.get(output + "/models/SnakeOptionOne.ts");
470+
TestUtils.assertFileExists(snakeOptionOne);
471+
TestUtils.assertFileContains(snakeOptionOne, "'discriminatorField' in value");
472+
TestUtils.assertFileContains(snakeOptionOne, "'discriminator_field' in value");
473+
TestUtils.assertFileContains(snakeOptionOne, "value['discriminatorField'] !== 'snakeOptionOne'");
474+
TestUtils.assertFileContains(snakeOptionOne, "value['discriminator_field'] !== 'snakeOptionOne'");
475+
// Also verify the non-enum required field checks both casings
476+
TestUtils.assertFileContains(snakeOptionOne, "'someProperty' in value");
477+
TestUtils.assertFileContains(snakeOptionOne, "'some_property' in value");
478+
479+
// DashedOptionOne: discriminator-field (dashed baseName) vs discriminatorField (camelCase name)
480+
Path dashedOptionOne = Paths.get(output + "/models/DashedOptionOne.ts");
481+
TestUtils.assertFileExists(dashedOptionOne);
482+
TestUtils.assertFileContains(dashedOptionOne, "'discriminatorField' in value");
483+
TestUtils.assertFileContains(dashedOptionOne, "'discriminator-field' in value");
484+
TestUtils.assertFileContains(dashedOptionOne, "value['discriminatorField'] !== 'dashedOptionOne'");
485+
TestUtils.assertFileContains(dashedOptionOne, "value['discriminator-field'] !== 'dashedOptionOne'");
486+
TestUtils.assertFileContains(dashedOptionOne, "'someProperty' in value");
487+
TestUtils.assertFileContains(dashedOptionOne, "'some-property' in value");
488+
489+
// Numeric singleton enum: value check must NOT quote the literal
490+
Path numericModel = Paths.get(output + "/models/NumericSingletonEnumModel.ts");
491+
TestUtils.assertFileExists(numericModel);
492+
TestUtils.assertFileContains(numericModel, "value['kind'] !== 42");
493+
TestUtils.assertFileNotContains(numericModel, "value['kind'] !== '42'");
494+
495+
// ToJSONTyped of discriminated oneOf must emit the wire-format discriminator key
496+
// (propertyBaseName), not the camelCase TS property name
497+
Path dashedDiscriminatorResponse = Paths.get(output + "/models/TestDashedDiscriminatorResponse.ts");
498+
TestUtils.assertFileExists(dashedDiscriminatorResponse);
499+
TestUtils.assertFileContains(dashedDiscriminatorResponse, "{ 'discriminator-field': 'dashedOptionOne' }");
500+
TestUtils.assertFileContains(dashedDiscriminatorResponse, "{ 'discriminator-field': 'dashedOptionTwo' }");
501+
502+
Path snakeDiscriminatorResponse = Paths.get(output + "/models/TestSnakeCaseDiscriminatorResponse.ts");
503+
TestUtils.assertFileExists(snakeDiscriminatorResponse);
504+
TestUtils.assertFileContains(snakeDiscriminatorResponse, "{ 'discriminator_field': 'snakeOptionOne' }");
505+
TestUtils.assertFileContains(snakeDiscriminatorResponse, "{ 'discriminator_field': 'snakeOptionTwo' }");
506+
}
507+
448508
@Test(description = "Verify validationAttributes works with withoutRuntimeChecks=true")
449509
public void testValidationAttributesWithWithoutRuntimeChecks() throws IOException {
450510
Map<String, Object> properties = new HashMap<>();

modules/openapi-generator/src/test/resources/3_0/typescript-fetch/oneOf.yaml

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,26 @@ paths:
3535
application/json:
3636
schema:
3737
$ref: '#/components/schemas/TestDiscriminatorResponse'
38+
/test-snake-case-discriminator:
39+
get:
40+
operationId: testSnakeCaseDiscriminator
41+
responses:
42+
200:
43+
description: OK
44+
content:
45+
application/json:
46+
schema:
47+
$ref: '#/components/schemas/TestSnakeCaseDiscriminatorResponse'
48+
/test-dashed-discriminator:
49+
get:
50+
operationId: testDashedDiscriminator
51+
responses:
52+
200:
53+
description: OK
54+
content:
55+
application/json:
56+
schema:
57+
$ref: '#/components/schemas/TestDashedDiscriminatorResponse'
3858
components:
3959
schemas:
4060
TestArrayResponse:
@@ -93,4 +113,79 @@ components:
93113
- "optionTwo"
94114
type: string
95115
required:
96-
- discriminatorField
116+
- discriminatorField
117+
NumericSingletonEnumModel:
118+
type: object
119+
properties:
120+
kind:
121+
type: integer
122+
enum:
123+
- 42
124+
required:
125+
- kind
126+
TestSnakeCaseDiscriminatorResponse:
127+
discriminator:
128+
propertyName: discriminator_field
129+
mapping:
130+
snakeOptionOne: "#/components/schemas/SnakeOptionOne"
131+
snakeOptionTwo: "#/components/schemas/SnakeOptionTwo"
132+
oneOf:
133+
- $ref: "#/components/schemas/SnakeOptionOne"
134+
- $ref: "#/components/schemas/SnakeOptionTwo"
135+
SnakeOptionOne:
136+
type: object
137+
properties:
138+
discriminator_field:
139+
enum:
140+
- "snakeOptionOne"
141+
type: string
142+
some_property:
143+
type: string
144+
required:
145+
- discriminator_field
146+
- some_property
147+
SnakeOptionTwo:
148+
type: object
149+
properties:
150+
discriminator_field:
151+
enum:
152+
- "snakeOptionTwo"
153+
type: string
154+
some_property:
155+
type: string
156+
required:
157+
- discriminator_field
158+
- some_property
159+
TestDashedDiscriminatorResponse:
160+
discriminator:
161+
propertyName: discriminator-field
162+
mapping:
163+
dashedOptionOne: "#/components/schemas/DashedOptionOne"
164+
dashedOptionTwo: "#/components/schemas/DashedOptionTwo"
165+
oneOf:
166+
- $ref: "#/components/schemas/DashedOptionOne"
167+
- $ref: "#/components/schemas/DashedOptionTwo"
168+
DashedOptionOne:
169+
type: object
170+
properties:
171+
discriminator-field:
172+
enum:
173+
- "dashedOptionOne"
174+
type: string
175+
some-property:
176+
type: string
177+
required:
178+
- discriminator-field
179+
- some-property
180+
DashedOptionTwo:
181+
type: object
182+
properties:
183+
discriminator-field:
184+
enum:
185+
- "dashedOptionTwo"
186+
type: string
187+
some-property:
188+
type: string
189+
required:
190+
- discriminator-field
191+
- some-property

samples/client/others/typescript-angular-v20/package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)