Skip to content

Commit 70d0f9a

Browse files
authored
refactor: allow narrowing IRParameter into more specific types (#385)
will make it easier to complete #381 by avoiding irrelevant `style` values needing to be considered / filtered.
1 parent acb7b42 commit 70d0f9a

File tree

5 files changed

+187
-11
lines changed

5 files changed

+187
-11
lines changed

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

Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
Responses,
1111
Schema,
1212
Server,
13+
Style,
1314
xInternalPreproccess,
1415
} from "./openapi-types"
1516
import type {
@@ -22,6 +23,11 @@ import type {
2223
IRModelString,
2324
IROperation,
2425
IRParameter,
26+
IRParameterBase,
27+
IRParameterCookie,
28+
IRParameterHeader,
29+
IRParameterPath,
30+
IRParameterQuery,
2531
IRPreprocess,
2632
IRRef,
2733
IRResponse,
@@ -333,18 +339,142 @@ export class Input {
333339
return parameters
334340
.map((it) => this.loader.parameter(it))
335341
.map((it: Parameter): IRParameter => {
336-
return {
342+
const base = {
337343
name: it.name,
338-
in: it.in,
339344
schema: this.normalizeSchemaObject(it.schema),
340345
description: it.description,
341346
required: it.required ?? false,
342347
deprecated: it.deprecated ?? false,
343-
allowEmptyValue: it.allowEmptyValue ?? false,
348+
} satisfies Omit<IRParameterBase, "explode">
349+
350+
function throwUnsupportedStyle(style: Style): never {
351+
throw new Error(
352+
`unsupported parameter style: '${style}' for in: '${it.in}'`,
353+
)
354+
}
355+
356+
switch (it.in) {
357+
case "path": {
358+
const style = it.style ?? "simple"
359+
const explode = this.explodeForParameter(it, style)
360+
361+
if (!this.isStyleForPathParameter(style)) {
362+
throwUnsupportedStyle(style)
363+
}
364+
365+
return {
366+
...base,
367+
in: "path",
368+
style,
369+
explode,
370+
} satisfies IRParameterPath
371+
}
372+
373+
case "query": {
374+
const style = it.style ?? "form"
375+
const explode = this.explodeForParameter(it, style)
376+
377+
if (!this.isStyleForQueryParameter(style)) {
378+
throwUnsupportedStyle(style)
379+
}
380+
381+
return {
382+
...base,
383+
in: "query",
384+
style,
385+
explode,
386+
allowEmptyValue: it.allowEmptyValue ?? false,
387+
} satisfies IRParameterQuery
388+
}
389+
390+
case "header": {
391+
const style = it.style ?? "simple"
392+
const explode = this.explodeForParameter(it, style)
393+
394+
if (!this.isStyleForHeaderParameter(style)) {
395+
throwUnsupportedStyle(style)
396+
}
397+
398+
return {
399+
...base,
400+
in: "header",
401+
style,
402+
explode,
403+
} satisfies IRParameterHeader
404+
}
405+
406+
case "cookie": {
407+
const style = it.style ?? "form"
408+
const explode = this.explodeForParameter(it, style)
409+
410+
if (!this.isStyleForCookieParameter(style)) {
411+
throwUnsupportedStyle(style)
412+
}
413+
414+
return {
415+
...base,
416+
in: "cookie",
417+
style,
418+
explode,
419+
} satisfies IRParameterCookie
420+
}
421+
422+
default: {
423+
throw new Error(
424+
`unsupported parameter location: '${it.in satisfies never}'`,
425+
)
426+
}
344427
}
345428
})
346429
}
347430

431+
private isStyleForPathParameter(
432+
style: Style,
433+
): style is IRParameterPath["style"] {
434+
return ["simple", "label", "matrix", "template"].includes(style)
435+
}
436+
437+
private isStyleForQueryParameter(
438+
style: Style,
439+
): style is IRParameterQuery["style"] {
440+
return ["form", "spaceDelimited", "pipeDelimited", "deepObject"].includes(
441+
style,
442+
)
443+
}
444+
445+
private isStyleForHeaderParameter(
446+
style: Style,
447+
): style is IRParameterHeader["style"] {
448+
return ["simple"].includes(style)
449+
}
450+
451+
private isStyleForCookieParameter(
452+
style: Style,
453+
): style is IRParameterCookie["style"] {
454+
if (style === "cookie") {
455+
// todo: openapi v3.2.0
456+
throw new Error("support for style: cookie not implemented.")
457+
}
458+
459+
return ["form"].includes(style)
460+
}
461+
462+
private explodeForParameter(parameter: Parameter, style: Style): boolean {
463+
if (typeof parameter.explode === "boolean") {
464+
return parameter.explode
465+
}
466+
467+
/**
468+
* "When style is "form" or "cookie", the default value is true. For all other styles, the default value is false."
469+
* ref: {@link https://spec.openapis.org/oas/v3.2.0.html#parameter-explode}
470+
*/
471+
if (style === "form" || style === "cookie") {
472+
return true
473+
}
474+
475+
return false
476+
}
477+
348478
private normalizeOperationId(
349479
operationId: string | undefined,
350480
method: string,

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

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,16 +123,52 @@ export interface IRServerVariable {
123123
description: string | undefined
124124
}
125125

126-
export interface IRParameter {
126+
export interface IRParameterBase {
127127
name: string
128-
in: "path" | "query" | "header" | "cookie" | "body"
129-
schema: MaybeIRModel
130128
description: string | undefined
131129
required: boolean
132130
deprecated: boolean
133-
allowEmptyValue: boolean
131+
schema: MaybeIRModel
132+
explode: boolean | undefined
133+
}
134+
135+
export interface IRParameterPath extends IRParameterBase {
136+
in: "path"
137+
// todo: matrix/label not supported
138+
style: "matrix" | "label" | "simple"
134139
}
135140

141+
export interface IRParameterQuery extends IRParameterBase {
142+
in: "query"
143+
style: "form" | "spaceDelimited" | "pipeDelimited" | "deepObject" // default: form
144+
explode: boolean | undefined // default: true for form/cookie, false for other styles
145+
allowEmptyValue: boolean //default: false
146+
}
147+
148+
export interface IRParameterHeader extends IRParameterBase {
149+
in: "header"
150+
style: "simple"
151+
}
152+
153+
export interface IRParameterCookie extends IRParameterBase {
154+
in: "cookie"
155+
style: "form"
156+
// todo: openapi v3.2.0 - support style: "cookie"
157+
// | "cookie"
158+
}
159+
160+
// note: not part of spec, but used internally
161+
export interface IRParameterRequestBody extends IRParameterBase {
162+
in: "body"
163+
}
164+
165+
export type IRParameter =
166+
| IRParameterPath
167+
| IRParameterQuery
168+
| IRParameterHeader
169+
| IRParameterCookie
170+
| IRParameterRequestBody
171+
136172
export interface IROperation {
137173
route: string
138174
method: HttpMethod

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ export type Style =
152152
| "pipeDelimited"
153153
| "simple"
154154
| "spaceDelimited"
155+
| "cookie"
155156

156157
export interface Encoding {
157158
allowReserved?: boolean
@@ -202,7 +203,15 @@ export interface Header {
202203
export interface Parameter {
203204
name: string
204205
in: "path" | "query" | "header" | "cookie"
206+
// todo: openapi v3.2.0 - support querystring
207+
// | "querystring"
205208
schema: Schema | Reference
209+
// todo: support content on parameters
210+
// content?: {
211+
// [contentType: string]: MediaType
212+
// }
213+
style?: Style
214+
explode?: boolean
206215
description?: string
207216
required?: boolean
208217
deprecated?: boolean

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type {Encoding} from "../../core/openapi-types"
33
import type {
44
IRMediaType,
55
IROperation,
6-
IRParameter,
6+
IRParameterRequestBody,
77
} from "../../core/openapi-types-normalized"
88
import {isDefined} from "../../core/utils"
99

@@ -148,7 +148,7 @@ export type Serializer = "JSON.stringify" | "String" | "URLSearchParams"
148148

149149
export type RequestBodyAsParameter = {
150150
isSupported: boolean
151-
parameter: IRParameter
151+
parameter: IRParameterRequestBody
152152
contentType: string
153153
serializer: Serializer | undefined
154154
encoding?: Record<string, Encoding>
@@ -249,8 +249,8 @@ export function requestBodyAsParameter(
249249
description: requestBody.description,
250250
in: "body",
251251
required: requestBody.required,
252+
explode: undefined,
252253
schema: result.mediaType.schema,
253-
allowEmptyValue: false,
254254
deprecated: false,
255255
},
256256
serializer: result.serializer,
@@ -276,8 +276,8 @@ export function requestBodyAsParameter(
276276
description: requestBody.description,
277277
in: "body",
278278
required: requestBody.required,
279+
explode: undefined,
279280
schema: {type: "never", nullable: false, readOnly: false},
280-
allowEmptyValue: false,
281281
deprecated: false,
282282
},
283283
serializer: undefined,

pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ ignoredBuiltDependencies:
1313
- '@parcel/watcher'
1414
- '@swc/core'
1515
- esbuild
16+
- lmdb
1617
- nx
1718
- sharp
1819
- unrs-resolver

0 commit comments

Comments
 (0)