diff --git a/packages/openapi-code-generator/src/core/input.ts b/packages/openapi-code-generator/src/core/input.ts index 47634631..88e8e23d 100644 --- a/packages/openapi-code-generator/src/core/input.ts +++ b/packages/openapi-code-generator/src/core/input.ts @@ -10,6 +10,7 @@ import type { Responses, Schema, Server, + Style, xInternalPreproccess, } from "./openapi-types" import type { @@ -22,6 +23,11 @@ import type { IRModelString, IROperation, IRParameter, + IRParameterBase, + IRParameterCookie, + IRParameterHeader, + IRParameterPath, + IRParameterQuery, IRPreprocess, IRRef, IRResponse, @@ -333,18 +339,142 @@ export class Input { return parameters .map((it) => this.loader.parameter(it)) .map((it: Parameter): IRParameter => { - return { + const base = { name: it.name, - in: it.in, schema: this.normalizeSchemaObject(it.schema), description: it.description, required: it.required ?? false, deprecated: it.deprecated ?? false, - allowEmptyValue: it.allowEmptyValue ?? false, + } satisfies Omit + + function throwUnsupportedStyle(style: Style): never { + throw new Error( + `unsupported parameter style: '${style}' for in: '${it.in}'`, + ) + } + + switch (it.in) { + case "path": { + const style = it.style ?? "simple" + const explode = this.explodeForParameter(it, style) + + if (!this.isStyleForPathParameter(style)) { + throwUnsupportedStyle(style) + } + + return { + ...base, + in: "path", + style, + explode, + } satisfies IRParameterPath + } + + case "query": { + const style = it.style ?? "form" + const explode = this.explodeForParameter(it, style) + + if (!this.isStyleForQueryParameter(style)) { + throwUnsupportedStyle(style) + } + + return { + ...base, + in: "query", + style, + explode, + allowEmptyValue: it.allowEmptyValue ?? false, + } satisfies IRParameterQuery + } + + case "header": { + const style = it.style ?? "simple" + const explode = this.explodeForParameter(it, style) + + if (!this.isStyleForHeaderParameter(style)) { + throwUnsupportedStyle(style) + } + + return { + ...base, + in: "header", + style, + explode, + } satisfies IRParameterHeader + } + + case "cookie": { + const style = it.style ?? "form" + const explode = this.explodeForParameter(it, style) + + if (!this.isStyleForCookieParameter(style)) { + throwUnsupportedStyle(style) + } + + return { + ...base, + in: "cookie", + style, + explode, + } satisfies IRParameterCookie + } + + default: { + throw new Error( + `unsupported parameter location: '${it.in satisfies never}'`, + ) + } } }) } + private isStyleForPathParameter( + style: Style, + ): style is IRParameterPath["style"] { + return ["simple", "label", "matrix", "template"].includes(style) + } + + private isStyleForQueryParameter( + style: Style, + ): style is IRParameterQuery["style"] { + return ["form", "spaceDelimited", "pipeDelimited", "deepObject"].includes( + style, + ) + } + + private isStyleForHeaderParameter( + style: Style, + ): style is IRParameterHeader["style"] { + return ["simple"].includes(style) + } + + private isStyleForCookieParameter( + style: Style, + ): style is IRParameterCookie["style"] { + if (style === "cookie") { + // todo: openapi v3.2.0 + throw new Error("support for style: cookie not implemented.") + } + + return ["form"].includes(style) + } + + private explodeForParameter(parameter: Parameter, style: Style): boolean { + if (typeof parameter.explode === "boolean") { + return parameter.explode + } + + /** + * "When style is "form" or "cookie", the default value is true. For all other styles, the default value is false." + * ref: {@link https://spec.openapis.org/oas/v3.2.0.html#parameter-explode} + */ + if (style === "form" || style === "cookie") { + return true + } + + return false + } + private normalizeOperationId( operationId: string | undefined, method: string, diff --git a/packages/openapi-code-generator/src/core/openapi-types-normalized.ts b/packages/openapi-code-generator/src/core/openapi-types-normalized.ts index ea428ca2..3700f5b5 100644 --- a/packages/openapi-code-generator/src/core/openapi-types-normalized.ts +++ b/packages/openapi-code-generator/src/core/openapi-types-normalized.ts @@ -123,16 +123,52 @@ export interface IRServerVariable { description: string | undefined } -export interface IRParameter { +export interface IRParameterBase { name: string - in: "path" | "query" | "header" | "cookie" | "body" - schema: MaybeIRModel description: string | undefined required: boolean deprecated: boolean - allowEmptyValue: boolean + schema: MaybeIRModel + explode: boolean | undefined +} + +export interface IRParameterPath extends IRParameterBase { + in: "path" + // todo: matrix/label not supported + style: "matrix" | "label" | "simple" } +export interface IRParameterQuery extends IRParameterBase { + in: "query" + style: "form" | "spaceDelimited" | "pipeDelimited" | "deepObject" // default: form + explode: boolean | undefined // default: true for form/cookie, false for other styles + allowEmptyValue: boolean //default: false +} + +export interface IRParameterHeader extends IRParameterBase { + in: "header" + style: "simple" +} + +export interface IRParameterCookie extends IRParameterBase { + in: "cookie" + style: "form" + // todo: openapi v3.2.0 - support style: "cookie" + // | "cookie" +} + +// note: not part of spec, but used internally +export interface IRParameterRequestBody extends IRParameterBase { + in: "body" +} + +export type IRParameter = + | IRParameterPath + | IRParameterQuery + | IRParameterHeader + | IRParameterCookie + | IRParameterRequestBody + export interface IROperation { route: string method: HttpMethod diff --git a/packages/openapi-code-generator/src/core/openapi-types.ts b/packages/openapi-code-generator/src/core/openapi-types.ts index 5a7b36f7..f1485b2c 100644 --- a/packages/openapi-code-generator/src/core/openapi-types.ts +++ b/packages/openapi-code-generator/src/core/openapi-types.ts @@ -152,6 +152,7 @@ export type Style = | "pipeDelimited" | "simple" | "spaceDelimited" + | "cookie" export interface Encoding { allowReserved?: boolean @@ -202,7 +203,15 @@ export interface Header { export interface Parameter { name: string in: "path" | "query" | "header" | "cookie" + // todo: openapi v3.2.0 - support querystring + // | "querystring" schema: Schema | Reference + // todo: support content on parameters + // content?: { + // [contentType: string]: MediaType + // } + style?: Style + explode?: boolean description?: string required?: boolean deprecated?: boolean diff --git a/packages/openapi-code-generator/src/typescript/common/typescript-common.ts b/packages/openapi-code-generator/src/typescript/common/typescript-common.ts index 03fc1918..b7dd8083 100644 --- a/packages/openapi-code-generator/src/typescript/common/typescript-common.ts +++ b/packages/openapi-code-generator/src/typescript/common/typescript-common.ts @@ -3,7 +3,7 @@ import type {Encoding} from "../../core/openapi-types" import type { IRMediaType, IROperation, - IRParameter, + IRParameterRequestBody, } from "../../core/openapi-types-normalized" import {isDefined} from "../../core/utils" @@ -148,7 +148,7 @@ export type Serializer = "JSON.stringify" | "String" | "URLSearchParams" export type RequestBodyAsParameter = { isSupported: boolean - parameter: IRParameter + parameter: IRParameterRequestBody contentType: string serializer: Serializer | undefined encoding?: Record @@ -249,8 +249,8 @@ export function requestBodyAsParameter( description: requestBody.description, in: "body", required: requestBody.required, + explode: undefined, schema: result.mediaType.schema, - allowEmptyValue: false, deprecated: false, }, serializer: result.serializer, @@ -276,8 +276,8 @@ export function requestBodyAsParameter( description: requestBody.description, in: "body", required: requestBody.required, + explode: undefined, schema: {type: "never", nullable: false, readOnly: false}, - allowEmptyValue: false, deprecated: false, }, serializer: undefined, diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 16470c1f..dfd78b34 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -13,6 +13,7 @@ ignoredBuiltDependencies: - '@parcel/watcher' - '@swc/core' - esbuild + - lmdb - nx - sharp - unrs-resolver