From 570e17c967ee76bc05ab72eb0a2277b2111e4206 Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Sat, 7 Jun 2025 10:52:41 +0200 Subject: [PATCH 1/3] wip --- docs/generators/README.md | 2 +- docs/generators/types.md | 19 +- docs/inputs/openapi.md | 30 +- src/codegen/generators/typescript/types.ts | 74 ++--- src/codegen/types.ts | 2 +- .../__snapshots__/types.spec.ts.snap | 258 ++++++++++++++++++ .../generators/typescript/types.spec.ts | 52 ++++ test/runtime/typescript/codegen-openapi.mjs | 4 + 8 files changed, 351 insertions(+), 90 deletions(-) diff --git a/docs/generators/README.md b/docs/generators/README.md index dae223cd..6126b373 100644 --- a/docs/generators/README.md +++ b/docs/generators/README.md @@ -20,7 +20,7 @@ All available generators, across languages and inputs: | **Inputs** | [`payloads`](./payloads.md) | [`parameters`](./parameters.md) | [`headers`](./headers.md) | [`types`](./types.md) | [`channels`](./channels.md) | [`client`](./client.md) | [`custom`](./custom.md) | |---|---|---|---|---|---|---|---| | AsyncAPI | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | -| OpenAPI | ✔️ | ✔️ | ✔️ | ➗ | ➗ | ➗ | ✔️ | +| OpenAPI | ✔️ | ✔️ | ✔️ | ✔️ | ➗ | ➗ | ✔️ | | **Languages** | [`payloads`](./payloads.md) | [`parameters`](./parameters.md) | [`headers`](./headers.md) | [`types`](./types.md) | [`channels`](./channels.md) | [`client`](./client.md) | [`custom`](./custom.md) | |---|---|---|---|---|---|---|---| diff --git a/docs/generators/types.md b/docs/generators/types.md index 547c194e..baacda63 100644 --- a/docs/generators/types.md +++ b/docs/generators/types.md @@ -19,16 +19,23 @@ export default { `types` preset is for generating simple types and utility functions that change based on the AsyncAPI document. -This is supported through the following inputs: `asyncapi` +This is supported through the following inputs: `asyncapi` and `openapi` It supports the following languages; `typescript` ## What it generates Here is what each language generate with this generator. -### TypeScript +### AsyncAPI -- A type that represents all the channel addresses in the document -- A type that represents all the channel IDs in the document -- A function that converts channel IDs to channel addresses -- A function that converts channel addresses to channel IDs \ No newline at end of file +- A type that represents all the channel addresses in the document (exported through `Topics`) +- A type that represents all the channel IDs in the document (exported through `TopicIds`) +- A function that converts channel addresses to channel IDs (exported through `ToTopicIds`) +- A function that converts channel IDs to channel addresses (exported through `ToTopics`) + +### OpenAPI + +- A type that represents all the operation paths in the document (exported through `Paths`) +- A type that represents all the operation IDs in the document (exported through `OperationIds`) +- A function that converts operation IDs to paths (exported through `ToPath`) +- A function that converts paths to operation IDs (exported through `ToOperationIds`) \ No newline at end of file diff --git a/docs/inputs/openapi.md b/docs/inputs/openapi.md index 87b14026..7745860f 100644 --- a/docs/inputs/openapi.md +++ b/docs/inputs/openapi.md @@ -13,9 +13,9 @@ Input support; `openapi` | **Presets** | OpenAPI | |---|---| | [`payloads`](../generators/payloads.md) | ✔️ | -| [`parameters`](../generators/parameters.md) | ➗ | +| [`parameters`](../generators/parameters.md) | ✔️ | | [`headers`](../generators/headers.md) | ✔️ | -| [`types`](../generators/types.md) | ➗ | +| [`types`](../generators/types.md) | ✔️ | | [`channels`](../generators/channels.md) | ➗ | | [`client`](../generators/client.md) | ➗ | | [`custom`](../generators/custom.md) | ✔️ | @@ -35,32 +35,6 @@ Create a configuration file that specifies OpenAPI as the input type: } ``` -## Supported Generators - -### Custom Generator - -For advanced use cases, you can create [custom generators](../generators/custom.md): - -```json -{ - "inputType": "openapi", - "inputPath": "./api/openapi.yaml", - "language": "typescript", - "generators": [ - { - preset: 'custom', - ... - renderFunction: ({generator, inputType, openapiDocument, dependencyOutputs}) - { - const modelinaGenerator = new JavaFileGenerator({}); - modelinaGenerator.generateCompleteModels(...) - } - } - ] -} - -``` - ## Advanced Features ### External References diff --git a/src/codegen/generators/typescript/types.ts b/src/codegen/generators/typescript/types.ts index 79ea4deb..72bdeadb 100644 --- a/src/codegen/generators/typescript/types.ts +++ b/src/codegen/generators/typescript/types.ts @@ -1,9 +1,9 @@ import {AsyncAPIDocumentInterface} from '@asyncapi/parser'; import {GenericCodegenContext, TypesRenderType} from '../../types'; import {z} from 'zod'; -import path from 'path'; -import {mkdir, writeFile} from 'fs/promises'; import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types'; +import {generateAsyncAPITypes} from '../../inputs/asyncapi/generators/types'; +import {generateOpenAPITypes} from '../../inputs/openapi/generators/types'; export const zodTypescriptTypesGenerator = z.object({ id: z.string().optional().default('types-typescript'), @@ -39,61 +39,27 @@ export type TypeScriptTypesRenderType = export async function generateTypescriptTypes( context: TypescriptTypesContext ): Promise { - const {asyncapiDocument, inputType, generator} = context; - if (inputType === 'asyncapi' && asyncapiDocument === undefined) { - throw new Error('Expected AsyncAPI input, was not given'); - } - const allChannels = asyncapiDocument!.allChannels().all(); - const channelAddressUnion = allChannels - .map((channel) => { - return `'${channel.address()}'`; - }) - .join(' | '); - const channelIdUnion = allChannels - .map((channel) => { - return `'${channel.id()}'`; - }) - .join(' | '); - const channelIdSwitch = allChannels - .map((channel) => { - return `case '${channel.id()}': - return '${channel.address()}';`; - }) - .join('\n '); - const channelAddressSwitch = allChannels - .map((channel) => { - return `case '${channel.address()}': - return '${channel.id()}';`; - }) - .join('\n '); - - await mkdir(context.generator.outputPath, {recursive: true}); - let result = `export type Topics = ${channelAddressUnion};\n`; - // For version 2.x we only need to generate topics - if (!asyncapiDocument!.version().startsWith('2.')) { - const topicIdsPart = `export type TopicIds = ${channelIdUnion};\n`; - const toTopicIdsPart = `export function ToTopicIds(topic: Topics): TopicIds { - switch (topic) { - ${channelAddressSwitch} - default: - throw new Error('Unknown topic: ' + topic); - } -}\n`; - const toTopicsPart = `export function ToTopics(topicId: TopicIds): Topics { - switch (topicId) { - ${channelIdSwitch} + const {asyncapiDocument, openapiDocument, inputType, generator} = context; + + let result: string; + + switch (inputType) { + case 'asyncapi': + if (!asyncapiDocument) { + throw new Error('Expected AsyncAPI input, was not given'); + } + result = await generateAsyncAPITypes(asyncapiDocument, generator); + break; + case 'openapi': + if (!openapiDocument) { + throw new Error('Expected OpenAPI input, was not given'); + } + result = await generateOpenAPITypes(openapiDocument, generator); + break; default: - throw new Error('Unknown topic ID: ' + topicId); + throw new Error(`Unsupported input type: ${inputType}`); } -}\n`; - result += topicIdsPart + toTopicIdsPart + toTopicsPart; - } - await writeFile( - path.resolve(context.generator.outputPath, 'Types.ts'), - result, - {} - ); return { result, generator diff --git a/src/codegen/types.ts b/src/codegen/types.ts index dfae23d0..f32330b3 100644 --- a/src/codegen/types.ts +++ b/src/codegen/types.ts @@ -51,7 +51,6 @@ export type PresetTypes = | 'headers' | 'types' | 'channels' - | 'channel-type' | 'custom' | 'client'; export interface LoadArgument { @@ -81,6 +80,7 @@ export const zodOpenAPITypeScriptGenerators = z.discriminatedUnion('preset', [ zodTypeScriptPayloadGenerator, zodTypescriptParametersGenerator, zodTypescriptHeadersGenerator, + zodTypescriptTypesGenerator, zodCustomGenerator ]); diff --git a/test/codegen/generators/typescript/__snapshots__/types.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/types.spec.ts.snap index fbf1699b..04a6cdf3 100644 --- a/test/codegen/generators/typescript/__snapshots__/types.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/types.spec.ts.snap @@ -1,5 +1,263 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`types typescript should work with OpenAPI 2.0 inputs 1`] = ` +"export type Paths = '/pet/{petId}/uploadImage' | '/pet' | '/pet/findByStatus' | '/pet/findByTags' | '/pet/{petId}' | '/store/inventory' | '/store/order' | '/store/order/{orderId}' | '/user/createWithList' | '/user/{username}' | '/user/login' | '/user/logout' | '/user/createWithArray' | '/user'; +export type OperationIds = 'uploadFile' | 'addPet' | 'updatePet' | 'findPetsByStatus' | 'findPetsByTags' | 'getPetById' | 'updatePetWithForm' | 'deletePet' | 'getInventory' | 'placeOrder' | 'getOrderById' | 'deleteOrder' | 'createUsersWithListInput' | 'getUserByName' | 'updateUser' | 'deleteUser' | 'loginUser' | 'logoutUser' | 'createUsersWithArrayInput' | 'createUser'; +export function ToPath(operationId: OperationIds): Paths { + switch (operationId) { + case 'uploadFile': + return '/pet/{petId}/uploadImage'; + case 'addPet': + return '/pet'; + case 'updatePet': + return '/pet'; + case 'findPetsByStatus': + return '/pet/findByStatus'; + case 'findPetsByTags': + return '/pet/findByTags'; + case 'getPetById': + return '/pet/{petId}'; + case 'updatePetWithForm': + return '/pet/{petId}'; + case 'deletePet': + return '/pet/{petId}'; + case 'getInventory': + return '/store/inventory'; + case 'placeOrder': + return '/store/order'; + case 'getOrderById': + return '/store/order/{orderId}'; + case 'deleteOrder': + return '/store/order/{orderId}'; + case 'createUsersWithListInput': + return '/user/createWithList'; + case 'getUserByName': + return '/user/{username}'; + case 'updateUser': + return '/user/{username}'; + case 'deleteUser': + return '/user/{username}'; + case 'loginUser': + return '/user/login'; + case 'logoutUser': + return '/user/logout'; + case 'createUsersWithArrayInput': + return '/user/createWithArray'; + case 'createUser': + return '/user'; + default: + throw new Error('Unknown operation ID: ' + operationId); + } +} +export function ToOperationIds(path: Paths): OperationIds[] { + switch (path) { + case '/pet/{petId}/uploadImage': + return ['uploadFile']; + case '/pet': + return ['addPet', 'updatePet']; + case '/pet/findByStatus': + return ['findPetsByStatus']; + case '/pet/findByTags': + return ['findPetsByTags']; + case '/pet/{petId}': + return ['getPetById', 'updatePetWithForm', 'deletePet']; + case '/store/inventory': + return ['getInventory']; + case '/store/order': + return ['placeOrder']; + case '/store/order/{orderId}': + return ['getOrderById', 'deleteOrder']; + case '/user/createWithList': + return ['createUsersWithListInput']; + case '/user/{username}': + return ['getUserByName', 'updateUser', 'deleteUser']; + case '/user/login': + return ['loginUser']; + case '/user/logout': + return ['logoutUser']; + case '/user/createWithArray': + return ['createUsersWithArrayInput']; + case '/user': + return ['createUser']; + default: + throw new Error('Unknown path: ' + path); + } +} +" +`; + +exports[`types typescript should work with OpenAPI 3.0 inputs 1`] = ` +"export type Paths = '/pet' | '/pet/findByStatus' | '/pet/findByTags' | '/pet/{petId}' | '/pet/{petId}/uploadImage' | '/store/inventory' | '/store/order' | '/store/order/{orderId}' | '/user' | '/user/createWithArray' | '/user/createWithList' | '/user/login' | '/user/logout' | '/user/{username}'; +export type OperationIds = 'addPet' | 'updatePet' | 'findPetsByStatus' | 'findPetsByTags' | 'getPetById' | 'updatePetWithForm' | 'deletePet' | 'uploadFile' | 'getInventory' | 'placeOrder' | 'getOrderById' | 'deleteOrder' | 'createUser' | 'createUsersWithArrayInput' | 'createUsersWithListInput' | 'loginUser' | 'logoutUser' | 'getUserByName' | 'updateUser' | 'deleteUser'; +export function ToPath(operationId: OperationIds): Paths { + switch (operationId) { + case 'addPet': + return '/pet'; + case 'updatePet': + return '/pet'; + case 'findPetsByStatus': + return '/pet/findByStatus'; + case 'findPetsByTags': + return '/pet/findByTags'; + case 'getPetById': + return '/pet/{petId}'; + case 'updatePetWithForm': + return '/pet/{petId}'; + case 'deletePet': + return '/pet/{petId}'; + case 'uploadFile': + return '/pet/{petId}/uploadImage'; + case 'getInventory': + return '/store/inventory'; + case 'placeOrder': + return '/store/order'; + case 'getOrderById': + return '/store/order/{orderId}'; + case 'deleteOrder': + return '/store/order/{orderId}'; + case 'createUser': + return '/user'; + case 'createUsersWithArrayInput': + return '/user/createWithArray'; + case 'createUsersWithListInput': + return '/user/createWithList'; + case 'loginUser': + return '/user/login'; + case 'logoutUser': + return '/user/logout'; + case 'getUserByName': + return '/user/{username}'; + case 'updateUser': + return '/user/{username}'; + case 'deleteUser': + return '/user/{username}'; + default: + throw new Error('Unknown operation ID: ' + operationId); + } +} +export function ToOperationIds(path: Paths): OperationIds[] { + switch (path) { + case '/pet': + return ['addPet', 'updatePet']; + case '/pet/findByStatus': + return ['findPetsByStatus']; + case '/pet/findByTags': + return ['findPetsByTags']; + case '/pet/{petId}': + return ['getPetById', 'updatePetWithForm', 'deletePet']; + case '/pet/{petId}/uploadImage': + return ['uploadFile']; + case '/store/inventory': + return ['getInventory']; + case '/store/order': + return ['placeOrder']; + case '/store/order/{orderId}': + return ['getOrderById', 'deleteOrder']; + case '/user': + return ['createUser']; + case '/user/createWithArray': + return ['createUsersWithArrayInput']; + case '/user/createWithList': + return ['createUsersWithListInput']; + case '/user/login': + return ['loginUser']; + case '/user/logout': + return ['logoutUser']; + case '/user/{username}': + return ['getUserByName', 'updateUser', 'deleteUser']; + default: + throw new Error('Unknown path: ' + path); + } +} +" +`; + +exports[`types typescript should work with OpenAPI 3.1 inputs 1`] = ` +"export type Paths = '/pet' | '/pet/findByStatus' | '/pet/findByTags' | '/pet/{petId}' | '/pet/{petId}/uploadImage' | '/store/inventory' | '/store/order' | '/store/order/{orderId}' | '/user' | '/user/createWithArray' | '/user/createWithList' | '/user/login' | '/user/logout' | '/user/{username}'; +export type OperationIds = 'addPet' | 'updatePet' | 'findPetsByStatus' | 'findPetsByTags' | 'getPetById' | 'updatePetWithForm' | 'deletePet' | 'uploadFile' | 'getInventory' | 'placeOrder' | 'getOrderById' | 'deleteOrder' | 'createUser' | 'createUsersWithArrayInput' | 'createUsersWithListInput' | 'loginUser' | 'logoutUser' | 'getUserByName' | 'updateUser' | 'deleteUser'; +export function ToPath(operationId: OperationIds): Paths { + switch (operationId) { + case 'addPet': + return '/pet'; + case 'updatePet': + return '/pet'; + case 'findPetsByStatus': + return '/pet/findByStatus'; + case 'findPetsByTags': + return '/pet/findByTags'; + case 'getPetById': + return '/pet/{petId}'; + case 'updatePetWithForm': + return '/pet/{petId}'; + case 'deletePet': + return '/pet/{petId}'; + case 'uploadFile': + return '/pet/{petId}/uploadImage'; + case 'getInventory': + return '/store/inventory'; + case 'placeOrder': + return '/store/order'; + case 'getOrderById': + return '/store/order/{orderId}'; + case 'deleteOrder': + return '/store/order/{orderId}'; + case 'createUser': + return '/user'; + case 'createUsersWithArrayInput': + return '/user/createWithArray'; + case 'createUsersWithListInput': + return '/user/createWithList'; + case 'loginUser': + return '/user/login'; + case 'logoutUser': + return '/user/logout'; + case 'getUserByName': + return '/user/{username}'; + case 'updateUser': + return '/user/{username}'; + case 'deleteUser': + return '/user/{username}'; + default: + throw new Error('Unknown operation ID: ' + operationId); + } +} +export function ToOperationIds(path: Paths): OperationIds[] { + switch (path) { + case '/pet': + return ['addPet', 'updatePet']; + case '/pet/findByStatus': + return ['findPetsByStatus']; + case '/pet/findByTags': + return ['findPetsByTags']; + case '/pet/{petId}': + return ['getPetById', 'updatePetWithForm', 'deletePet']; + case '/pet/{petId}/uploadImage': + return ['uploadFile']; + case '/store/inventory': + return ['getInventory']; + case '/store/order': + return ['placeOrder']; + case '/store/order/{orderId}': + return ['getOrderById', 'deleteOrder']; + case '/user': + return ['createUser']; + case '/user/createWithArray': + return ['createUsersWithArrayInput']; + case '/user/createWithList': + return ['createUsersWithListInput']; + case '/user/login': + return ['loginUser']; + case '/user/logout': + return ['logoutUser']; + case '/user/{username}': + return ['getUserByName', 'updateUser', 'deleteUser']; + default: + throw new Error('Unknown path: ' + path); + } +} +" +`; + exports[`types typescript should work with basic AsyncAPI 2.x inputs 1`] = ` "export type Topics = 'user/signedup'; " diff --git a/test/codegen/generators/typescript/types.spec.ts b/test/codegen/generators/typescript/types.spec.ts index 2700d36d..d22798b0 100644 --- a/test/codegen/generators/typescript/types.spec.ts +++ b/test/codegen/generators/typescript/types.spec.ts @@ -1,5 +1,6 @@ import path from "node:path"; import { loadAsyncapiDocument } from "../../../../src/codegen/inputs/asyncapi"; +import { loadOpenapiDocument } from "../../../../src/codegen/inputs/openapi/parser"; import { generateTypescriptTypes } from "../../../../src/codegen/generators"; describe('types', () => { @@ -38,5 +39,56 @@ describe('types', () => { }); expect(renderedContent.result).toMatchSnapshot(); }); + it('should work with OpenAPI 2.0 inputs', async () => { + const parsedOpenAPIDocument = await loadOpenapiDocument(path.resolve(__dirname, '../../../configs/openapi-2.json')); + + const renderedContent = await generateTypescriptTypes({ + generator: { + outputPath: path.resolve(__dirname, './output'), + preset: 'types', + language: 'typescript', + dependencies: [], + id: 'test' + }, + inputType: 'openapi', + openapiDocument: parsedOpenAPIDocument, + dependencyOutputs: { } + }); + expect(renderedContent.result).toMatchSnapshot(); + }); + it('should work with OpenAPI 3.0 inputs', async () => { + const parsedOpenAPIDocument = await loadOpenapiDocument(path.resolve(__dirname, '../../../configs/openapi-3.json')); + + const renderedContent = await generateTypescriptTypes({ + generator: { + outputPath: path.resolve(__dirname, './output'), + preset: 'types', + language: 'typescript', + dependencies: [], + id: 'test' + }, + inputType: 'openapi', + openapiDocument: parsedOpenAPIDocument, + dependencyOutputs: { } + }); + expect(renderedContent.result).toMatchSnapshot(); + }); + it('should work with OpenAPI 3.1 inputs', async () => { + const parsedOpenAPIDocument = await loadOpenapiDocument(path.resolve(__dirname, '../../../configs/openapi-3_1.json')); + + const renderedContent = await generateTypescriptTypes({ + generator: { + outputPath: path.resolve(__dirname, './output'), + preset: 'types', + language: 'typescript', + dependencies: [], + id: 'test' + }, + inputType: 'openapi', + openapiDocument: parsedOpenAPIDocument, + dependencyOutputs: { } + }); + expect(renderedContent.result).toMatchSnapshot(); + }); }); }); diff --git a/test/runtime/typescript/codegen-openapi.mjs b/test/runtime/typescript/codegen-openapi.mjs index 830a893f..244ae863 100644 --- a/test/runtime/typescript/codegen-openapi.mjs +++ b/test/runtime/typescript/codegen-openapi.mjs @@ -16,6 +16,10 @@ export default { { preset: 'headers', outputPath: './src/openapi/headers', + }, + { + preset: 'types', + outputPath: './src/openapi', } ] }; From 3de9b398a22cf3b609bab9e8f9214e2ea623ac73 Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Sat, 7 Jun 2025 10:54:42 +0200 Subject: [PATCH 2/3] wip --- .../inputs/asyncapi/generators/types.ts | 68 ++++++++++++ .../inputs/openapi/generators/types.ts | 103 ++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 src/codegen/inputs/asyncapi/generators/types.ts create mode 100644 src/codegen/inputs/openapi/generators/types.ts diff --git a/src/codegen/inputs/asyncapi/generators/types.ts b/src/codegen/inputs/asyncapi/generators/types.ts new file mode 100644 index 00000000..1d0861c6 --- /dev/null +++ b/src/codegen/inputs/asyncapi/generators/types.ts @@ -0,0 +1,68 @@ +import {AsyncAPIDocumentInterface} from '@asyncapi/parser'; +import {TypescriptTypesGeneratorInternal} from '../../../generators/typescript/types'; +import path from 'path'; +import {mkdir, writeFile} from 'fs/promises'; + +export async function generateAsyncAPITypes( + asyncapiDocument: AsyncAPIDocumentInterface, + generator: TypescriptTypesGeneratorInternal +): Promise { + const allChannels = asyncapiDocument.allChannels().all(); + const channelAddressUnion = allChannels + .map((channel) => { + return `'${channel.address()}'`; + }) + .join(' | '); + + let result = `export type Topics = ${channelAddressUnion};\n`; + + // For version 3.x+ we generate additional topic ID types and helper functions + if (!asyncapiDocument.version().startsWith('2.')) { + const channelIdUnion = allChannels + .map((channel) => { + return `'${channel.id()}'`; + }) + .join(' | '); + + const channelIdSwitch = allChannels + .map((channel) => { + return `case '${channel.id()}': + return '${channel.address()}';`; + }) + .join('\n '); + + const channelAddressSwitch = allChannels + .map((channel) => { + return `case '${channel.address()}': + return '${channel.id()}';`; + }) + .join('\n '); + + const topicIdsPart = `export type TopicIds = ${channelIdUnion};\n`; + const toTopicIdsPart = `export function ToTopicIds(topic: Topics): TopicIds { + switch (topic) { + ${channelAddressSwitch} + default: + throw new Error('Unknown topic: ' + topic); + } +}\n`; + const toTopicsPart = `export function ToTopics(topicId: TopicIds): Topics { + switch (topicId) { + ${channelIdSwitch} + default: + throw new Error('Unknown topic ID: ' + topicId); + } +}\n`; + + result += topicIdsPart + toTopicIdsPart + toTopicsPart; + } + + await mkdir(generator.outputPath, {recursive: true}); + await writeFile( + path.resolve(generator.outputPath, 'Types.ts'), + result, + {} + ); + + return result; +} diff --git a/src/codegen/inputs/openapi/generators/types.ts b/src/codegen/inputs/openapi/generators/types.ts new file mode 100644 index 00000000..be683ec8 --- /dev/null +++ b/src/codegen/inputs/openapi/generators/types.ts @@ -0,0 +1,103 @@ +/* eslint-disable security/detect-object-injection */ +import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types'; +import {TypescriptTypesGeneratorInternal} from '../../../generators/typescript/types'; +import path from 'path'; +import {mkdir, writeFile} from 'fs/promises'; + +export async function generateOpenAPITypes( + openapiDocument: OpenAPIV3.Document | OpenAPIV2.Document | OpenAPIV3_1.Document, + generator: TypescriptTypesGeneratorInternal +): Promise { + const paths = openapiDocument.paths ?? {}; + const allPaths = Object.keys(paths); + + // Generate union type for all paths + const pathsUnion = allPaths + .map((pathStr) => { + return `'${pathStr}'`; + }) + .join(' | '); + + let result = `export type Paths = ${pathsUnion};\n`; + + // Generate operation IDs and their corresponding paths + const operationIds: string[] = []; + const operationIdToPathMap: Record = {}; + const pathToOperationIdMap: Record = {}; + + for (const [pathStr, pathItem] of Object.entries(paths)) { + const pathOperationIds: string[] = []; + + for (const [method, operation] of Object.entries(pathItem)) { + const operationObj = operation as + | OpenAPIV3.OperationObject + | OpenAPIV2.OperationObject + | OpenAPIV3_1.OperationObject; + + if (operationObj && typeof operationObj === 'object' && method !== 'parameters') { + const operationId = operationObj.operationId ?? + `${method}${pathStr.replace(/[^a-zA-Z0-9]/g, '')}`; + operationIds.push(operationId); + operationIdToPathMap[operationId] = pathStr; + pathOperationIds.push(operationId); + } + } + + if (pathOperationIds.length > 0) { + pathToOperationIdMap[pathStr] = pathOperationIds; + } + } + + // Generate operation IDs union type + if (operationIds.length > 0) { + const operationIdsUnion = operationIds + .map((id) => `'${id}'`) + .join(' | '); + + result += `export type OperationIds = ${operationIdsUnion};\n`; + + // Generate helper function to get path from operation ID + const operationIdToPathSwitch = Object.entries(operationIdToPathMap) + .map(([operationId, pathStr]) => { + return `case '${operationId}': + return '${pathStr}';`; + }) + .join('\n '); + + const toPathPart = `export function ToPath(operationId: OperationIds): Paths { + switch (operationId) { + ${operationIdToPathSwitch} + default: + throw new Error('Unknown operation ID: ' + operationId); + } +}\n`; + + // Generate helper function to get operation IDs from path + const pathToOperationIdSwitch = Object.entries(pathToOperationIdMap) + .map(([pathStr, operationIds]) => { + const operationIdsArray = operationIds.map(id => `'${id}'`).join(', '); + return `case '${pathStr}': + return [${operationIdsArray}];`; + }) + .join('\n '); + + const toOperationIdsPart = `export function ToOperationIds(path: Paths): OperationIds[] { + switch (path) { + ${pathToOperationIdSwitch} + default: + throw new Error('Unknown path: ' + path); + } +}\n`; + + result += toPathPart + toOperationIdsPart; + } + + await mkdir(generator.outputPath, {recursive: true}); + await writeFile( + path.resolve(generator.outputPath, 'Types.ts'), + result, + {} + ); + + return result; +} From 9fd6036a9d390b82330b34e7ebec5d6f10437972 Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Sat, 7 Jun 2025 10:57:02 +0200 Subject: [PATCH 3/3] fix cd f:/project-to-be-named/cli --- src/codegen/inputs/openapi/generators/parameters.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/codegen/inputs/openapi/generators/parameters.ts b/src/codegen/inputs/openapi/generators/parameters.ts index 08e423fa..9958fe3f 100644 --- a/src/codegen/inputs/openapi/generators/parameters.ts +++ b/src/codegen/inputs/openapi/generators/parameters.ts @@ -985,14 +985,7 @@ function generateQueryDeserializationLogic( paramType ); default: - return generateFormStyleDeserializationLogic( - name, - explode, - isArray, - isBoolean, - isNumber, - paramType - ); + throw new Error(`Unsupported style: ${style}`); } }