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
62 changes: 62 additions & 0 deletions docs/generators/channels.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ These are the available options for the `channels` generator;
| functionTypeMapping | `{}` | Record\<String, [ChannelFunctionTypes](https://the-codegen-project.org/docs/api/enumerations/ChannelFunctionTypes)[]\> | Used in conjunction with AsyncAPI input, can define channel ID along side the type of functions that should be rendered. |
| kafkaTopicSeparator | `'.'` | String | Used with AsyncAPI to ensure the right character separate topics, example if address is my/resource/path it will be converted to my.resource.path |
| eventSourceDependency | `'@microsoft/fetch-event-source'` | String | Because @microsoft/fetch-event-source is out-dated in some areas we allow you to change the fork/variant that can be used instead |
| organization | `'flat'` | `'flat' \| 'tag' \| 'path'` | Controls how generated channel functions are organized in the barrel `index.ts`. `flat` re-exports each function directly under its protocol namespace (default, unchanged). `tag` groups them under their API tag (operation tag first, then a v3 channel tag, otherwise an `untagged` bucket). `path` nests them by URL path / channel address segments; the leaf is the HTTP method for OpenAPI and a clean action verb (`publish`, `subscribe`, `jetStreamPublish`, …) for AsyncAPI. Only the barrel shape changes — the per-protocol function code is identical across styles. [See Organization](#organization) |

## TypeScript
Regardless of protocol, these are the dependencies:
Expand Down Expand Up @@ -83,3 +84,64 @@ outputPath/
```

Each protocol file contains standalone exported functions for interacting with channels defined in your AsyncAPI document.

### Organization

The `organization` option controls how the generated functions are surfaced in the barrel `index.ts`. The per-protocol `<protocol>.ts` files are **identical** across every style — only the re-export shape changes, so switching styles never changes the generated function code.

| Value | Behavior |
|---|---|
| `flat` (default) | Every function is re-exported directly under its protocol namespace. Byte-identical to previous versions. |
| `tag` | Functions are grouped under their API tag. |
| `path` | Functions are nested by their URL path / channel address segments. |

#### `flat` (default)

```ts
import { http_client } from './channels';
await http_client.updatePet({ /* ... */ });
```

#### `tag`

Functions are grouped one level deep under their first tag. Leaf names are kept **verbatim** (the operationId / generated function name is unchanged).

- **OpenAPI**: the tag comes from the operation's `tags`.
- **AsyncAPI**: the tag comes from the operation's `tags` first; if the operation has none, the (AsyncAPI v3-only) channel `tags` are used. AsyncAPI v2 channels have no tags. Functions with no resolvable tag fall into an `untagged` bucket.

```ts
import { http_client } from './channels';
await http_client.pet.updatePet({ /* ... */ }); // OpenAPI, grouped by tag "pet"

import { nats } from './channels';
await nats.user.publishToSendUserSignedup({ /* ... */ }); // AsyncAPI operation tagged "user"
await nats.untagged.publishToSendSystemPing({ /* ... */ }); // operation with no tag
```

#### `path`

Functions are nested through the static segments of the URL path (OpenAPI) or channel address (AsyncAPI); `{parameter}` placeholders and empty segments are dropped. The leaf differs by input:

- **OpenAPI**: the leaf is the lowercased HTTP **method** (so `POST /pet` and `PUT /pet` coexist as `pet.post` and `pet.put`).
- **AsyncAPI**: the leaf is a clean **action verb** derived from the function type — `publish`, `subscribe`, `request`, `reply`, `jetStreamPublish`, `jetStreamPullSubscribe`, `jetStreamPushSubscribe`, etc. — mirroring the OpenAPI method leaf (an address has no HTTP method). If two functions would resolve to the same leaf at the same node, the second falls back to its full function name so nothing is ever dropped.

```ts
import { http_client } from './channels';
await http_client.pet.put({ /* ... */ }); // PUT /pet
await http_client.pet.findByStatus.get({ /* ... */ }); // GET /pet/findByStatus/{status}/{categoryId}

import { nats } from './channels';
await nats.user.signedup.publish({ /* ... */ }); // address user/signedup/{id}
await nats.user.signedup.jetStreamPublish({ /* ... */ });
```

Configure it per channels generator:

```js
{
preset: 'channels',
outputPath: './src/__gen__/channels',
protocols: ['http_client'],
organization: 'tag' // 'flat' | 'tag' | 'path'
}
```
29 changes: 29 additions & 0 deletions examples/openapi-http-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,32 @@ The operations in this document have **no `operationId`**, so names are synthesi
| `GET /v2/users/{safepayAccountId}/bank-accounts` | `getV2UsersSafepayAccountIdBankAccounts` |

If your document declares `operationId`s, those are used verbatim (camel-cased) instead — which is almost always what you want. Give your operations meaningful `operationId`s for the cleanest client.

## Organizing the client surface

By default every function is a flat sibling under the `http_client` namespace. For larger APIs you can group them with the `organization` option — the generated function code is unchanged; only the barrel `index.ts` re-export shape changes.

```js
// codegen.config.js
{
preset: 'channels',
outputPath: './src/generated',
protocols: ['http_client'],
organization: 'tag' // 'flat' (default) | 'tag' | 'path'
}
```

Call sites, before and after:

```ts
// organization: 'flat' (default)
await http_client.postV2Connect({server, payload});

// organization: 'tag' — grouped under the operation's OpenAPI tag (tag & leaf verbatim; here the tag is "Connect")
await http_client.Connect.postV2Connect({server, payload});

// organization: 'path' — nested by URL path segments, HTTP method as the leaf
await http_client.v2.connect.post({server, payload});
```

See the [channels generator docs](https://the-codegen-project.org/docs/generators/channels) for the full description of each style.
10 changes: 10 additions & 0 deletions schemas/configuration-schema-0-with-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,16 @@
"default": "@microsoft/fetch-event-source",
"markdownDescription": "The npm package used as the EventSource (Server-Sent Events) client implementation. Override this when you need a fork or alternative because @microsoft/fetch-event-source is out of date in some areas. [Read more about the EventSource protocol here](https://the-codegen-project.org/docs/protocols/eventsource)"
},
"organization": {
"type": "string",
"enum": [
"flat",
"tag",
"path"
],
"default": "flat",
"markdownDescription": "Controls how the generated channel functions are organized in the barrel `index.ts`. \"flat\" (default) re-exports every function directly under each protocol namespace (today's behavior). \"tag\" groups the functions under their API tag (operation tag first, then a v3 channel tag, otherwise an \"untagged\" bucket). \"path\" nests the functions by their URL path / channel address segments; the leaf key is the HTTP method for OpenAPI inputs and a clean action verb (publish, subscribe, jetStreamPublish, etc.) for AsyncAPI inputs. The per-protocol function code is identical across all three styles — only the barrel re-export shape changes. [Read more about the channels generator here](https://the-codegen-project.org/docs/generators/channels)"
},
"language": {
"type": "string",
"const": "typescript",
Expand Down
10 changes: 10 additions & 0 deletions schemas/configuration-schema-0.json
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,16 @@
"default": "@microsoft/fetch-event-source",
"description": "The npm package used as the EventSource (Server-Sent Events) client implementation. Override this when you need a fork or alternative because @microsoft/fetch-event-source is out of date in some areas. Read more about the EventSource protocol here"
},
"organization": {
"type": "string",
"enum": [
"flat",
"tag",
"path"
],
"default": "flat",
"description": "Controls how the generated channel functions are organized in the barrel `index.ts`. \"flat\" (default) re-exports every function directly under each protocol namespace (today's behavior). \"tag\" groups the functions under their API tag (operation tag first, then a v3 channel tag, otherwise an \"untagged\" bucket). \"path\" nests the functions by their URL path / channel address segments; the leaf key is the HTTP method for OpenAPI inputs and a clean action verb (publish, subscribe, jetStreamPublish, etc.) for AsyncAPI inputs. The per-protocol function code is identical across all three styles — only the barrel re-export shape changes. Read more about the channels generator here"
},
"language": {
"type": "string",
"const": "typescript",
Expand Down
30 changes: 10 additions & 20 deletions src/codegen/generators/typescript/channels/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@
/* eslint-disable security/detect-object-injection */
/* eslint-disable sonarjs/no-duplicate-string */
import {TheCodegenConfiguration, GeneratedFile} from '../../../types';
import {
appendImportExtension,
resolveImportExtension,
joinPath
} from '../../../utils';
import {resolveImportExtension, joinPath} from '../../../utils';
import {
defaultTypeScriptParametersOptions,
TypeScriptParameterRenderType,
Expand Down Expand Up @@ -34,6 +30,7 @@ import {
} from './types';
import {generateTypeScriptChannelsForAsyncAPI} from './asyncapi';
import {generateTypeScriptChannelsForOpenAPI} from './openapi';
import {renderChannelIndex} from './utils';
export {
TypeScriptChannelRenderedFunctionType,
TypeScriptChannelRenderType,
Expand Down Expand Up @@ -147,21 +144,14 @@ async function finalizeGeneration(
protocolFiles[protocol] = fileContent;
}

// Generate index.ts with namespace re-exports
let indexContent: string;
if (generatedProtocols.length > 0) {
const ext = resolveImportExtension(context.generator, context.config);
const imports = generatedProtocols
.map((p) => {
const importPath = appendImportExtension(`./${p}`, ext);
return `import * as ${p} from '${importPath}';`;
})
.join('\n');
const exports = generatedProtocols.join(', ');
indexContent = `${imports}\n\nexport {${exports}};\n`;
} else {
indexContent = '// No protocols generated\n';
}
// Generate index.ts. The per-protocol `<protocol>.ts` files are identical
// across every organization style — only this barrel changes shape.
const indexContent = renderChannelIndex({
generatedProtocols,
externalProtocolFunctionInformation,
organization: context.generator.organization,
importExtension: resolveImportExtension(context.generator, context.config)
});

const indexFilePath = joinPath(context.generator.outputPath, 'index.ts');
files.push({path: indexFilePath, content: indexContent});
Expand Down
41 changes: 23 additions & 18 deletions src/codegen/generators/typescript/channels/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import {
TypeScriptChannelsContext
} from './types';
import {ConstrainedObjectModel} from '@asyncapi/modelina';
import {collectProtocolDependencies} from './utils';
import {collectProtocolDependencies, addRendersToExternal} from './utils';
import {
renderHttpFetchClient,
renderHttpCommonTypes,
analyzeSecuritySchemes
} from './protocols/http';
import {getMessageTypeAndModule} from './utils';
import {getMessageTypeAndModule, splitAddressSegments} from './utils';
import {camelCase} from '../utils';
import {createMissingInputDocumentError} from '../../../errors';
import {resolveImportExtension} from '../../../utils';
Expand Down Expand Up @@ -116,21 +116,17 @@ export async function generateTypeScriptChannelsForOpenAPI(
protocolCodeFunctions['http_client'].unshift(commonTypesCode);
}

// 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: r.parameterType
}))
);

// Add dependencies
const renderedDeps = renders.flatMap((r) => r.dependencies);
deps.push(...new Set(renderedDeps));
// Add renders (code + external function information + dependencies) to output
// via the shared helper, so the `organization` grouping metadata and the
// path-parameter model name are forwarded from the same single place every
// protocol uses.
addRendersToExternal({
protocol: 'http_client',
renders,
protocolCodeFunctions,
externalProtocolFunctionInformation,
dependencies: deps
});
}

/**
Expand Down Expand Up @@ -243,7 +239,7 @@ function processOperation(
// Use the operationId directly as the function name; the HTTP method is
// already encoded in synthesized ids (and meaningful in spec-provided ones),
// so prepending the method here would double the verb (e.g. getGetUser).
return renderHttpFetchClient({
const render = renderHttpFetchClient({
functionName: camelCase(operationId),
requestMessageModule: hasBody ? requestMessageModule : undefined,
requestMessageType: hasBody ? requestMessageType : undefined,
Expand All @@ -266,6 +262,15 @@ function processOperation(
deprecated,
oauth2Enabled
});

// Grouping metadata for the `organization` option (consumed in
// finalizeGeneration). tag → operation tag; path → static path segments with
// the HTTP method as the leaf.
render.tags = operation.tags ?? [];
render.pathSegments = splitAddressSegments(path);
render.method = method.toLowerCase();

return render;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import {
findOperationId,
getOperationMetadata
} from '../../../../../utils';
import {getMessageTypeAndModule} from '../../utils';
import {
getMessageTypeAndModule,
attachGroupingMetadata,
addRendersToExternal
} from '../../utils';
import {
shouldRenderFunctionType,
getFunctionTypeMappingFromAsyncAPI
Expand All @@ -21,7 +25,6 @@ import {renderPublishQueue} from './publishQueue';
import {renderSubscribeQueue} from './subscribeQueue';
import {ChannelInterface} from '@asyncapi/parser';
import {SingleFunctionRenderType} from '../../../../../types';
import {ConstrainedObjectModel} from '@asyncapi/modelina';
import {createMissingPayloadError} from '../../../../../errors';

export {renderPublishExchange, renderPublishQueue, renderSubscribeQueue};
Expand Down Expand Up @@ -54,39 +57,14 @@ export async function generateAmqpChannels(
? await generateForOperations(context, channel, amqpContext)
: await generateForChannels(context, channel, amqpContext);

addRendersToExternal(
addRendersToExternal({
protocol: 'amqp',
renders,
protocolCodeFunctions,
externalProtocolFunctionInformation,
dependencies,
parameter
);
}

function addRendersToExternal(
renders: SingleFunctionRenderType[],
protocolCodeFunctions: Record<string, string[]>,
externalProtocolFunctionInformation: Record<
string,
TypeScriptChannelRenderedFunctionType[]
>,
dependencies: string[],
parameter?: ConstrainedObjectModel
) {
protocolCodeFunctions['amqp'].push(...renders.map((value) => value.code));
externalProtocolFunctionInformation['amqp'].push(
...renders.map((value) => ({
functionType: value.functionType,
functionName: value.functionName,
messageType: value.messageType,
replyType: value.replyType,
parameterType: parameter?.type
}))
);
const renderedDependencies = renders
.map((value) => value.dependencies)
.flat(Infinity);
dependencies.push(...(new Set(renderedDependencies) as any));
});
}

async function generateForOperations(
Expand Down Expand Up @@ -128,15 +106,20 @@ async function generateForOperations(
deprecated
};

renders.push(
...generateOperationRenders(
operation,
updatedContext,
updatedFunctionTypeMapping,
generator,
exchangeName
)
const operationRenders = generateOperationRenders(
operation,
updatedContext,
updatedFunctionTypeMapping,
generator,
exchangeName
);
attachGroupingMetadata({
renders: operationRenders,
operation,
channel,
topic: context.topic
});
renders.push(...operationRenders);
}
return renders;
}
Expand Down Expand Up @@ -242,5 +225,10 @@ async function generateForChannels(
renders.push(render({...updatedContext, additionalProperties}));
}
}
attachGroupingMetadata({
renders,
channel,
topic: context.topic
});
return renders;
}
Loading
Loading