Skip to content

Commit 015426c

Browse files
authored
feat: add openapi headers support (#220)
1 parent 26a5410 commit 015426c

File tree

9 files changed

+1023
-660
lines changed

9 files changed

+1023
-660
lines changed

docs/generators/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ All available generators, across languages and inputs:
2020
| **Inputs** | [`payloads`](./payloads.md) | [`parameters`](./parameters.md) | [`headers`](./headers.md) | [`types`](./types.md) | [`channels`](./channels.md) | [`client`](./client.md) | [`custom`](./custom.md) |
2121
|---|---|---|---|---|---|---|---|
2222
| AsyncAPI | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
23-
| OpenAPI ||| |||| ✔️ |
23+
| OpenAPI ||| ✔️ |||| ✔️ |
2424

2525
| **Languages** | [`payloads`](./payloads.md) | [`parameters`](./parameters.md) | [`headers`](./headers.md) | [`types`](./types.md) | [`channels`](./channels.md) | [`client`](./client.md) | [`custom`](./custom.md) |
2626
|---|---|---|---|---|---|---|---|

docs/generators/headers.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default {
2020

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

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

2525
It supports the following languages; `typescript`
2626

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

3232
The return type is a map of channels and the model that represent the headers.
33+
34+
### `openapi`
35+
The `headers` preset with `openapi` input generates all the headers for each path in the OpenAPI document.
36+
37+
The return type is a map of paths and the model that represent the headers.

src/codegen/generators/typescript/headers.ts

Lines changed: 64 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable security/detect-object-injection */
12
import {
23
OutputModel,
34
TS_COMMON_PRESET,
@@ -7,8 +8,10 @@ import {
78
import {AsyncAPIDocumentInterface} from '@asyncapi/parser';
89
import {GenericCodegenContext, HeadersRenderType} from '../../types';
910
import {z} from 'zod';
10-
import {defaultCodegenTypescriptModelinaOptions, pascalCase} from './utils';
11+
import {defaultCodegenTypescriptModelinaOptions} from './utils';
1112
import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types';
13+
import { processAsyncAPIHeaders } from '../../inputs/asyncapi/generators/headers';
14+
import { processOpenAPIHeaders } from '../../inputs/openapi/generators/headers';
1215

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

45-
export async function generateTypescriptHeaders(
46-
context: TypescriptHeadersContext
47-
): Promise<TypeScriptHeadersRenderType> {
48-
const {asyncapiDocument, inputType, generator} = context;
49-
if (inputType === 'asyncapi' && asyncapiDocument === undefined) {
50-
throw new Error('Expected AsyncAPI input, was not given');
51-
}
48+
// Interface for processed headers data (input-agnostic)
49+
export interface ProcessedHeadersData {
50+
channelHeaders: Record<string, {
51+
schema: any;
52+
schemaId: string;
53+
} | undefined>;
54+
}
55+
56+
// Core generator function that works with processed data
57+
export async function generateTypescriptHeadersCore(
58+
processedData: ProcessedHeadersData,
59+
generator: TypescriptHeadersGeneratorInternal
60+
): Promise<Record<string, OutputModel | undefined>> {
5261
const modelinaGenerator = new TypeScriptFileGenerator({
5362
...defaultCodegenTypescriptModelinaOptions,
5463
enumType: 'union',
@@ -63,33 +72,57 @@ export async function generateTypescriptHeaders(
6372
}
6473
]
6574
});
66-
const returnType: Record<string, OutputModel | undefined> = {};
67-
for (const channel of asyncapiDocument!.allChannels().all()) {
68-
const messages = channel.messages().all();
69-
for (const message of messages) {
70-
if (message.hasHeaders()) {
71-
const schemaObj: any = {
72-
additionalProperties: false,
73-
...message.headers()?.json(),
74-
type: 'object',
75-
$id: pascalCase(`${message.id()}_headers`),
76-
$schema: 'http://json-schema.org/draft-07/schema'
77-
};
78-
const models = await modelinaGenerator.generateToFiles(
79-
schemaObj,
80-
generator.outputPath,
81-
{exportType: 'named'},
82-
true
83-
);
84-
returnType[channel.id()] = models[0];
85-
} else {
86-
returnType[channel.id()] = undefined;
87-
}
75+
76+
const channelModels: Record<string, OutputModel | undefined> = {};
77+
78+
for (const [channelId, headerData] of Object.entries(processedData.channelHeaders)) {
79+
if (headerData) {
80+
const models = await modelinaGenerator.generateToFiles(
81+
headerData.schema,
82+
generator.outputPath,
83+
{exportType: 'named'},
84+
true
85+
);
86+
channelModels[channelId] = models[0];
87+
} else {
88+
channelModels[channelId] = undefined;
8889
}
8990
}
9091

92+
return channelModels;
93+
}
94+
95+
// Main generator function that orchestrates input processing and generation
96+
export async function generateTypescriptHeaders(
97+
context: TypescriptHeadersContext
98+
): Promise<TypeScriptHeadersRenderType> {
99+
const {asyncapiDocument, openapiDocument, inputType, generator} = context;
100+
101+
let processedData: ProcessedHeadersData;
102+
103+
// Process input based on type
104+
switch (inputType) {
105+
case 'asyncapi':
106+
if (!asyncapiDocument) {
107+
throw new Error('Expected AsyncAPI input, was not given');
108+
}
109+
processedData = processAsyncAPIHeaders(asyncapiDocument);
110+
break;
111+
case 'openapi':
112+
if (!openapiDocument) {
113+
throw new Error('Expected OpenAPI input, was not given');
114+
}
115+
processedData = processOpenAPIHeaders(openapiDocument);
116+
break;
117+
default:
118+
throw new Error(`Unsupported input type: ${inputType}`);
119+
}
120+
121+
// Generate models using processed data
122+
const channelModels = await generateTypescriptHeadersCore(processedData, generator);
123+
91124
return {
92-
channelModels: returnType,
125+
channelModels,
93126
generator
94127
};
95128
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { AsyncAPIDocumentInterface } from "@asyncapi/parser";
2+
import { ProcessedHeadersData } from "../../../generators/typescript/headers";
3+
import { pascalCase } from "../../../generators/typescript/utils";
4+
5+
// AsyncAPI input processor
6+
export function processAsyncAPIHeaders(
7+
asyncapiDocument: AsyncAPIDocumentInterface
8+
): ProcessedHeadersData {
9+
const channelHeaders: Record<string, {
10+
schema: any;
11+
schemaId: string;
12+
} | undefined> = {};
13+
14+
for (const channel of asyncapiDocument.allChannels().all()) {
15+
const messages = channel.messages().all();
16+
let hasHeadersInChannel = false;
17+
18+
for (const message of messages) {
19+
if (message.hasHeaders()) {
20+
const schemaObj: any = {
21+
additionalProperties: false,
22+
...message.headers()?.json(),
23+
type: 'object',
24+
$id: pascalCase(`${message.id()}_headers`),
25+
$schema: 'http://json-schema.org/draft-07/schema'
26+
};
27+
28+
channelHeaders[channel.id()] = {
29+
schema: schemaObj,
30+
schemaId: pascalCase(`${message.id()}_headers`)
31+
};
32+
hasHeadersInChannel = true;
33+
break; // Use first message with headers for the channel
34+
}
35+
}
36+
37+
if (!hasHeadersInChannel) {
38+
channelHeaders[channel.id()] = undefined;
39+
}
40+
}
41+
42+
return { channelHeaders };
43+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/* eslint-disable security/detect-object-injection */
2+
import { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from "openapi-types";
3+
import { ProcessedHeadersData } from "../../../generators/typescript/headers";
4+
import { pascalCase } from "../../../generators/typescript/utils";
5+
6+
// Helper function to convert OpenAPI parameter schema to JSON Schema
7+
function convertParameterSchemaToJsonSchema(parameter: any): any {
8+
let schema: any;
9+
10+
if (parameter.schema) {
11+
// OpenAPI 3.x format
12+
schema = { ...parameter.schema };
13+
} else if (parameter.type) {
14+
// OpenAPI 2.x format
15+
schema = {
16+
type: parameter.type,
17+
...(parameter.format && { format: parameter.format }),
18+
...(parameter.enum && { enum: parameter.enum }),
19+
...(parameter.minimum !== undefined && { minimum: parameter.minimum }),
20+
...(parameter.maximum !== undefined && { maximum: parameter.maximum }),
21+
...(parameter.minLength !== undefined && { minLength: parameter.minLength }),
22+
...(parameter.maxLength !== undefined && { maxLength: parameter.maxLength }),
23+
...(parameter.pattern && { pattern: parameter.pattern }),
24+
};
25+
} else {
26+
// Fallback to string type
27+
schema = { type: 'string' };
28+
}
29+
30+
return schema;
31+
}
32+
33+
// Extract header parameters from OpenAPI operations
34+
function extractHeadersFromOperations(paths: OpenAPIV3.PathsObject | OpenAPIV2.PathsObject | OpenAPIV3_1.PathsObject): Record<string, any[]> {
35+
const operationHeaders: Record<string, any[]> = {};
36+
37+
for (const [pathKey, pathItem] of Object.entries(paths)) {
38+
for (const [method, operation] of Object.entries(pathItem)) {
39+
const operationObj = operation as OpenAPIV3.OperationObject | OpenAPIV2.OperationObject | OpenAPIV3_1.OperationObject;
40+
// Collect header parameters from operation and path-level
41+
const allParameters = operationObj.parameters ?? [];
42+
43+
const headerParams = allParameters.filter((param: any) => {
44+
return param.in === 'header';
45+
});
46+
47+
if (allParameters.length > 0) {
48+
const operationId = operationObj.operationId ?? `${method}${pathKey.replace(/[^a-zA-Z0-9]/g, '')}`;
49+
operationHeaders[operationId] = headerParams;
50+
}
51+
}
52+
}
53+
54+
return operationHeaders;
55+
}
56+
57+
// OpenAPI input processor
58+
export function processOpenAPIHeaders(
59+
openapiDocument: OpenAPIV3.Document | OpenAPIV2.Document | OpenAPIV3_1.Document
60+
): ProcessedHeadersData {
61+
const channelHeaders: Record<string, {
62+
schema: any;
63+
schemaId: string;
64+
} | undefined> = {};
65+
66+
// Extract header parameters from all operations
67+
const operationHeaders = extractHeadersFromOperations(openapiDocument.paths ?? {});
68+
69+
// Process each operation that has header parameters
70+
for (const [operationId, headerParams] of Object.entries(operationHeaders)) {
71+
if (headerParams.length === 0) {
72+
channelHeaders[operationId] = undefined;
73+
continue;
74+
}
75+
76+
// Create a JSON Schema object for the headers
77+
const properties: Record<string, any> = {};
78+
const required: string[] = [];
79+
80+
for (const param of headerParams) {
81+
const paramName = param.name;
82+
const paramSchema = convertParameterSchemaToJsonSchema(param);
83+
84+
// Add description if available
85+
if (param.description) {
86+
paramSchema.description = param.description;
87+
}
88+
89+
properties[paramName] = paramSchema;
90+
91+
// Check if parameter is required
92+
if (param.required === true) {
93+
required.push(paramName);
94+
}
95+
}
96+
97+
// Create the complete schema object
98+
const schemaObj: any = {
99+
type: 'object',
100+
additionalProperties: false,
101+
properties,
102+
$id: pascalCase(`${operationId}_headers`),
103+
$schema: 'http://json-schema.org/draft-07/schema'
104+
};
105+
106+
// Add required array if there are required parameters
107+
if (required.length > 0) {
108+
schemaObj.required = required;
109+
}
110+
111+
channelHeaders[operationId] = {
112+
schema: schemaObj,
113+
schemaId: pascalCase(`${operationId}_headers`)
114+
};
115+
}
116+
117+
return { channelHeaders };
118+
}

src/codegen/types.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,13 @@ export const zodAsyncAPIGenerators = z.union([
7878
]);
7979

8080
export const zodOpenAPITypeScriptGenerators = z.discriminatedUnion('preset', [
81+
zodTypescriptHeadersGenerator,
8182
zodCustomGenerator
8283
]);
8384

84-
// export const zodOpenAPIGenerators = z.union([
85-
// ...zodOpenAPITypeScriptGenerators.options
86-
// ]);
87-
export const zodOpenAPIGenerators = zodOpenAPITypeScriptGenerators;
85+
export const zodOpenAPIGenerators = z.union([
86+
...zodOpenAPITypeScriptGenerators.options
87+
]);
8888

8989
export type Generators =
9090
| TypescriptHeadersGenerator

0 commit comments

Comments
 (0)