From 4f0263e0f3c4205b22e4d511b03fc43292de6743 Mon Sep 17 00:00:00 2001 From: Lubos Date: Sun, 20 Oct 2024 23:33:56 +0800 Subject: [PATCH] feat: use new parser for services --- packages/client-fetch/src/utils.ts | 1 + packages/openapi-ts/src/generate/services.ts | 425 +++++++++++++++--- packages/openapi-ts/src/index.ts | 2 +- packages/openapi-ts/src/ir/ir.d.ts | 3 + packages/openapi-ts/src/ir/mediaType.ts | 27 ++ packages/openapi-ts/src/ir/operation.ts | 16 + .../src/openApi/3.1.0/parser/mediaType.ts | 40 +- .../src/openApi/3.1.0/parser/operation.ts | 11 +- .../src/openApi/3.1.0/parser/parameter.ts | 4 +- .../src/openApi/3.1.0/parser/schema.ts | 21 +- .../plugins/@tanstack/query-core/plugin.ts | 20 +- packages/openapi-ts/src/types/config.ts | 12 +- packages/openapi-ts/src/utils/type.ts | 2 +- packages/openapi-ts/test/3.1.0.spec.ts | 11 + .../3.1.0/object-properties-any-of/index.ts | 2 + .../object-properties-any-of/types.gen.ts | 13 + .../client/utils.ts.snap | 1 + .../client/utils.ts.snap | 1 + packages/openapi-ts/test/sample.cjs | 2 +- packages/openapi-ts/test/spec/3.1.0/full.json | 16 +- .../spec/3.1.0/object-properties-any-of.json | 28 ++ 21 files changed, 536 insertions(+), 122 deletions(-) create mode 100644 packages/openapi-ts/src/ir/mediaType.ts create mode 100644 packages/openapi-ts/src/ir/operation.ts create mode 100644 packages/openapi-ts/test/__snapshots__/3.1.0/object-properties-any-of/index.ts create mode 100644 packages/openapi-ts/test/__snapshots__/3.1.0/object-properties-any-of/types.gen.ts create mode 100644 packages/openapi-ts/test/spec/3.1.0/object-properties-any-of.json diff --git a/packages/client-fetch/src/utils.ts b/packages/client-fetch/src/utils.ts index 8e5185670..2f0b29739 100644 --- a/packages/client-fetch/src/utils.ts +++ b/packages/client-fetch/src/utils.ts @@ -332,6 +332,7 @@ export const getParseAs = ( return; } + // TODO: parser - better detection of MIME types if (content.startsWith('application/json') || content.endsWith('+json')) { return 'json'; } diff --git a/packages/openapi-ts/src/generate/services.ts b/packages/openapi-ts/src/generate/services.ts index c3c135e2e..69e6ffd1d 100644 --- a/packages/openapi-ts/src/generate/services.ts +++ b/packages/openapi-ts/src/generate/services.ts @@ -1,3 +1,5 @@ +import type ts from 'typescript'; + import type { ClassElement, Comments, @@ -12,6 +14,7 @@ import type { IRPathItemObject, IRPathsObject, } from '../ir/ir'; +import { hasOperationDataRequired } from '../ir/operation'; import { isOperationParameterRequired } from '../openApi'; import type { Client, @@ -25,6 +28,7 @@ import type { Files } from '../types/utils'; import { camelCase } from '../utils/camelCase'; import { getConfig, isLegacyClient } from '../utils/config'; import { escapeComment, escapeName } from '../utils/escape'; +import { getServiceName } from '../utils/postprocess'; import { reservedWordsRegExp } from '../utils/regexp'; import { transformServiceName } from '../utils/transform'; import { setUniqueTypeName } from '../utils/type'; @@ -135,10 +139,13 @@ export const operationResponseTypeName = (name: string) => * @param importedType unique type name returned from `setUniqueTypeName()` * @returns options type */ -export const operationOptionsType = ( - importedType?: string, - throwOnError?: string, -) => { +export const operationOptionsType = ({ + importedType, + throwOnError, +}: { + importedType?: string; + throwOnError?: string; +}) => { const optionsName = clientOptionsTypeName(); // TODO: refactor this to be more generic, works for now if (throwOnError) { @@ -171,7 +178,10 @@ const toOperationParamType = ( { isRequired, name: 'options', - type: operationOptionsType(importedType, 'ThrowOnError'), + type: operationOptionsType({ + importedType, + throwOnError: 'ThrowOnError', + }), }, ]; } @@ -514,7 +524,7 @@ const toRequestOptions = ( }); }; -export const toOperationName = ({ +export const serviceFunctionIdentifier = ({ config, id, operation, @@ -707,7 +717,7 @@ const processService = ({ comment: toOperationComment(operation), exportConst: true, expression, - name: toOperationName({ + name: serviceFunctionIdentifier({ config, handleIllegal: true, id: operation.name, @@ -725,7 +735,7 @@ const processService = ({ comment: toOperationComment(operation), isStatic: config.name === undefined && config.client.name !== 'legacy/angular', - name: toOperationName({ + name: serviceFunctionIdentifier({ config, id: operation.name, operation, @@ -961,6 +971,347 @@ export const generateLegacyServices = async ({ } }; +const requestOptions = ({ + context, + operation, + path, +}: { + context: IRContext; + operation: IROperationObject; + path: string; +}) => { + const file = context.file({ id: servicesId })!; + const servicesOutput = file.getName(false); + // const typesModule = `./${context.file({ id: 'types' })!.getName(false)}` + + // TODO: parser - add response transformers + // const operationName = operationResponseTypeName(operation.name); + // const { name: responseTransformerName } = setUniqueTypeName({ + // client, + // meta: { + // $ref: `transformers/${operationName}`, + // name: operationName, + // }, + // nameTransformer: operationResponseTransformerTypeName, + // }); + + // if (responseTransformerName) { + // file.import({ + // // this detection could be done safer, but it shouldn't cause any issues + // asType: !responseTransformerName.endsWith('Transformer'), + // module: typesModule, + // name: responseTransformerName, + // }); + // } + + const obj: ObjectValue[] = [{ spread: 'options' }]; + + if (operation.body) { + switch (operation.body.type) { + case 'form-data': + obj.push({ spread: 'formDataBodySerializer' }); + file.import({ + module: clientModulePath({ + config: context.config, + sourceOutput: servicesOutput, + }), + name: 'formDataBodySerializer', + }); + break; + case 'json': + break; + case 'url-search-params': + obj.push({ spread: 'urlSearchParamsBodySerializer' }); + file.import({ + module: clientModulePath({ + config: context.config, + sourceOutput: servicesOutput, + }), + name: 'urlSearchParamsBodySerializer', + }); + break; + } + + obj.push({ + key: 'headers', + value: [ + { + key: 'Content-Type', + // form-data does not need Content-Type header, browser will set it automatically + value: + operation.body.type === 'form-data' + ? null + : operation.body.mediaType, + }, + { + spread: 'options?.headers', + }, + ], + }); + } + + // TODO: parser - set parseAs to skip inference if every response has the same + // content type. currently impossible because successes do not contain + // header information + + obj.push({ + key: 'url', + value: path, + }); + + // TODO: parser - add response transformers + // if (responseTransformerName) { + // obj = [ + // ...obj, + // { + // key: 'responseTransformer', + // value: responseTransformerName, + // }, + // ]; + // } + + return compiler.objectExpression({ + identifiers: ['responseTransformer'], + obj, + }); +}; + +const generateClassServices = ({ context }: { context: IRContext }) => { + const file = context.file({ id: servicesId })!; + const typesModule = `./${context.file({ id: 'types' })!.getName(false)}`; + + const services = new Map>(); + + for (const path in context.ir.paths) { + const pathItem = context.ir.paths[path as keyof IRPathsObject]; + + for (const _method in pathItem) { + const method = _method as keyof IRPathItemObject; + const operation = pathItem[method]!; + + const identifierData = context.file({ id: 'types' })!.identifier({ + $ref: operationDataRef({ id: operation.id }), + namespace: 'type', + }); + if (identifierData.name) { + file.import({ + // this detection could be done safer, but it shouldn't cause any issues + asType: !identifierData.name.endsWith('Transformer'), + module: typesModule, + name: identifierData.name, + }); + } + + const identifierError = context.file({ id: 'types' })!.identifier({ + $ref: operationErrorRef({ id: operation.id }), + namespace: 'type', + }); + if (identifierError.name) { + file.import({ + // this detection could be done safer, but it shouldn't cause any issues + asType: !identifierError.name.endsWith('Transformer'), + module: typesModule, + name: identifierError.name, + }); + } + + const identifierResponse = context.file({ id: 'types' })!.identifier({ + $ref: operationResponseRef({ id: operation.id }), + namespace: 'type', + }); + if (identifierResponse.name) { + file.import({ + // this detection could be done safer, but it shouldn't cause any issues + asType: !identifierResponse.name.endsWith('Transformer'), + module: typesModule, + name: identifierResponse.name, + }); + } + + const node = compiler.methodDeclaration({ + accessLevel: 'public', + comment: [ + operation.deprecated && '@deprecated', + operation.summary && escapeComment(operation.summary), + operation.description && escapeComment(operation.description), + ], + isStatic: true, + name: serviceFunctionIdentifier({ + config: context.config, + handleIllegal: false, + id: operation.id, + operation, + }), + parameters: [ + { + isRequired: hasOperationDataRequired(operation), + name: 'options', + type: operationOptionsType({ + importedType: identifierData.name, + throwOnError: 'ThrowOnError', + }), + }, + ], + returnType: undefined, + statements: [ + compiler.returnFunctionCall({ + args: [ + requestOptions({ + context, + operation, + path, + }), + ], + name: `(options?.client ?? client).${method}`, + types: [ + identifierResponse.name || 'unknown', + identifierError.name || 'unknown', + 'ThrowOnError', + ], + }), + ], + types: [ + { + default: false, + extends: 'boolean', + name: 'ThrowOnError', + }, + ], + }); + + const uniqueTags = Array.from(new Set(operation.tags)); + if (!uniqueTags.length) { + uniqueTags.push('default'); + } + + for (const tag of uniqueTags) { + const serviceName = getServiceName(tag); + const nodes = services.get(serviceName) ?? []; + nodes.push(node); + services.set(serviceName, nodes); + } + } + } + + for (const [serviceName, nodes] of services) { + const node = compiler.classDeclaration({ + decorator: undefined, + members: nodes, + name: transformServiceName({ + config: context.config, + name: serviceName, + }), + }); + file.add(node); + } +}; + +const generateFlatServices = ({ context }: { context: IRContext }) => { + const file = context.file({ id: servicesId })!; + const typesModule = `./${context.file({ id: 'types' })!.getName(false)}`; + + for (const path in context.ir.paths) { + const pathItem = context.ir.paths[path as keyof IRPathsObject]; + + for (const _method in pathItem) { + const method = _method as keyof IRPathItemObject; + const operation = pathItem[method]!; + + const identifierData = context.file({ id: 'types' })!.identifier({ + $ref: operationDataRef({ id: operation.id }), + namespace: 'type', + }); + if (identifierData.name) { + file.import({ + // this detection could be done safer, but it shouldn't cause any issues + asType: !identifierData.name.endsWith('Transformer'), + module: typesModule, + name: identifierData.name, + }); + } + + const identifierError = context.file({ id: 'types' })!.identifier({ + $ref: operationErrorRef({ id: operation.id }), + namespace: 'type', + }); + if (identifierError.name) { + file.import({ + // this detection could be done safer, but it shouldn't cause any issues + asType: !identifierError.name.endsWith('Transformer'), + module: typesModule, + name: identifierError.name, + }); + } + + const identifierResponse = context.file({ id: 'types' })!.identifier({ + $ref: operationResponseRef({ id: operation.id }), + namespace: 'type', + }); + if (identifierResponse.name) { + file.import({ + // this detection could be done safer, but it shouldn't cause any issues + asType: !identifierResponse.name.endsWith('Transformer'), + module: typesModule, + name: identifierResponse.name, + }); + } + + const node = compiler.constVariable({ + comment: [ + operation.deprecated && '@deprecated', + operation.summary && escapeComment(operation.summary), + operation.description && escapeComment(operation.description), + ], + exportConst: true, + expression: compiler.arrowFunction({ + parameters: [ + { + isRequired: hasOperationDataRequired(operation), + name: 'options', + type: operationOptionsType({ + importedType: identifierData.name, + throwOnError: 'ThrowOnError', + }), + }, + ], + returnType: undefined, + statements: [ + compiler.returnFunctionCall({ + args: [ + requestOptions({ + context, + operation, + path, + }), + ], + name: `(options?.client ?? client).${method}`, + types: [ + identifierResponse.name || 'unknown', + identifierError.name || 'unknown', + 'ThrowOnError', + ], + }), + ], + types: [ + { + default: false, + extends: 'boolean', + name: 'ThrowOnError', + }, + ], + }), + name: serviceFunctionIdentifier({ + config: context.config, + handleIllegal: true, + id: operation.id, + operation, + }), + }); + file.add(node); + } + } +}; + export const generateServices = ({ context }: { context: IRContext }) => { // TODO: parser - once services are a plugin, this logic can be simplified if (!context.config.services.export) { @@ -1014,59 +1365,9 @@ export const generateServices = ({ context }: { context: IRContext }) => { }); file.add(statement); - // TODO: parser - generate services - for (const path in context.ir.paths) { - const pathItem = context.ir.paths[path as keyof IRPathsObject]; - // console.warn(pathItem) - - for (const method in pathItem) { - const operation = pathItem[method as keyof IRPathItemObject]!; - - if (operation.parameters) { - const identifier = context.file({ id: 'types' })!.identifier({ - $ref: operationDataRef({ id: operation.id }), - namespace: 'type', - }); - if (identifier.name) { - file.import({ - // this detection could be done safer, but it shouldn't cause any issues - asType: !identifier.name.endsWith('Transformer'), - module: `./${context.file({ id: 'types' })!.getName(false)}`, - name: identifier.name, - }); - } - } - - // if (!isLegacy) { - // generateImport({ - // client, - // meta: { - // // TODO: this should be exact ref to operation for consistency, - // // but name should work too as operation ID is unique - // $ref: operation.name, - // name: operation.name, - // }, - // nameTransformer: operationErrorTypeName, - // onImport, - // }); - // } - - // const successResponses = operation.responses.filter((response) => - // response.responseTypes.includes('success'), - // ); - // if (successResponses.length) { - // generateImport({ - // client, - // meta: { - // // TODO: this should be exact ref to operation for consistency, - // // but name should work too as operation ID is unique - // $ref: operation.name, - // name: operation.name, - // }, - // nameTransformer: operationResponseTypeName, - // onImport, - // }); - // } - } + if (context.config.services.asClass) { + generateClassServices({ context }); + } else { + generateFlatServices({ context }); } }; diff --git a/packages/openapi-ts/src/index.ts b/packages/openapi-ts/src/index.ts index 4a530d961..f0eb5a59e 100644 --- a/packages/openapi-ts/src/index.ts +++ b/packages/openapi-ts/src/index.ts @@ -355,7 +355,7 @@ export async function createClient( operationParameter: operationParameterNameFn, }, }; - if (config.experimental_parser && !isLegacyClient(config)) { + if (config.experimental_parser && !isLegacyClient(config) && !config.name) { context = parseExperimental({ config, parserConfig, diff --git a/packages/openapi-ts/src/ir/ir.d.ts b/packages/openapi-ts/src/ir/ir.d.ts index 38811c476..3865fcb6a 100644 --- a/packages/openapi-ts/src/ir/ir.d.ts +++ b/packages/openapi-ts/src/ir/ir.d.ts @@ -1,4 +1,5 @@ import type { JsonSchemaDraft2020_12 } from '../openApi/3.1.0/types/json-schema-draft-2020-12'; +import type { IRMediaType } from './mediaType'; export interface IR { components?: IRComponentsObject; @@ -40,8 +41,10 @@ export interface IROperationObject { } export interface IRBodyObject { + mediaType: string; required?: boolean; schema: IRSchemaObject; + type?: IRMediaType; } export interface IRParametersObject { diff --git a/packages/openapi-ts/src/ir/mediaType.ts b/packages/openapi-ts/src/ir/mediaType.ts new file mode 100644 index 000000000..bac16e97f --- /dev/null +++ b/packages/openapi-ts/src/ir/mediaType.ts @@ -0,0 +1,27 @@ +const jsonMimeRegExp = /^application\/(.*\+)?json(;.*)?$/i; +const multipartFormDataMimeRegExp = /^multipart\/form-data(;.*)?$/i; +const xWwwFormUrlEncodedMimeRegExp = + /^application\/x-www-form-urlencoded(;.*)?$/i; + +export type IRMediaType = 'form-data' | 'json' | 'url-search-params'; + +export const mediaTypeToIrMediaType = ({ + mediaType, +}: { + mediaType: string; +}): IRMediaType | undefined => { + jsonMimeRegExp.lastIndex = 0; + if (jsonMimeRegExp.test(mediaType)) { + return 'json'; + } + + multipartFormDataMimeRegExp.lastIndex = 0; + if (multipartFormDataMimeRegExp.test(mediaType)) { + return 'form-data'; + } + + xWwwFormUrlEncodedMimeRegExp.lastIndex = 0; + if (xWwwFormUrlEncodedMimeRegExp.test(mediaType)) { + return 'url-search-params'; + } +}; diff --git a/packages/openapi-ts/src/ir/operation.ts b/packages/openapi-ts/src/ir/operation.ts new file mode 100644 index 000000000..69f886264 --- /dev/null +++ b/packages/openapi-ts/src/ir/operation.ts @@ -0,0 +1,16 @@ +import type { IROperationObject } from './ir'; +import { hasParametersObjectRequired } from './parameter'; + +export const hasOperationDataRequired = ( + operation: IROperationObject, +): boolean => { + if (hasParametersObjectRequired(operation.parameters)) { + return true; + } + + if (operation.body?.required) { + return true; + } + + return false; +}; diff --git a/packages/openapi-ts/src/openApi/3.1.0/parser/mediaType.ts b/packages/openapi-ts/src/openApi/3.1.0/parser/mediaType.ts index 34cdef04f..f178b8696 100644 --- a/packages/openapi-ts/src/openApi/3.1.0/parser/mediaType.ts +++ b/packages/openapi-ts/src/openApi/3.1.0/parser/mediaType.ts @@ -1,40 +1,24 @@ +import type { IRMediaType } from '../../../ir/mediaType'; +import { mediaTypeToIrMediaType } from '../../../ir/mediaType'; import type { MediaTypeObject, SchemaObject } from '../types/spec'; -const SUPPORTED_MEDIA_TYPES = [ - 'application/json-patch+json', - 'application/json', - 'application/ld+json', - 'application/x-www-form-urlencoded', - 'audio/*', - 'multipart/batch', - 'multipart/form-data', - 'multipart/mixed', - 'multipart/related', - 'text/json', - 'text/plain', - 'video/*', -] as const; - -type MediaType = (typeof SUPPORTED_MEDIA_TYPES)[number]; - interface Content { - mediaType: MediaType; + mediaType: string; schema: SchemaObject | undefined; + type: IRMediaType | undefined; } -export const getMediaTypeSchema = ({ +export const mediaTypeObject = ({ content, }: { content: Record | undefined; }): Content | undefined => { - for (const rawMediaType in content) { - const mediaTypeContent = content[rawMediaType]; - const mediaType: MediaType = rawMediaType.split(';')[0].trim() as MediaType; - if (SUPPORTED_MEDIA_TYPES.includes(mediaType)) { - return { - mediaType, - schema: mediaTypeContent.schema, - }; - } + // return the first supported MIME type + for (const mediaType in content) { + return { + mediaType, + schema: content[mediaType].schema, + type: mediaTypeToIrMediaType({ mediaType }), + }; } }; diff --git a/packages/openapi-ts/src/openApi/3.1.0/parser/operation.ts b/packages/openapi-ts/src/openApi/3.1.0/parser/operation.ts index 9f9a09f63..103bb92ae 100644 --- a/packages/openapi-ts/src/openApi/3.1.0/parser/operation.ts +++ b/packages/openapi-ts/src/openApi/3.1.0/parser/operation.ts @@ -6,7 +6,7 @@ import type { RequestBodyObject, ResponseObject, } from '../types/spec'; -import { getMediaTypeSchema } from './mediaType'; +import { mediaTypeObject } from './mediaType'; import { schemaToIrSchema } from './schema'; interface Operation @@ -72,11 +72,12 @@ const operationToIrOperation = ({ '$ref' in operation.requestBody ? context.resolveRef(operation.requestBody.$ref) : operation.requestBody; - const content = getMediaTypeSchema({ + const content = mediaTypeObject({ content: requestBodyObject.content, }); if (content) { irOperation.body = { + mediaType: content.mediaType, schema: schemaToIrSchema({ context, schema: { @@ -89,6 +90,10 @@ const operationToIrOperation = ({ if (requestBodyObject.required) { irOperation.body.required = requestBodyObject.required; } + + if (content.type) { + irOperation.body.type = content.type; + } } } @@ -98,7 +103,7 @@ const operationToIrOperation = ({ '$ref' in response ? context.resolveRef(response.$ref) : response; - const content = getMediaTypeSchema({ + const content = mediaTypeObject({ content: responseObject.content, }); if (content) { diff --git a/packages/openapi-ts/src/openApi/3.1.0/parser/parameter.ts b/packages/openapi-ts/src/openApi/3.1.0/parser/parameter.ts index 076e41019..c9778d7b7 100644 --- a/packages/openapi-ts/src/openApi/3.1.0/parser/parameter.ts +++ b/packages/openapi-ts/src/openApi/3.1.0/parser/parameter.ts @@ -1,7 +1,7 @@ import type { IRContext } from '../../../ir/context'; import type { IRParameterObject, IRParametersObject } from '../../../ir/ir'; import type { ParameterObject, ReferenceObject } from '../types/spec'; -import { getMediaTypeSchema } from './mediaType'; +import { mediaTypeObject } from './mediaType'; import { schemaToIrSchema } from './schema'; export const parametersArrayToObject = ({ @@ -109,7 +109,7 @@ const parameterToIrParameter = ({ let schema = parameter.schema; if (!schema) { - const content = getMediaTypeSchema({ + const content = mediaTypeObject({ content: parameter.content, }); if (content) { diff --git a/packages/openapi-ts/src/openApi/3.1.0/parser/schema.ts b/packages/openapi-ts/src/openApi/3.1.0/parser/schema.ts index 04a63e4eb..4c9197075 100644 --- a/packages/openapi-ts/src/openApi/3.1.0/parser/schema.ts +++ b/packages/openapi-ts/src/openApi/3.1.0/parser/schema.ts @@ -15,8 +15,22 @@ const getSchemaTypes = ({ schema, }: { schema: SchemaObject; -}): ReadonlyArray => - typeof schema.type === 'string' ? [schema.type] : schema.type ?? []; +}): ReadonlyArray => { + if (typeof schema.type === 'string') { + return [schema.type]; + } + + if (schema.type) { + return schema.type; + } + + // infer object based on the presence of properties + if (schema.properties) { + return ['object']; + } + + return []; +}; const parseSchemaMeta = ({ irSchema, @@ -750,7 +764,8 @@ export const schemaToIrSchema = ({ }); } - if (schema.type) { + // infer object based on the presence of properties + if (schema.type || schema.properties) { return parseType({ context, schema: schema as SchemaWithRequired<'type'>, diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts index 97eea525f..2ef83e0e8 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts @@ -13,11 +13,11 @@ import { operationErrorTypeName, operationOptionsType, operationResponseTypeName, - toOperationName, + serviceFunctionIdentifier, } from '../../../generate/services'; import { relativeModulePath } from '../../../generate/utils'; import type { IROperationObject, IRPathsObject } from '../../../ir/ir'; -import { hasParametersObjectRequired } from '../../../ir/parameter'; +import { hasOperationDataRequired } from '../../../ir/operation'; import { isOperationParameterRequired } from '../../../openApi'; import { getOperationKey } from '../../../openApi/common/parser/operation'; import type { @@ -39,14 +39,14 @@ import type { PluginConfig as SvelteQueryPluginConfig } from '../svelte-query'; import type { PluginConfig as VueQueryPluginConfig } from '../vue-query'; const toInfiniteQueryOptionsName = (operation: Operation) => - `${toOperationName({ + `${serviceFunctionIdentifier({ config: getConfig(), id: operation.name, operation, })}InfiniteOptions`; const toMutationOptionsName = (operation: Operation) => - `${toOperationName({ + `${serviceFunctionIdentifier({ config: getConfig(), id: operation.name, operation, @@ -61,7 +61,7 @@ const toQueryOptionsName = ({ id: string; operation: IROperationObject | Operation; }) => - `${toOperationName({ + `${serviceFunctionIdentifier({ config, id, operation, @@ -78,7 +78,7 @@ const toQueryKeyName = ({ isInfinite?: boolean; operation: IROperationObject | Operation; }) => - `${toOperationName({ + `${serviceFunctionIdentifier({ config, id, operation, @@ -564,7 +564,7 @@ const createTypeData = ({ }, }); - const typeData = operationOptionsType(nameTypeData); + const typeData = operationOptionsType({ importedType: nameTypeData }); return { typeData }; }; @@ -738,7 +738,7 @@ export const handlerLegacy: PluginLegacyHandler< config, name: service.name, }), - toOperationName({ + serviceFunctionIdentifier({ config, handleIllegal: !config.services.asClass, id: operation.name, @@ -1375,7 +1375,7 @@ export const handler: PluginHandler< // name: service.name, name: getServiceName('TODO'), }), - toOperationName({ + serviceFunctionIdentifier({ config: context.config, handleIllegal: !context.config.services.asClass, id: operation.id, @@ -1411,7 +1411,7 @@ export const handler: PluginHandler< // typesModulePath, // }); - const isRequired = hasParametersObjectRequired(operation.parameters); + const isRequired = hasOperationDataRequired(operation); const queryKeyStatement = compiler.constVariable({ exportConst: true, diff --git a/packages/openapi-ts/src/types/config.ts b/packages/openapi-ts/src/types/config.ts index 99e5752d2..5e261e969 100644 --- a/packages/openapi-ts/src/types/config.ts +++ b/packages/openapi-ts/src/types/config.ts @@ -74,7 +74,9 @@ export interface ClientConfig { */ input: string | Record; /** - * Custom client class name + * Custom client class name. Please note this option is deprecated and + * will be removed in favor of clients. + * @link https://heyapi.dev/openapi-ts/migrating.html#deprecated-name * @deprecated */ name?: string; @@ -104,7 +106,9 @@ export interface ClientConfig { */ plugins?: ReadonlyArray; /** - * Path to custom request file + * Path to custom request file. Please note this option is deprecated and + * will be removed in favor of clients. + * @link https://heyapi.dev/openapi-ts/migrating.html#deprecated-request * @deprecated */ request?: string; @@ -241,7 +245,9 @@ export interface ClientConfig { tree?: boolean; }; /** - * Use options or arguments functions + * Use options or arguments functions. Please note this option is deprecated and + * will be removed in favor of clients. + * @link https://heyapi.dev/openapi-ts/migrating.html#deprecated-useoptions * @deprecated * @default true */ diff --git a/packages/openapi-ts/src/utils/type.ts b/packages/openapi-ts/src/utils/type.ts index dd034c9fe..a1fc34078 100644 --- a/packages/openapi-ts/src/utils/type.ts +++ b/packages/openapi-ts/src/utils/type.ts @@ -187,7 +187,7 @@ const typeInterface = (model: Model) => { return compiler.typeInterfaceNode({ isNullable: model.isNullable, properties, - useLegacyResolution: !config.experimental_parser, + useLegacyResolution: true, }); }; diff --git a/packages/openapi-ts/test/3.1.0.spec.ts b/packages/openapi-ts/test/3.1.0.spec.ts index b9b1e8586..40aa84c23 100644 --- a/packages/openapi-ts/test/3.1.0.spec.ts +++ b/packages/openapi-ts/test/3.1.0.spec.ts @@ -43,6 +43,17 @@ describe(`OpenAPI ${VERSION}`, () => { }), description: 'does not generate duplicate null', }, + { + config: createConfig({ + input: 'object-properties-any-of.json', + output: 'object-properties-any-of', + services: { + export: false, + }, + }), + description: + 'sets correct logical operator and brackets on object with properties and anyOf composition', + }, { config: createConfig({ input: 'required-all-of-ref.json', diff --git a/packages/openapi-ts/test/__snapshots__/3.1.0/object-properties-any-of/index.ts b/packages/openapi-ts/test/__snapshots__/3.1.0/object-properties-any-of/index.ts new file mode 100644 index 000000000..56bade120 --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/3.1.0/object-properties-any-of/index.ts @@ -0,0 +1,2 @@ +// This file is auto-generated by @hey-api/openapi-ts +export * from './types.gen'; \ No newline at end of file diff --git a/packages/openapi-ts/test/__snapshots__/3.1.0/object-properties-any-of/types.gen.ts b/packages/openapi-ts/test/__snapshots__/3.1.0/object-properties-any-of/types.gen.ts new file mode 100644 index 000000000..33bce0e30 --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/3.1.0/object-properties-any-of/types.gen.ts @@ -0,0 +1,13 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type Foo = { + bar: string; +} | { + baz: string; +} | { + foo: string; + bar?: string; + baz?: string; +}; + +export type $OpenApiTs = unknown; \ No newline at end of file diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle/client/utils.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle/client/utils.ts.snap index 8e5185670..2f0b29739 100644 --- a/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle/client/utils.ts.snap +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle/client/utils.ts.snap @@ -332,6 +332,7 @@ export const getParseAs = ( return; } + // TODO: parser - better detection of MIME types if (content.startsWith('application/json') || content.endsWith('+json')) { return 'json'; } diff --git a/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle_transform/client/utils.ts.snap b/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle_transform/client/utils.ts.snap index 8e5185670..2f0b29739 100644 --- a/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle_transform/client/utils.ts.snap +++ b/packages/openapi-ts/test/__snapshots__/test/generated/v3-hey-api-client-fetch-bundle_transform/client/utils.ts.snap @@ -332,6 +332,7 @@ export const getParseAs = ( return; } + // TODO: parser - better detection of MIME types if (content.startsWith('application/json') || content.endsWith('+json')) { return 'json'; } diff --git a/packages/openapi-ts/test/sample.cjs b/packages/openapi-ts/test/sample.cjs index b9e9aa26b..fd5424a82 100644 --- a/packages/openapi-ts/test/sample.cjs +++ b/packages/openapi-ts/test/sample.cjs @@ -31,7 +31,7 @@ const main = async () => { }, services: { // export: false, - // asClass: true, + asClass: true, // filter: '^GET /api/v{api-version}/simple:operation$', // export: false, // name: '^Parameters', diff --git a/packages/openapi-ts/test/spec/3.1.0/full.json b/packages/openapi-ts/test/spec/3.1.0/full.json index 6e8be8aa2..e8b8c46a7 100644 --- a/packages/openapi-ts/test/spec/3.1.0/full.json +++ b/packages/openapi-ts/test/spec/3.1.0/full.json @@ -781,26 +781,26 @@ } }, "/api/v{api-version}/duplicate": { + "delete": { + "tags": ["Duplicate", "Duplicate"], + "operationId": "DuplicateName" + }, "get": { "tags": ["Duplicate"], - "operationId": "DuplicateName" + "operationId": "DuplicateName2" }, "post": { "tags": ["Duplicate"], - "operationId": "DuplicateName" + "operationId": "DuplicateName3" }, "put": { "tags": ["Duplicate"], - "operationId": "DuplicateName" - }, - "delete": { - "tags": ["Duplicate"], - "operationId": "DuplicateName" + "operationId": "DuplicateName4" } }, "/api/v{api-version}/no-content": { "get": { - "tags": ["NoContent"], + "tags": ["noContent"], "operationId": "CallWithNoContentResponse", "responses": { "204": { diff --git a/packages/openapi-ts/test/spec/3.1.0/object-properties-any-of.json b/packages/openapi-ts/test/spec/3.1.0/object-properties-any-of.json new file mode 100644 index 000000000..bb5d99fca --- /dev/null +++ b/packages/openapi-ts/test/spec/3.1.0/object-properties-any-of.json @@ -0,0 +1,28 @@ +{ + "openapi": "3.1.0", + "info": { + "version": "1.0.0" + }, + "components": { + "schemas": { + "Foo": { + "properties": { + "foo": { "type": "string" }, + "bar": { "type": "string" }, + "baz": { "type": "string" } + }, + "required": ["foo"], + "anyOf": [ + { + "properties": { "bar": { "type": "string" } }, + "required": ["bar"] + }, + { + "properties": { "baz": { "type": "string" } }, + "required": ["baz"] + } + ] + } + } + } +}