Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/generators/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
|---|---|---|---|---|---|---|---|
Expand Down
19 changes: 13 additions & 6 deletions docs/generators/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
- 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`)
30 changes: 2 additions & 28 deletions docs/inputs/openapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) | ✔️ |
Expand All @@ -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
Expand Down
74 changes: 20 additions & 54 deletions src/codegen/generators/typescript/types.ts
Original file line number Diff line number Diff line change
@@ -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'),
Expand Down Expand Up @@ -39,61 +39,27 @@ export type TypeScriptTypesRenderType =
export async function generateTypescriptTypes(
context: TypescriptTypesContext
): Promise<TypeScriptTypesRenderType> {
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
Expand Down
68 changes: 68 additions & 0 deletions src/codegen/inputs/asyncapi/generators/types.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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;
}
9 changes: 1 addition & 8 deletions src/codegen/inputs/openapi/generators/parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -985,14 +985,7 @@ function generateQueryDeserializationLogic(
paramType
);
default:
return generateFormStyleDeserializationLogic(
name,
explode,
isArray,
isBoolean,
isNumber,
paramType
);
throw new Error(`Unsupported style: ${style}`);
}
}

Expand Down
103 changes: 103 additions & 0 deletions src/codegen/inputs/openapi/generators/types.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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<string, string> = {};
const pathToOperationIdMap: Record<string, string[]> = {};

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;
}
2 changes: 1 addition & 1 deletion src/codegen/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ export type PresetTypes =
| 'headers'
| 'types'
| 'channels'
| 'channel-type'
| 'custom'
| 'client';
export interface LoadArgument {
Expand Down Expand Up @@ -81,6 +80,7 @@ export const zodOpenAPITypeScriptGenerators = z.discriminatedUnion('preset', [
zodTypeScriptPayloadGenerator,
zodTypescriptParametersGenerator,
zodTypescriptHeadersGenerator,
zodTypescriptTypesGenerator,
zodCustomGenerator
]);

Expand Down
Loading