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
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ tmp
jest.config.js
website
playground/__gen__
test/codegen/generators/*/output
test/codegen/generators/*/output
mcp-server
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ tmp
test/runtime
website
playground/__gen__
mcp-server
17 changes: 17 additions & 0 deletions docs/ai-assistants.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,20 @@ https://the-codegen-project.org/api/mcp
## Self-Hosting

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.

## Q&A

### Q: If AI can generate code, why use The Codegen Project?
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.

### Q: Won’t AI output be maintained by developers anyway?
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.

### Q: When should I prefer AI over a generator?
Use AI for exploration, prototypes, or one-off scripts. Use the generator when you want consistent output, shared conventions, automated regeneration.

### Q: Does this project compete with AI assistants?
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.

### Q: What’s the biggest advantage over “just using AI”?
Repeatability. The same input produces the same output every time, which is critical for CI, refactors, and multi-team consistency.
3 changes: 2 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
'<rootDir>/dist',
'<rootDir>/tmp',
'<rootDir>/coverage',
'<rootDir>/website'
'<rootDir>/website',
'<rootDir>/mcp-server'
],
};
14 changes: 7 additions & 7 deletions mcp-server/lib/resources/bundled-docs.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion mcp-server/lib/tools/config-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ function generateYamlConfig(
return ` ${key}: ${typeof value === 'string' ? value : JSON.stringify(value)}`;
})
.join('\n');
return ` - ${lines.replace(/^ /, '')}`;
return ` - ${lines.replace(/^ {4}/, '')}`;
})
.join('\n');

Expand Down
12 changes: 12 additions & 0 deletions src/codegen/generators/typescript/channels/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
ChannelFunctionTypes
} from './types';
import {generateTypeScriptChannelsForAsyncAPI} from './asyncapi';
import {generateTypeScriptChannelsForOpenAPI} from './openapi';
export {
TypeScriptChannelRenderedFunctionType,
TypeScriptChannelRenderType,
Expand Down Expand Up @@ -72,6 +73,17 @@ export async function generateTypeScriptChannels(
externalProtocolFunctionInformation,
protocolDependencies
);
} else if (context.inputType === 'openapi') {
await generateTypeScriptChannelsForOpenAPI(
context,
parameters,
payloads,
headers,
protocolsToUse,
protocolCodeFunctions,
externalProtocolFunctionInformation,
protocolDependencies
);
}

