Skip to content

Commit 5cce27c

Browse files
committed
fix: select first supported requestBody
- instead of using the first `requestBody` `content-type`, search for the first supported one. - when none are currently supported, output a `never` type, and a `todo` comment to make this clearer to the user.
1 parent 91a9939 commit 5cce27c

14 files changed

Lines changed: 205 additions & 56 deletions

File tree

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
24.1.0
1+
24.4.1

packages/openapi-code-generator/src/core/input.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -460,12 +460,6 @@ export class Input {
460460

461461
return self.normalizeSchemaObject({...schemaObject, type: "object"})
462462
}
463-
case "any": {
464-
return {
465-
...base,
466-
type: "any",
467-
}
468-
}
469463
case "null": // TODO: HACK to support OA 3.1
470464
case "object": {
471465
if (deepEqual(schemaObject, {type: "object"})) {
@@ -591,8 +585,23 @@ export class Input {
591585
...base,
592586
type: schemaObject.type,
593587
} satisfies IRModelBoolean
588+
// custom extension types used internally
589+
case "any": {
590+
return {
591+
...base,
592+
type: "any",
593+
}
594+
}
595+
case "never": {
596+
return {
597+
...base,
598+
type: "never",
599+
}
600+
}
594601
default:
595-
throw new Error(`unsupported type '${schemaObject.type}'`)
602+
throw new Error(
603+
`unsupported type '${schemaObject.type satisfies never}'`,
604+
)
596605
}
597606

598607
function normalizeProperties(

packages/openapi-code-generator/src/core/openapi-types-normalized.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ export interface IRModelAny extends IRModelBase {
7979
type: "any"
8080
}
8181

82+
export interface IRModelNever extends IRModelBase {
83+
type: "never"
84+
}
85+
8286
export interface IRModelArray extends IRModelBase {
8387
type: "array"
8488
items: MaybeIRModel
@@ -100,6 +104,7 @@ export type IRModel =
100104
| IRModelArray
101105
| IRModelAny
102106
| IRModelNull
107+
| IRModelNever
103108

104109
export type MaybeIRModel = IRModel | IRRef
105110

packages/openapi-code-generator/src/core/openapi-types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,9 @@ export interface Schema {
217217
| "object"
218218
| "array"
219219
| "null" // only valid in OA 3.1
220-
| string
220+
// internal extension schema types
221+
| "any"
222+
| "never"
221223
| undefined
222224
not?: Schema | Reference | undefined
223225
allOf?: (Schema | Reference)[] | undefined

packages/openapi-code-generator/src/typescript/client/client-operation-builder.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,16 @@ export class ClientOperationBuilder {
131131
.map((it) => `'${it.name}': ${this.paramName(it.name)}`)
132132

133133
const hasAcceptHeader = this.hasHeader("Accept")
134-
135-
const {requestBodyContentType} = this.requestBodyAsParameter()
134+
const hasContentTypeHeader = this.hasHeader("Content-Type")
135+
const {requestBodyContentType, requestBodyParameter} =
136+
this.requestBodyAsParameter()
136137

137138
const result = [
138139
hasAcceptHeader ? undefined : "'Accept': 'application/json'",
139-
requestBodyContentType
140+
// !hasContentTypeHeader &&
141+
requestBodyContentType &&
142+
// omit Content-Type when unsupported (never)
143+
!this.models.isNever(requestBodyParameter?.schema)
140144
? `'Content-Type': '${requestBodyContentType}'`
141145
: undefined,
142146
]

packages/openapi-code-generator/src/typescript/client/typescript-angular/angular-service-builder.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ export class AngularServiceBuilder extends AbstractClientBuilder {
1717

1818
protected buildOperation(builder: ClientOperationBuilder): string {
1919
const {operationId, method, hasServers} = builder
20-
const {requestBodyParameter} = builder.requestBodyAsParameter()
20+
const {requestBodyContentType, requestBodyParameter} =
21+
builder.requestBodyAsParameter()
22+
23+
const requestBodyIsSupported = !this.types.isNever(
24+
requestBodyParameter?.schema,
25+
)
2126

2227
const operationParameter = builder.methodParameter()
2328

@@ -38,7 +43,7 @@ export class AngularServiceBuilder extends AbstractClientBuilder {
3843
${[
3944
headers ? `const headers = this._headers(${headers})` : "",
4045
queryString ? `const params = this._queryParams({${queryString}})` : "",
41-
requestBodyParameter
46+
requestBodyParameter && requestBodyIsSupported
4247
? `const body = ${builder.paramName(requestBodyParameter.name)}`
4348
: "",
4449
]
@@ -49,14 +54,18 @@ return this.httpClient.request<any>(
4954
"${method}",
5055
${hasServers ? "basePath" : "this.config.basePath"} + \`${url}\`, {
5156
${[
52-
queryString ? "params," : "",
53-
headers ? "headers," : "",
54-
requestBodyParameter ? "body," : "",
57+
queryString ? "params" : "",
58+
headers ? "headers" : "",
59+
requestBodyParameter
60+
? requestBodyIsSupported
61+
? "body"
62+
: `// todo: request bodies with content-type '${requestBodyContentType}' not yet supported`
63+
: "",
64+
'observe: "response"',
65+
"reportProgress: false",
5566
]
5667
.filter(Boolean)
57-
.join("\n")}
58-
observe: "response",
59-
reportProgress: false,
68+
.join(",\n")}
6069
});
6170
`
6271

packages/openapi-code-generator/src/typescript/client/typescript-axios/typescript-axios-client-builder.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ export class TypescriptAxiosClientBuilder extends AbstractClientBuilder {
1717

1818
protected buildOperation(builder: ClientOperationBuilder): string {
1919
const {operationId, method, hasServers} = builder
20-
const {requestBodyParameter} = builder.requestBodyAsParameter()
20+
const {requestBodyContentType, requestBodyParameter} =
21+
builder.requestBodyAsParameter()
22+
23+
const requestBodyIsSupported = !this.types.isNever(
24+
requestBodyParameter?.schema,
25+
)
2126

2227
const operationParameter = builder.methodParameter()
2328

@@ -49,7 +54,11 @@ export class TypescriptAxiosClientBuilder extends AbstractClientBuilder {
4954
const axiosFragment = `this._request({${[
5055
`url: url ${queryString ? "+ query" : ""}`,
5156
`method: "${method}"`,
52-
requestBodyParameter ? "data: body" : "",
57+
requestBodyParameter
58+
? requestBodyIsSupported
59+
? "data: body"
60+
: `// todo: request bodies with content-type '${requestBodyContentType}' not yet supported`
61+
: "",
5362
hasServers ? "baseURL: basePath" : undefined,
5463
// ensure compatibility with `exactOptionalPropertyTypes` compiler option
5564
// https://www.typescriptlang.org/tsconfig#exactOptionalPropertyTypes
@@ -67,7 +76,9 @@ export class TypescriptAxiosClientBuilder extends AbstractClientBuilder {
6776
? `const headers = this._headers(${headers}, opts.headers)`
6877
: "const headers = this._headers({}, opts.headers)",
6978
queryString ? `const query = this._query({ ${queryString} })` : "",
70-
requestBodyParameter ? "const body = JSON.stringify(p.requestBody)" : "",
79+
requestBodyParameter && requestBodyIsSupported
80+
? "const body = JSON.stringify(p.requestBody)"
81+
: "",
7182
]
7283
.filter(Boolean)
7384
.join("\n")}

packages/openapi-code-generator/src/typescript/client/typescript-fetch/typescript-fetch-client-builder.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,12 @@ export class TypescriptFetchClientBuilder extends AbstractClientBuilder {
3737

3838
protected buildOperation(builder: ClientOperationBuilder): string {
3939
const {operationId, method, hasServers} = builder
40-
const {requestBodyParameter} = builder.requestBodyAsParameter()
40+
const {requestBodyContentType, requestBodyParameter} =
41+
builder.requestBodyAsParameter()
42+
43+
const requestBodyIsSupported = !this.types.isNever(
44+
requestBodyParameter?.schema,
45+
)
4146

4247
const operationParameter = builder.methodParameter()
4348

@@ -57,13 +62,17 @@ export class TypescriptFetchClientBuilder extends AbstractClientBuilder {
5762

5863
const fetchFragment = `this._fetch(url ${queryString ? "+ query" : ""},
5964
{${[
60-
`method: "${method}",`,
61-
requestBodyParameter ? "body," : "",
62-
"...opts,",
65+
`method: "${method}"`,
66+
requestBodyParameter
67+
? requestBodyIsSupported
68+
? "body"
69+
: `// todo: request bodies with content-type '${requestBodyContentType}' not yet supported`
70+
: "",
71+
"...opts",
6372
"headers",
6473
]
6574
.filter(Boolean)
66-
.join("\n")}}, timeout)`
75+
.join(",\n")}}, timeout)`
6776

6877
const body = `
6978
const url = ${hasServers ? "basePath" : "this.basePath"} + \`${builder.routeToTemplateString()}\`
@@ -72,7 +81,9 @@ export class TypescriptFetchClientBuilder extends AbstractClientBuilder {
7281
? `const headers = this._headers(${headers}, opts.headers)`
7382
: "const headers = this._headers({}, opts.headers)",
7483
queryString ? `const query = this._query({ ${queryString} })` : "",
75-
requestBodyParameter ? "const body = JSON.stringify(p.requestBody)" : "",
84+
requestBodyParameter && requestBodyIsSupported
85+
? "const body = JSON.stringify(p.requestBody)"
86+
: "",
7687
]
7788
.filter(Boolean)
7889
.join("\n")}

packages/openapi-code-generator/src/typescript/common/schema-builders/abstract-schema-builder.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,8 +323,22 @@ export abstract class AbstractSchemaBuilder<
323323
result = this.config.allowAny ? this.any() : this.unknown()
324324
break
325325
}
326-
case "null":
326+
327+
case "never": {
328+
// todo: use z.never() ?
329+
result = `z.never()`
330+
break
331+
}
332+
333+
case "null": {
327334
throw new Error("unreachable - input should normalize this out")
335+
}
336+
337+
default: {
338+
throw new Error(
339+
`unsupported type '${JSON.stringify(model satisfies never, undefined, 2)}'`,
340+
)
341+
}
328342
}
329343

330344
if (model["x-internal-preprocess"]) {

packages/openapi-code-generator/src/typescript/common/type-builder.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -191,11 +191,6 @@ export class TypeBuilder implements ICompilable {
191191
break
192192
}
193193

194-
case "any": {
195-
result.push(this.config.allowAny ? "any" : "unknown")
196-
break
197-
}
198-
199194
case "object": {
200195
const properties = Object.entries(schemaObject.properties)
201196
.sort(([a], [b]) => (a < b ? -1 : 1))
@@ -243,9 +238,23 @@ export class TypeBuilder implements ICompilable {
243238
break
244239
}
245240

241+
case "any": {
242+
result.push(this.config.allowAny ? "any" : "unknown")
243+
break
244+
}
245+
246+
case "never": {
247+
result.push("never")
248+
break
249+
}
250+
251+
case "null": {
252+
throw new Error("unreachable - input should normalize this out")
253+
}
254+
246255
default: {
247256
throw new Error(
248-
`unsupported type '${JSON.stringify(schemaObject, undefined, 2)}'`,
257+
`unsupported type '${JSON.stringify(schemaObject satisfies never, undefined, 2)}'`,
249258
)
250259
}
251260
}
@@ -262,7 +271,7 @@ export class TypeBuilder implements ICompilable {
262271
return new CompilationUnit(this.filename, this.imports, this.toString())
263272
}
264273

265-
isEmptyObject(schemaObject: MaybeIRModel) {
274+
isEmptyObject(schemaObject: MaybeIRModel): boolean {
266275
const dereferenced = this.input.schema(schemaObject)
267276

268277
return (
@@ -274,4 +283,14 @@ export class TypeBuilder implements ICompilable {
274283
Object.keys(dereferenced.properties).length === 0
275284
)
276285
}
286+
287+
isNever(schemaObject?: MaybeIRModel): boolean {
288+
if (!schemaObject) {
289+
return false
290+
}
291+
292+
const dereferenced = this.input.schema(schemaObject)
293+
294+
return dereferenced.type === "never"
295+
}
277296
}

0 commit comments

Comments
 (0)