Skip to content

Commit 4f12190

Browse files
authored
feat: enable request and reply for nats (#170)
1 parent c5246de commit 4f12190

File tree

37 files changed

+1481
-471
lines changed

37 files changed

+1481
-471
lines changed

docs/configurations.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ If no explicit configuration file is sat, it will be looked for in the following
4040
- codegen.ts
4141
- codegen.mjs
4242
- codegen.cjs
43+

docs/contributing.md

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,9 @@ sidebar_label: Contributing
1515
- [Getting started](#getting-started)
1616
- [Contribution recogniton](#contribution-recogniton)
1717
- [Summary of the contribution flow](#summary-of-the-contribution-flow)
18-
- [Code of Conduct](#code-of-conduct)
1918
- [Our Development Process](#our-development-process)
2019
- [Pull Requests](#pull-requests)
2120
- [Conventional commits](#conventional-commits)
22-
- [License](#license)
2321

2422
<!-- tocstop -->
2523

@@ -110,7 +108,7 @@ The following is a summary of the ideal contribution flow. Please, note that Pul
110108
```
111109

112110
## Code of Conduct
113-
AsyncAPI has adopted a Code of Conduct that we expect project participants to adhere to. Please [read the full text](../CODE_OF_CONDUCT.md) so that you can understand what sort of behaviour is expected.
111+
We have adopted a Code of Conduct that we expect project participants to adhere to. Please [read the full text](../CODE_OF_CONDUCT.md) so that you can understand what sort of behaviour is expected.
114112

115113
## Our Development Process
116114
We use Github to host code, to track issues and feature requests, as well as accept pull requests.
@@ -135,14 +133,3 @@ Pull requests should have a title that follows the specification, otherwise, mer
135133
What about MAJOR release? just add `!` to the prefix, like `fix!: ` or `refactor!: `
136134

137135
Prefix that follows specification is not enough though. Remember that the title must be clear and descriptive with usage of [imperative mood](https://chris.beams.io/posts/git-commit/#imperative).
138-
139-
## License
140-
When you submit changes, your submissions are understood to be under the same [Apache 2.0 License](../LICENSE) that covers the project.
141-
142-
143-
144-
145-
146-
147-
148-

docs/generators/channels.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ It supports the following languages; [`typescript`](#typescript)
2828

2929
It supports the following protocols; [`nats`](../protocols/nats.md)
3030

31+
## Options
32+
These are the available options for the `channels` generator;
33+
34+
| **Option** | Default | Type | Description |
35+
|---|---|---|
36+
| asyncapiReverseOperations | `false` | Boolean | Used in conjunction with AsyncAPI input, and reverses the operation actions i.e. send becomes receive and receive becomes send. Often used in testing scenarios to act as the reverse API. |
37+
| asyncapiGenerateForOperations | `true` | Boolean | Used in conjunction with AsyncAPI input, which if `true` generate the functions upholding how operations are defined. If `false` the functions are generated regardless of what operations define. I.e. `send` and `receive` does not matter. |
38+
| functionTypeMapping | {} | Record\<string, [ChannelFunctionTypes](https://the-codegen-project.org/docs/api/enumerations/ChannelFunctionTypes.md)[]\> | Used in conjunction with AsyncAPI input, can define channel ID along side the type of functions that should be rendered. |
39+
3140
## TypeScript
3241

3342
Depending on which protocol, these are the dependencies:
Lines changed: 103 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1+
/* eslint-disable security/detect-object-injection */
12
import {
23
AsyncAPIInputProcessor,
34
ConstrainedObjectModel,
45
OutputModel
56
} from '@asyncapi/modelina';
6-
import {AsyncAPIDocumentInterface} from '@asyncapi/parser';
7+
import {AsyncAPIDocumentInterface, ChannelInterface, MessageInterface, OperationInterface, OperationReplyInterface} from '@asyncapi/parser';
78
import {ChannelPayload, PayloadRenderType} from '../../types';
89
import {pascalCase} from '../typescript/utils';
9-
import {findNameFromChannel} from '../../utils';
10-
type ChannelReturnType = Record<
10+
import {findExtensionObject, findNameFromChannel} from '../../utils';
11+
type PayloadGenerationType = Record<
1112
string,
1213
{messageModel: OutputModel; messageType: string}
1314
>;
@@ -17,68 +18,100 @@ export async function generateAsyncAPIPayloads<GeneratorType>(
1718
generator: (input: any) => Promise<OutputModel[]>,
1819
generatorConfig: GeneratorType
1920
): Promise<PayloadRenderType<GeneratorType>> {
20-
const channelReturnType: ChannelReturnType = {};
21+
const generatedChannelPayloads: PayloadGenerationType = {};
22+
const generatedOperationPayloads: PayloadGenerationType = {};
2123
let otherModels: ChannelPayload[] = [];
2224
if (asyncapiDocument.allChannels().all().length > 0) {
2325
for (const channel of asyncapiDocument.allChannels().all()) {
24-
let schemaObj: any = {
25-
type: 'object',
26-
$schema: 'http://json-schema.org/draft-07/schema'
27-
};
28-
const replyMessages = channel.messages().all();
29-
const messages = channel.messages().all();
30-
if (messages.length > 1) {
31-
schemaObj.oneOf = [];
32-
schemaObj['$id'] = pascalCase(
33-
`${findNameFromChannel(channel)}_Payload`
34-
);
35-
for (const message of messages) {
36-
const schema = AsyncAPIInputProcessor.convertToInternalSchema(
37-
message.payload() as any
26+
const processMessages = async (messagesToProcess: MessageInterface[], preId: string): Promise<{generatedMessages: any[], messageType: string} | undefined> => {
27+
let schemaObj: any = {
28+
type: 'object',
29+
$schema: 'http://json-schema.org/draft-07/schema'
30+
};
31+
const messages = messagesToProcess;
32+
if (messages.length > 1) {
33+
schemaObj.oneOf = [];
34+
schemaObj['$id'] = pascalCase(
35+
`${preId}_Payload`
3836
);
39-
const payloadId = message.id() ?? message.name();
40-
if (typeof schema === 'boolean') {
41-
schemaObj.oneOf.push(schema);
37+
for (const message of messages) {
38+
const schema = AsyncAPIInputProcessor.convertToInternalSchema(
39+
message.payload() as any
40+
);
41+
const payloadId = message.id() ?? message.name();
42+
if (typeof schema === 'boolean') {
43+
schemaObj.oneOf.push(schema);
44+
} else {
45+
schemaObj.oneOf.push({
46+
...schema,
47+
$id: payloadId
48+
});
49+
}
50+
}
51+
} else if (messages.length === 1) {
52+
const message = messages[0];
53+
if (message.hasPayload()) {
54+
const schema = AsyncAPIInputProcessor.convertToInternalSchema(
55+
message.payload() as any
56+
);
57+
let payloadId = message.id() ?? message.name();
58+
if (payloadId.includes('AnonymousSchema_')) {
59+
payloadId = pascalCase(`${preId}_Payload`);
60+
}
61+
if (typeof schema === 'boolean') {
62+
schemaObj = schema;
63+
} else {
64+
schemaObj = {
65+
...schemaObj,
66+
...(schema as any),
67+
$id: payloadId
68+
};
69+
}
4270
} else {
43-
schemaObj.oneOf.push({
44-
...schema,
45-
$id: payloadId
46-
});
71+
return;
4772
}
73+
} else {
74+
return;
4875
}
49-
} else if (messages.length === 1) {
50-
const messagePayload = messages[0].payload();
51-
const schema = AsyncAPIInputProcessor.convertToInternalSchema(
52-
messagePayload as any
53-
);
54-
const message = messages[0];
55-
let payloadId = message.id() ?? message.name();
56-
if (payloadId.includes('AnonymousSchema_')) {
57-
payloadId = pascalCase(`${findNameFromChannel(channel)}_Payload`);
76+
const models = await generator(schemaObj);
77+
const messageModel = models[0].model;
78+
let messageType = messageModel.type;
79+
// Workaround from Modelina as rendering root payload model can create simple types and `.type` is no longer valid for what we use it for
80+
if (!(messageModel instanceof ConstrainedObjectModel)) {
81+
messageType = messageModel.name;
5882
}
59-
if (typeof schema === 'boolean') {
60-
schemaObj = schema;
61-
} else {
62-
schemaObj = {
63-
...schemaObj,
64-
...(schema as any),
65-
$id: payloadId
83+
return {generatedMessages: models, messageType};
84+
};
85+
for (const operation of channel.operations().all()) {
86+
//const operationMessages = operation.messages().all().filter((message) => message.id() !== undefined);
87+
const operationMessages = operation.messages().all();
88+
const operationReply = operation.reply();
89+
if (operationReply) {
90+
const operationReplyId = findReplyId(operation, operationReply, channel);
91+
const operationReplyGeneratedMessages = await processMessages(operationReply.messages().all(), operationReplyId);
92+
if (operationReplyGeneratedMessages) {
93+
generatedOperationPayloads[operationReplyId] = {
94+
messageModel: operationReplyGeneratedMessages.generatedMessages[0],
95+
messageType: operationReplyGeneratedMessages.messageType
96+
};
97+
}
98+
}
99+
const operationId = findOperationId(operation, channel);
100+
const operationGeneratedMessages = await processMessages(operationMessages, operationId);
101+
if (operationGeneratedMessages) {
102+
generatedOperationPayloads[operationId] = {
103+
messageModel: operationGeneratedMessages.generatedMessages[0],
104+
messageType: operationGeneratedMessages.messageType
66105
};
67106
}
68-
} else {
69-
continue;
70107
}
71-
const models = await generator(schemaObj);
72-
const messageModel = models[0].model;
73-
let messageType = messageModel.type;
74-
// Workaround from Modelina as rendering root payload model can create simple types and `.type` is no longer valid for what we use it for
75-
if (!(messageModel instanceof ConstrainedObjectModel)) {
76-
messageType = messageModel.name;
108+
const channelGeneratedMessages = await processMessages(channel.messages().all(), findNameFromChannel(channel));
109+
if (channelGeneratedMessages) {
110+
generatedChannelPayloads[channel.id()] = {
111+
messageModel: channelGeneratedMessages.generatedMessages[0],
112+
messageType: channelGeneratedMessages.messageType
113+
};
77114
}
78-
channelReturnType[channel.id()] = {
79-
messageModel: models[0],
80-
messageType
81-
};
82115
}
83116
} else {
84117
const generatedModels = await generator(asyncapiDocument);
@@ -91,8 +124,24 @@ export async function generateAsyncAPIPayloads<GeneratorType>(
91124
});
92125
}
93126
return {
94-
channelModels: channelReturnType,
127+
channelModels: generatedChannelPayloads,
128+
operationModels: generatedOperationPayloads,
95129
otherModels,
96130
generator: generatorConfig
97131
};
98132
}
133+
134+
export function findReplyId(operation: OperationInterface, reply: OperationReplyInterface, channel: ChannelInterface) {
135+
return (reply.json() as any)?.id ?? `${findOperationId(operation, reply.channel() ?? channel)}_reply`;
136+
}
137+
export function findOperationId(operation: OperationInterface, channel: ChannelInterface) {
138+
let operationId = operation.id();
139+
operationId = operation.hasOperationId() ? operation.operationId() : operationId;
140+
const userSpecificName = findExtensionObject(operation)
141+
? findExtensionObject(operation)['channelName']
142+
: undefined;
143+
if (userSpecificName) {
144+
return userSpecificName;
145+
}
146+
return operationId ?? channel.id();
147+
}

src/codegen/generators/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ export {
1919
TypeScriptChannelsGeneratorInternal,
2020
TypeScriptClientGeneratorInternal,
2121
TypescriptParametersGeneratorInternal,
22-
TypeScriptparameterRenderType,
22+
TypeScriptParameterRenderType,
2323
TypescriptHeadersContext,
2424
TypescriptHeadersGenerator,
2525
defaultTypeScriptHeadersOptions,
26-
generateTypescriptHeaders
26+
generateTypescriptHeaders,
27+
ChannelFunctionTypes
2728
} from './typescript';
2829
export {defaultCustomGenerator, CustomGenerator} from './generic/custom';
2930
import {RunGeneratorContext} from '../types';
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { ChannelFunctionTypes } from "./types";
2+
3+
type Action = 'send' | 'receive' | 'subscribe' | 'publish'
4+
const sendingFunctionTypes = [
5+
ChannelFunctionTypes.NATS_JETSTREAM_PUBLISH,
6+
ChannelFunctionTypes.NATS_PUBLISH,
7+
ChannelFunctionTypes.NATS_REQUEST
8+
];
9+
const receivingFunctionTypes = [
10+
ChannelFunctionTypes.NATS_JETSTREAM_PULL_SUBSCRIBE,
11+
ChannelFunctionTypes.NATS_JETSTREAM_PUSH_SUBSCRIBE,
12+
ChannelFunctionTypes.NATS_REPLY,
13+
ChannelFunctionTypes.NATS_SUBSCRIBE
14+
];
15+
16+
// eslint-disable-next-line sonarjs/cognitive-complexity
17+
export function shouldRenderFunctionType(
18+
givenFunctionTypes: ChannelFunctionTypes[] | undefined,
19+
functionTypesToCheckFor: ChannelFunctionTypes | ChannelFunctionTypes[],
20+
action: Action,
21+
reverseOperation: boolean
22+
) {
23+
const listToCheck = [...(Array.isArray(functionTypesToCheckFor) ? functionTypesToCheckFor : [functionTypesToCheckFor])];
24+
const hasSendingOperation = action === 'send' || action === 'subscribe';
25+
const hasReceivingOperation = action === 'receive' || action === 'publish';
26+
const hasFunctionMappingConfig = givenFunctionTypes !== undefined;
27+
const checkForSending = listToCheck.some(item => sendingFunctionTypes.includes(item));
28+
const checkForReceiving = listToCheck.some(item => receivingFunctionTypes.includes(item));
29+
const hasFunctionType = (givenFunctionTypes ?? []).some(item => listToCheck.includes(item));
30+
if (hasFunctionMappingConfig) {
31+
if (hasFunctionType) {
32+
const renderForSending = checkForSending && hasSendingOperation;
33+
const renderForReceiving = checkForReceiving && hasReceivingOperation;
34+
return renderForSending || renderForReceiving;
35+
}
36+
return false;
37+
}
38+
39+
if (reverseOperation) {
40+
const renderForSending = hasSendingOperation && checkForReceiving;
41+
const renderForReceiving = hasReceivingOperation && checkForSending;
42+
return renderForSending || renderForReceiving;
43+
}
44+
45+
const renderForSending = checkForSending && hasSendingOperation;
46+
const renderForReceiving = checkForReceiving && hasReceivingOperation;
47+
return renderForSending || renderForReceiving;
48+
}

0 commit comments

Comments
 (0)