return await finalizeGeneration(
Expand Down
268 changes: 268 additions & 0 deletions src/codegen/generators/typescript/channels/openapi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
/**
* Generates TypeScript HTTP client functions from OpenAPI specifications.
* Maps OpenAPI paths and operations to the existing renderHttpFetchClient infrastructure.
*/
import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types';
import {TypeScriptParameterRenderType} from '../parameters';
import {TypeScriptPayloadRenderType} from '../payloads';
import {TypeScriptHeadersRenderType} from '../headers';
import {
TypeScriptChannelRenderedFunctionType,
SupportedProtocols,
TypeScriptChannelsContext
} from './types';
import {ConstrainedObjectModel} from '@asyncapi/modelina';
import {collectProtocolDependencies} from './utils';
import {resetHttpCommonTypesState} from './protocols/http';
import {
renderHttpFetchClient,
renderHttpCommonTypes
} from './protocols/http/fetch';
import {getMessageTypeAndModule} from './utils';
import {pascalCase} from '../utils';

type OpenAPIDocument =
| OpenAPIV3.Document
| OpenAPIV2.Document
| OpenAPIV3_1.Document;
type HttpMethod =
| 'get'
| 'post'
| 'put'
| 'patch'
| 'delete'
| 'options'
| 'head';

type OpenAPIOperation =
| OpenAPIV3.OperationObject
| OpenAPIV2.OperationObject
| OpenAPIV3_1.OperationObject;

const HTTP_METHODS: HttpMethod[] = [
'get',
'post',
'put',
'patch',
'delete',
'options',
'head'
];
const METHODS_WITH_BODY: HttpMethod[] = ['post', 'put', 'patch'];

// Track whether common types have been generated
let httpCommonTypesGenerated = false;

/**
* Generates TypeScript HTTP client channels from an OpenAPI document.
* Only supports http_client protocol - other protocols are ignored for OpenAPI input.
*/
export async function generateTypeScriptChannelsForOpenAPI(
context: TypeScriptChannelsContext,
parameters: TypeScriptParameterRenderType,
payloads: TypeScriptPayloadRenderType,
headers: TypeScriptHeadersRenderType,
protocolsToUse: SupportedProtocols[],
protocolCodeFunctions: Record<string, string[]>,
externalProtocolFunctionInformation: Record<
string,
TypeScriptChannelRenderedFunctionType[]
>,
protocolDependencies: Record<string, string[]>
): Promise<void> {
// Only http_client is supported for OpenAPI
if (!protocolsToUse.includes('http_client')) {
return;
}

// Reset HTTP common types state
resetHttpCommonTypesState();
httpCommonTypesGenerated = false;

const {openapiDocument} = validateOpenAPIContext(context);

// Collect dependencies
const deps = protocolDependencies['http_client'];
collectProtocolDependencies(payloads, parameters, headers, context, deps);

// Process all operations and collect renders
const renders = processOpenAPIOperations(
openapiDocument,
payloads,
parameters
);

// Generate common types once
if (!httpCommonTypesGenerated && renders.length > 0) {
const commonTypesCode = renderHttpCommonTypes();
protocolCodeFunctions['http_client'].unshift(commonTypesCode);
httpCommonTypesGenerated = true;
}

// Add renders to output
protocolCodeFunctions['http_client'].push(...renders.map((r) => r.code));
externalProtocolFunctionInformation['http_client'].push(
...renders.map((r) => ({
functionType: r.functionType,
functionName: r.functionName,
messageType: r.messageType ?? '',
replyType: r.replyType,
parameterType: undefined
}))
);

// Add dependencies
const renderedDeps = renders.flatMap((r) => r.dependencies);
deps.push(...new Set(renderedDeps));
}

/**
* Process all OpenAPI operations and generate HTTP client functions.
*/
function processOpenAPIOperations(
openapiDocument: OpenAPIDocument,
payloads: TypeScriptPayloadRenderType,
parameters: TypeScriptParameterRenderType
): ReturnType<typeof renderHttpFetchClient>[] {
const renders: ReturnType<typeof renderHttpFetchClient>[] = [];

for (const [path, pathItem] of Object.entries(openapiDocument.paths ?? {})) {
if (!pathItem) {
continue;
}

for (const method of HTTP_METHODS) {
const render = processOperation(
pathItem,
method,
path,
payloads,
parameters
);
if (render) {
renders.push(render);
}
}
}

return renders;
}

/**
* Process a single OpenAPI operation and generate an HTTP client function.
*/
function processOperation(
pathItem: OpenAPIV3.PathItemObject | OpenAPIV2.PathsObject,
method: HttpMethod,
path: string,
payloads: TypeScriptPayloadRenderType,
parameters: TypeScriptParameterRenderType
): ReturnType<typeof renderHttpFetchClient> | undefined {
// eslint-disable-next-line security/detect-object-injection
const operation = (pathItem as Record<string, unknown>)[method] as
| OpenAPIOperation
| undefined;
if (!operation) {
return undefined;
}

const operationId = getOperationId(operation, method, path);
const hasBody = METHODS_WITH_BODY.includes(method);

// Look up payloads
const requestPayload = hasBody
? // eslint-disable-next-line security/detect-object-injection
payloads.operationModels[operationId]
: undefined;
const responsePayloadKey = `${operationId}_Response`;
// eslint-disable-next-line security/detect-object-injection
const responsePayload = payloads.operationModels[responsePayloadKey];

// Look up parameters
// eslint-disable-next-line security/detect-object-injection
const parameterModel = parameters.channelModels[operationId];

// Get message types - handle undefined payloads
const requestMessageInfo = requestPayload
? getMessageTypeAndModule(requestPayload)
: {
messageModule: undefined,
messageType: undefined,
includesStatusCodes: false
};
const responseMessageInfo = responsePayload
? getMessageTypeAndModule(responsePayload)
: {
messageModule: undefined,
messageType: undefined,
includesStatusCodes: false
};

const {messageModule: requestMessageModule, messageType: requestMessageType} =
requestMessageInfo;
const {
messageModule: replyMessageModule,
messageType: replyMessageType,
includesStatusCodes: replyIncludesStatusCodes
} = responseMessageInfo;

// Skip if no response type (nothing to generate)
if (!replyMessageType) {
return undefined;
}

// Generate the HTTP client function
return renderHttpFetchClient({
subName: pascalCase(operationId),
requestMessageModule: hasBody ? requestMessageModule : undefined,
requestMessageType: hasBody ? requestMessageType : undefined,
replyMessageModule,
replyMessageType,
requestTopic: path,
method: method.toUpperCase() as
| 'GET'
| 'POST'
| 'PUT'
| 'PATCH'
| 'DELETE'
| 'OPTIONS'
| 'HEAD',
channelParameters: parameterModel?.model as
| ConstrainedObjectModel
| undefined,
includesStatusCodes: replyIncludesStatusCodes
});
}

/**
* Validates the context is for OpenAPI input and has a parsed document.
*/
function validateOpenAPIContext(context: TypeScriptChannelsContext): {
openapiDocument: OpenAPIDocument;
} {
const {openapiDocument, inputType} = context;
if (inputType !== 'openapi') {
throw new Error('Expected OpenAPI input, was not given');
}
if (!openapiDocument) {
throw new Error('Expected a parsed OpenAPI document, was not given');
}
return {openapiDocument};
}

/**
* Gets the operation ID from an OpenAPI operation.
* Falls back to generating one from method+path if not present.
*/
function getOperationId(
operation: OpenAPIOperation,
method: string,
path: string
): string {
if (operation.operationId) {
return operation.operationId;
}
// Generate from method + path
const sanitizedPath = path.replace(/[^a-zA-Z0-9]/g, '');
return `${method}${sanitizedPath}`;
}
Loading