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
7 changes: 6 additions & 1 deletion docs/generators/headers.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default {

`headers` preset is for generating models that represent typed models representing headers.

This is supported through the following inputs: [`asyncapi`](#inputs)
This is supported through the following inputs: [`asyncapi`](#inputs), [`openapi`](#inputs)

It supports the following languages; `typescript`

Expand All @@ -30,3 +30,8 @@ It supports the following languages; `typescript`
The `headers` preset with `asyncapi` input generates all the message headers for each channel in the AsyncAPI document.

The return type is a map of channels and the model that represent the headers.

### `openapi`
The `headers` preset with `openapi` input generates all the headers for each path in the OpenAPI document.

The return type is a map of paths and the model that represent the headers.
95 changes: 64 additions & 31 deletions src/codegen/generators/typescript/headers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable security/detect-object-injection */
import {
OutputModel,
TS_COMMON_PRESET,
Expand All @@ -7,8 +8,10 @@ import {
import {AsyncAPIDocumentInterface} from '@asyncapi/parser';
import {GenericCodegenContext, HeadersRenderType} from '../../types';
import {z} from 'zod';
import {defaultCodegenTypescriptModelinaOptions, pascalCase} from './utils';
import {defaultCodegenTypescriptModelinaOptions} from './utils';
import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types';
import { processAsyncAPIHeaders } from '../../inputs/asyncapi/generators/headers';
import { processOpenAPIHeaders } from '../../inputs/openapi/generators/headers';

export const zodTypescriptHeadersGenerator = z.object({
id: z.string().optional().default('headers-typescript'),
Expand Down Expand Up @@ -42,13 +45,19 @@ export interface TypescriptHeadersContext extends GenericCodegenContext {
export type TypeScriptHeadersRenderType =
HeadersRenderType<TypescriptHeadersGeneratorInternal>;

export async function generateTypescriptHeaders(
context: TypescriptHeadersContext
): Promise<TypeScriptHeadersRenderType> {
const {asyncapiDocument, inputType, generator} = context;
if (inputType === 'asyncapi' && asyncapiDocument === undefined) {
throw new Error('Expected AsyncAPI input, was not given');
}
// Interface for processed headers data (input-agnostic)
export interface ProcessedHeadersData {
channelHeaders: Record<string, {
schema: any;
schemaId: string;
} | undefined>;
}

// Core generator function that works with processed data
export async function generateTypescriptHeadersCore(
processedData: ProcessedHeadersData,
generator: TypescriptHeadersGeneratorInternal
): Promise<Record<string, OutputModel | undefined>> {
const modelinaGenerator = new TypeScriptFileGenerator({
...defaultCodegenTypescriptModelinaOptions,
enumType: 'union',
Expand All @@ -63,33 +72,57 @@ export async function generateTypescriptHeaders(
}
]
});
const returnType: Record<string, OutputModel | undefined> = {};
for (const channel of asyncapiDocument!.allChannels().all()) {
const messages = channel.messages().all();
for (const message of messages) {
if (message.hasHeaders()) {
const schemaObj: any = {
additionalProperties: false,
...message.headers()?.json(),
type: 'object',
$id: pascalCase(`${message.id()}_headers`),
$schema: 'http://json-schema.org/draft-07/schema'
};
const models = await modelinaGenerator.generateToFiles(
schemaObj,
generator.outputPath,
{exportType: 'named'},
true
);
returnType[channel.id()] = models[0];
} else {
returnType[channel.id()] = undefined;
}

const channelModels: Record<string, OutputModel | undefined> = {};

for (const [channelId, headerData] of Object.entries(processedData.channelHeaders)) {
if (headerData) {
const models = await modelinaGenerator.generateToFiles(
headerData.schema,
generator.outputPath,
{exportType: 'named'},
true
);
channelModels[channelId] = models[0];
} else {
channelModels[channelId] = undefined;
}
}

return channelModels;
}

// Main generator function that orchestrates input processing and generation
export async function generateTypescriptHeaders(
context: TypescriptHeadersContext
): Promise<TypeScriptHeadersRenderType> {
const {asyncapiDocument, openapiDocument, inputType, generator} = context;

let processedData: ProcessedHeadersData;

// Process input based on type
switch (inputType) {
case 'asyncapi':
if (!asyncapiDocument) {
throw new Error('Expected AsyncAPI input, was not given');
}
processedData = processAsyncAPIHeaders(asyncapiDocument);
break;
case 'openapi':
if (!openapiDocument) {
throw new Error('Expected OpenAPI input, was not given');
}
processedData = processOpenAPIHeaders(openapiDocument);
break;
default:
throw new Error(`Unsupported input type: ${inputType}`);
}

// Generate models using processed data
const channelModels = await generateTypescriptHeadersCore(processedData, generator);

return {
channelModels: returnType,
channelModels,
generator
};
}
43 changes: 43 additions & 0 deletions src/codegen/inputs/asyncapi/generators/headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { AsyncAPIDocumentInterface } from "@asyncapi/parser";
import { ProcessedHeadersData } from "../../../generators/typescript/headers";
import { pascalCase } from "../../../generators/typescript/utils";

// AsyncAPI input processor
export function processAsyncAPIHeaders(
asyncapiDocument: AsyncAPIDocumentInterface
): ProcessedHeadersData {
const channelHeaders: Record<string, {
schema: any;
schemaId: string;
} | undefined> = {};

for (const channel of asyncapiDocument.allChannels().all()) {
const messages = channel.messages().all();
let hasHeadersInChannel = false;

for (const message of messages) {
if (message.hasHeaders()) {
const schemaObj: any = {
additionalProperties: false,
...message.headers()?.json(),
type: 'object',
$id: pascalCase(`${message.id()}_headers`),
$schema: 'http://json-schema.org/draft-07/schema'
};

channelHeaders[channel.id()] = {
schema: schemaObj,
schemaId: pascalCase(`${message.id()}_headers`)
};
hasHeadersInChannel = true;
break; // Use first message with headers for the channel
}
}

if (!hasHeadersInChannel) {
channelHeaders[channel.id()] = undefined;
}
}

return { channelHeaders };
}
118 changes: 118 additions & 0 deletions src/codegen/inputs/openapi/generators/headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/* eslint-disable security/detect-object-injection */
import { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from "openapi-types";
import { ProcessedHeadersData } from "../../../generators/typescript/headers";
import { pascalCase } from "../../../generators/typescript/utils";

// Helper function to convert OpenAPI parameter schema to JSON Schema
function convertParameterSchemaToJsonSchema(parameter: any): any {
let schema: any;

if (parameter.schema) {
// OpenAPI 3.x format
schema = { ...parameter.schema };
} else if (parameter.type) {
// OpenAPI 2.x format
schema = {
type: parameter.type,
...(parameter.format && { format: parameter.format }),
...(parameter.enum && { enum: parameter.enum }),
...(parameter.minimum !== undefined && { minimum: parameter.minimum }),
...(parameter.maximum !== undefined && { maximum: parameter.maximum }),
...(parameter.minLength !== undefined && { minLength: parameter.minLength }),
...(parameter.maxLength !== undefined && { maxLength: parameter.maxLength }),
...(parameter.pattern && { pattern: parameter.pattern }),
};
} else {
// Fallback to string type
schema = { type: 'string' };
}

return schema;
}

// Extract header parameters from OpenAPI operations
function extractHeadersFromOperations(paths: OpenAPIV3.PathsObject | OpenAPIV2.PathsObject | OpenAPIV3_1.PathsObject): Record<string, any[]> {
const operationHeaders: Record<string, any[]> = {};

for (const [pathKey, pathItem] of Object.entries(paths)) {
for (const [method, operation] of Object.entries(pathItem)) {
const operationObj = operation as OpenAPIV3.OperationObject | OpenAPIV2.OperationObject | OpenAPIV3_1.OperationObject;
// Collect header parameters from operation and path-level
const allParameters = operationObj.parameters ?? [];

const headerParams = allParameters.filter((param: any) => {
return param.in === 'header';
});

if (allParameters.length > 0) {
const operationId = operationObj.operationId ?? `${method}${pathKey.replace(/[^a-zA-Z0-9]/g, '')}`;
operationHeaders[operationId] = headerParams;
}
}
}

return operationHeaders;
}

// OpenAPI input processor
export function processOpenAPIHeaders(
openapiDocument: OpenAPIV3.Document | OpenAPIV2.Document | OpenAPIV3_1.Document
): ProcessedHeadersData {
const channelHeaders: Record<string, {
schema: any;
schemaId: string;
} | undefined> = {};

// Extract header parameters from all operations
const operationHeaders = extractHeadersFromOperations(openapiDocument.paths ?? {});

// Process each operation that has header parameters
for (const [operationId, headerParams] of Object.entries(operationHeaders)) {
if (headerParams.length === 0) {
channelHeaders[operationId] = undefined;
continue;
}

// Create a JSON Schema object for the headers
const properties: Record<string, any> = {};
const required: string[] = [];

for (const param of headerParams) {
const paramName = param.name;
const paramSchema = convertParameterSchemaToJsonSchema(param);

// Add description if available
if (param.description) {
paramSchema.description = param.description;
}

properties[paramName] = paramSchema;

// Check if parameter is required
if (param.required === true) {
required.push(paramName);
}
}

// Create the complete schema object
const schemaObj: any = {
type: 'object',
additionalProperties: false,
properties,
$id: pascalCase(`${operationId}_headers`),
$schema: 'http://json-schema.org/draft-07/schema'
};

// Add required array if there are required parameters
if (required.length > 0) {
schemaObj.required = required;
}

channelHeaders[operationId] = {
schema: schemaObj,
schemaId: pascalCase(`${operationId}_headers`)
};
}

return { channelHeaders };
}
8 changes: 4 additions & 4 deletions src/codegen/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,13 @@ export const zodAsyncAPIGenerators = z.union([
]);

export const zodOpenAPITypeScriptGenerators = z.discriminatedUnion('preset', [
zodTypescriptHeadersGenerator,
zodCustomGenerator
]);

// export const zodOpenAPIGenerators = z.union([
// ...zodOpenAPITypeScriptGenerators.options
// ]);
export const zodOpenAPIGenerators = zodOpenAPITypeScriptGenerators;
export const zodOpenAPIGenerators = z.union([
...zodOpenAPITypeScriptGenerators.options
]);

export type Generators =
| TypescriptHeadersGenerator
Expand Down
Loading