Skip to content

Commit fc6aa1d

Browse files
authored
feat: enable channels generator for openapi (#311)
1 parent a0395e0 commit fc6aa1d

24 files changed

Lines changed: 3927 additions & 160 deletions

File tree

.eslintignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ tmp
77
jest.config.js
88
website
99
playground/__gen__
10-
test/codegen/generators/*/output
10+
test/codegen/generators/*/output
11+
mcp-server

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ tmp
77
test/runtime
88
website
99
playground/__gen__
10+
mcp-server

docs/ai-assistants.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,20 @@ https://the-codegen-project.org/api/mcp
7171
## Self-Hosting
7272

7373
You can run your own instance of the MCP server for development or private use. See the [MCP server repository](https://github.com/the-codegen-project/cli/tree/main/mcp-server) for setup instructions.
74+
75+
## Q&A
76+
77+
### Q: If AI can generate code, why use The Codegen Project?
78+
The generator gives you deterministic, repeatable output from a stable input (spec + config). That makes CI consistent, supports large-team conventions, and lets you regenerate code without drift across runs or models.
79+
80+
### Q: Won’t AI output be maintained by developers anyway?
81+
Yes, but generators keep a traceable “source of truth” in the configuration. That means you can explain why code exists, regenerate it reliably, and keep updates consistent across many services.
82+
83+
### Q: When should I prefer AI over a generator?
84+
Use AI for exploration, prototypes, or one-off scripts. Use the generator when you want consistent output, shared conventions, automated regeneration.
85+
86+
### Q: Does this project compete with AI assistants?
87+
It complements them. The MCP server gives assistants a deterministic interface to create and adjust configs, while the generator produces the exact code your repo expects.
88+
89+
### Q: What’s the biggest advantage over “just using AI”?
90+
Repeatability. The same input produces the same output every time, which is critical for CI, refactors, and multi-team consistency.

jest.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
'<rootDir>/dist',
3030
'<rootDir>/tmp',
3131
'<rootDir>/coverage',
32-
'<rootDir>/website'
32+
'<rootDir>/website',
33+
'<rootDir>/mcp-server'
3334
],
3435
};

mcp-server/lib/resources/bundled-docs.ts

Lines changed: 7 additions & 7 deletions
Large diffs are not rendered by default.

mcp-server/lib/tools/config-tools.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ function generateYamlConfig(
173173
return ` ${key}: ${typeof value === 'string' ? value : JSON.stringify(value)}`;
174174
})
175175
.join('\n');
176-
return ` - ${lines.replace(/^ /, '')}`;
176+
return ` - ${lines.replace(/^ {4}/, '')}`;
177177
})
178178
.join('\n');
179179

src/codegen/generators/typescript/channels/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
ChannelFunctionTypes
3131
} from './types';
3232
import {generateTypeScriptChannelsForAsyncAPI} from './asyncapi';
33+
import {generateTypeScriptChannelsForOpenAPI} from './openapi';
3334
export {
3435
TypeScriptChannelRenderedFunctionType,
3536
TypeScriptChannelRenderType,
@@ -72,6 +73,17 @@ export async function generateTypeScriptChannels(
7273
externalProtocolFunctionInformation,
7374
protocolDependencies
7475
);
76+
} else if (context.inputType === 'openapi') {
77+
await generateTypeScriptChannelsForOpenAPI(
78+
context,
79+
parameters,
80+
payloads,
81+
headers,
82+
protocolsToUse,
83+
protocolCodeFunctions,
84+
externalProtocolFunctionInformation,
85+
protocolDependencies
86+
);
7587
}
7688

7789
return await finalizeGeneration(
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
/**
2+
* Generates TypeScript HTTP client functions from OpenAPI specifications.
3+
* Maps OpenAPI paths and operations to the existing renderHttpFetchClient infrastructure.
4+
*/
5+
import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types';
6+
import {TypeScriptParameterRenderType} from '../parameters';
7+
import {TypeScriptPayloadRenderType} from '../payloads';
8+
import {TypeScriptHeadersRenderType} from '../headers';
9+
import {
10+
TypeScriptChannelRenderedFunctionType,
11+
SupportedProtocols,
12+
TypeScriptChannelsContext
13+
} from './types';
14+
import {ConstrainedObjectModel} from '@asyncapi/modelina';
15+
import {collectProtocolDependencies} from './utils';
16+
import {resetHttpCommonTypesState} from './protocols/http';
17+
import {
18+
renderHttpFetchClient,
19+
renderHttpCommonTypes
20+
} from './protocols/http/fetch';
21+
import {getMessageTypeAndModule} from './utils';
22+
import {pascalCase} from '../utils';
23+
24+
type OpenAPIDocument =
25+
| OpenAPIV3.Document
26+
| OpenAPIV2.Document
27+
| OpenAPIV3_1.Document;
28+
type HttpMethod =
29+
| 'get'
30+
| 'post'
31+
| 'put'
32+
| 'patch'
33+
| 'delete'
34+
| 'options'
35+
| 'head';
36+
37+
type OpenAPIOperation =
38+
| OpenAPIV3.OperationObject
39+
| OpenAPIV2.OperationObject
40+
| OpenAPIV3_1.OperationObject;
41+
42+
const HTTP_METHODS: HttpMethod[] = [
43+
'get',
44+
'post',
45+
'put',
46+
'patch',
47+
'delete',
48+
'options',
49+
'head'
50+
];
51+
const METHODS_WITH_BODY: HttpMethod[] = ['post', 'put', 'patch'];
52+
53+
// Track whether common types have been generated
54+
let httpCommonTypesGenerated = false;
55+
56+
/**
57+
* Generates TypeScript HTTP client channels from an OpenAPI document.
58+
* Only supports http_client protocol - other protocols are ignored for OpenAPI input.
59+
*/
60+
export async function generateTypeScriptChannelsForOpenAPI(
61+
context: TypeScriptChannelsContext,
62+
parameters: TypeScriptParameterRenderType,
63+
payloads: TypeScriptPayloadRenderType,
64+
headers: TypeScriptHeadersRenderType,
65+
protocolsToUse: SupportedProtocols[],
66+
protocolCodeFunctions: Record<string, string[]>,
67+
externalProtocolFunctionInformation: Record<
68+
string,
69+
TypeScriptChannelRenderedFunctionType[]
70+
>,
71+
protocolDependencies: Record<string, string[]>
72+
): Promise<void> {
73+
// Only http_client is supported for OpenAPI
74+
if (!protocolsToUse.includes('http_client')) {
75+
return;
76+
}
77+
78+
// Reset HTTP common types state
79+
resetHttpCommonTypesState();
80+
httpCommonTypesGenerated = false;
81+
82+
const {openapiDocument} = validateOpenAPIContext(context);
83+
84+
// Collect dependencies
85+
const deps = protocolDependencies['http_client'];
86+
collectProtocolDependencies(payloads, parameters, headers, context, deps);
87+
88+
// Process all operations and collect renders
89+
const renders = processOpenAPIOperations(
90+
openapiDocument,
91+
payloads,
92+
parameters
93+
);
94+
95+
// Generate common types once
96+
if (!httpCommonTypesGenerated && renders.length > 0) {
97+
const commonTypesCode = renderHttpCommonTypes();
98+
protocolCodeFunctions['http_client'].unshift(commonTypesCode);
99+
httpCommonTypesGenerated = true;
100+
}
101+
102+
// Add renders to output
103+
protocolCodeFunctions['http_client'].push(...renders.map((r) => r.code));
104+
externalProtocolFunctionInformation['http_client'].push(
105+
...renders.map((r) => ({
106+
functionType: r.functionType,
107+
functionName: r.functionName,
108+
messageType: r.messageType ?? '',
109+
replyType: r.replyType,
110+
parameterType: undefined
111+
}))
112+
);
113+
114+
// Add dependencies
115+
const renderedDeps = renders.flatMap((r) => r.dependencies);
116+
deps.push(...new Set(renderedDeps));
117+
}
118+
119+
/**
120+
* Process all OpenAPI operations and generate HTTP client functions.
121+
*/
122+
function processOpenAPIOperations(
123+
openapiDocument: OpenAPIDocument,
124+
payloads: TypeScriptPayloadRenderType,
125+
parameters: TypeScriptParameterRenderType
126+
): ReturnType<typeof renderHttpFetchClient>[] {
127+
const renders: ReturnType<typeof renderHttpFetchClient>[] = [];
128+
129+
for (const [path, pathItem] of Object.entries(openapiDocument.paths ?? {})) {
130+
if (!pathItem) {
131+
continue;
132+
}
133+
134+
for (const method of HTTP_METHODS) {
135+
const render = processOperation(
136+
pathItem,
137+
method,
138+
path,
139+
payloads,
140+
parameters
141+
);
142+
if (render) {
143+
renders.push(render);
144+
}
145+
}
146+
}
147+
148+
return renders;
149+
}
150+
151+
/**
152+
* Process a single OpenAPI operation and generate an HTTP client function.
153+
*/
154+
function processOperation(
155+
pathItem: OpenAPIV3.PathItemObject | OpenAPIV2.PathsObject,
156+
method: HttpMethod,
157+
path: string,
158+
payloads: TypeScriptPayloadRenderType,
159+
parameters: TypeScriptParameterRenderType
160+
): ReturnType<typeof renderHttpFetchClient> | undefined {
161+
// eslint-disable-next-line security/detect-object-injection
162+
const operation = (pathItem as Record<string, unknown>)[method] as
163+
| OpenAPIOperation
164+
| undefined;
165+
if (!operation) {
166+
return undefined;
167+
}
168+
169+
const operationId = getOperationId(operation, method, path);
170+
const hasBody = METHODS_WITH_BODY.includes(method);
171+
172+
// Look up payloads
173+
const requestPayload = hasBody
174+
? // eslint-disable-next-line security/detect-object-injection
175+
payloads.operationModels[operationId]
176+
: undefined;
177+
const responsePayloadKey = `${operationId}_Response`;
178+
// eslint-disable-next-line security/detect-object-injection
179+
const responsePayload = payloads.operationModels[responsePayloadKey];
180+
181+
// Look up parameters
182+
// eslint-disable-next-line security/detect-object-injection
183+
const parameterModel = parameters.channelModels[operationId];
184+
185+
// Get message types - handle undefined payloads
186+
const requestMessageInfo = requestPayload
187+
? getMessageTypeAndModule(requestPayload)
188+
: {
189+
messageModule: undefined,
190+
messageType: undefined,
191+
includesStatusCodes: false
192+
};
193+
const responseMessageInfo = responsePayload
194+
? getMessageTypeAndModule(responsePayload)
195+
: {
196+
messageModule: undefined,
197+
messageType: undefined,
198+
includesStatusCodes: false
199+
};
200+
201+
const {messageModule: requestMessageModule, messageType: requestMessageType} =
202+
requestMessageInfo;
203+
const {
204+
messageModule: replyMessageModule,
205+
messageType: replyMessageType,
206+
includesStatusCodes: replyIncludesStatusCodes
207+
} = responseMessageInfo;
208+
209+
// Skip if no response type (nothing to generate)
210+
if (!replyMessageType) {
211+
return undefined;
212+
}
213+
214+
// Generate the HTTP client function
215+
return renderHttpFetchClient({
216+
subName: pascalCase(operationId),
217+
requestMessageModule: hasBody ? requestMessageModule : undefined,
218+
requestMessageType: hasBody ? requestMessageType : undefined,
219+
replyMessageModule,
220+
replyMessageType,
221+
requestTopic: path,
222+
method: method.toUpperCase() as
223+
| 'GET'
224+
| 'POST'
225+
| 'PUT'
226+
| 'PATCH'
227+
| 'DELETE'
228+
| 'OPTIONS'
229+
| 'HEAD',
230+
channelParameters: parameterModel?.model as
231+
| ConstrainedObjectModel
232+
| undefined,
233+
includesStatusCodes: replyIncludesStatusCodes
234+
});
235+
}
236+
237+
/**
238+
* Validates the context is for OpenAPI input and has a parsed document.
239+
*/
240+
function validateOpenAPIContext(context: TypeScriptChannelsContext): {
241+
openapiDocument: OpenAPIDocument;
242+
} {
243+
const {openapiDocument, inputType} = context;
244+
if (inputType !== 'openapi') {
245+
throw new Error('Expected OpenAPI input, was not given');
246+
}
247+
if (!openapiDocument) {
248+
throw new Error('Expected a parsed OpenAPI document, was not given');
249+
}
250+
return {openapiDocument};
251+
}
252+
253+
/**
254+
* Gets the operation ID from an OpenAPI operation.
255+
* Falls back to generating one from method+path if not present.
256+
*/
257+
function getOperationId(
258+
operation: OpenAPIOperation,
259+
method: string,
260+
path: string
261+
): string {
262+
if (operation.operationId) {
263+
return operation.operationId;
264+
}
265+
// Generate from method + path
266+
const sanitizedPath = path.replace(/[^a-zA-Z0-9]/g, '');
267+
return `${method}${sanitizedPath}`;
268+
}

0 commit comments

Comments
 (0)