Skip to content

Commit

Permalink
feat: add runtime test and various fixes (#13)
Browse files Browse the repository at this point in the history
Fixes #9
Fixes #11
  • Loading branch information
jonaslagoni authored Jun 13, 2024
1 parent 3252540 commit 22271b4
Show file tree
Hide file tree
Showing 27 changed files with 408 additions and 417 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pr-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
types: [opened, reopened, synchronize, ready_for_review]

jobs:
test-nodejs-pr:
test-pr:
name: Test NodeJS PR - ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
Expand Down
6 changes: 2 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@ jobs:
# Release workflow will be skipped if release conventional commits are not used
if: |
startsWith( github.repository, 'the-codegen-project/' ) &&
(startsWith( github.event.commits[0].message , 'fix:' ) ||
startsWith( github.event.commits[0].message, 'fix!:' ) ||
startsWith( github.event.commits[0].message, 'feat:' ) ||
startsWith( github.event.commits[0].message, 'feat!:' ))
(startsWith( github.event.commits[0].message , 'fix' ) ||
startsWith( github.event.commits[0].message, 'feat' ))
name: Test NodeJS release on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
Expand Down
85 changes: 85 additions & 0 deletions .github/workflows/runtime-testing.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: Runtime Testing The Codegen Project
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

jobs:
build:
name: Prepare for runtime testing
runs-on: ubuntu-latest
if: "github.event.pull_request.draft == false && (startsWith(github.event.pull_request.title, 'feat') || startsWith(github.event.pull_request.title, 'fix'))"
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check package-lock version
uses: asyncapi/.github/.github/actions/get-node-version-from-package-lock@master
id: lockversion
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "${{ steps.lockversion.outputs.version }}"
- name: Install dependencies
shell: bash
run: npm ci
- name: Build library
run: npm run build
- name: Assets generation
run: npm run generate:assets
- name: Archive build artifacts
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: |
dist
node_modules
oclif.manifest.json
typescript:
name: Runtime Testing TypeScript
runs-on: ubuntu-latest
needs: 'build'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Archive build artifacts
uses: actions/download-artifact@v4
with:
name: build-artifacts
- name: Check package-lock version
uses: asyncapi/.github/.github/actions/get-node-version-from-package-lock@master
id: lockversion
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "${{ steps.lockversion.outputs.version }}"
- name: Setup for runtime test
run: npm run runtime:typescript:setup
- name: Run runtime tests
run: npm run runtime:typescript:test

java:
name: Runtime Testing Java
runs-on: ubuntu-latest
needs: 'build'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Archive build artifacts
uses: actions/download-artifact@v4
with:
name: build-artifacts
- name: Check package-lock version
uses: asyncapi/.github/.github/actions/get-node-version-from-package-lock@master
id: lockversion
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "${{ steps.lockversion.outputs.version }}"
- uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '11'
- name: Setup for runtime test
run: npm run runtime:java:setup
- name: Run runtime tests
run: npm run runtime:java:test
13 changes: 10 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,18 @@
"pack:windows": "oclif pack win && npm run pack:rename",
"pack:rename": "node scripts/releasePackagesRename.js",
"prepublishOnly": "npm run build",
"test": "cross-env CI=true jest --coverage",
"test": "jest --coverage",
"coverage": "nyc npm run test",
"bump:version": "npm --no-git-tag-version --allow-same-version version $VERSION",
"runtime:prepare": "npm run build && npm link",
"runtime:typescript": "cd test/runtime/typescript && codegen generate"
"runtime:prepare": "npm link",
"runtime:java:setup": "npm run runtime:prepare && npm run runtime:services:start && cd test/runtime/java && mvn install",
"runtime:java:test": "cd test/runtime/java && mvn test",
"runtime:typescript:setup": "npm run runtime:prepare && npm run runtime:services:start && cd test/runtime/typescript && npm ci && npm run generate",
"runtime:typescript:test": "cd test/runtime/typescript && npm run test",
"runtime:services:start": "npm run runtime:nats:start",
"runtime:services:stop": "npm run runtime:nats:stop",
"runtime:nats:start": "cd test/runtime && docker-compose -f ./docker-compose-nats.yml up -d",
"runtime:nats:stop": "cd test/runtime && docker-compose -f ./docker-compose-nats.yml down"
},
"engines": {
"node": ">=18.0.0"
Expand Down
6 changes: 4 additions & 2 deletions src/codegen/generators/helpers/payloads.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { AsyncAPIInputProcessor, OutputModel } from "@asyncapi/modelina";
import { AsyncAPIDocumentInterface } from "@asyncapi/parser";
import { PayloadRenderType } from "../../types";
import { TypeScriptPayloadGenerator } from "../typescript/payloads";

export async function generateAsyncAPIPayloads(asyncapiDocument: AsyncAPIDocumentInterface, generator: (input: any) => Promise<OutputModel[]>): Promise<PayloadRenderType> {
export async function generateAsyncAPIPayloads(asyncapiDocument: AsyncAPIDocumentInterface, generator: (input: any) => Promise<OutputModel[]>, generatorConfig: TypeScriptPayloadGenerator): Promise<PayloadRenderType> {
const returnType: Record<string, OutputModel> = {};
for (const channel of asyncapiDocument.allChannels().all()) {
let schemaObj: any = {
Expand Down Expand Up @@ -32,6 +33,7 @@ export async function generateAsyncAPIPayloads(asyncapiDocument: AsyncAPIDocumen
returnType[channel.id()] = models[0];
}
return {
channelModels: returnType
channelModels: returnType,
generator: generatorConfig
};
}
34 changes: 26 additions & 8 deletions src/codegen/generators/typescript/channels/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { renderJetstreamPublish } from './protocols/nats/jetstreamPublish';
import { renderJetstreamPullSubscribe } from './protocols/nats/pullSubscribe';
import { TypescriptParametersGenerator } from '../parameters';
import { OutputModel } from '@asyncapi/modelina';
import { TypeScriptPayloadGenerator } from '../payloads';
export type SupportedProtocols = "nats";
export interface TypeScriptChannelsGenerator extends GenericGeneratorOptions {
preset: 'channels',
Expand Down Expand Up @@ -55,28 +58,43 @@ export async function generateTypeScriptChannels(context: TypeScriptChannelsCont
throw new Error("Internal error, could not determine previous rendered parameters generator that is required for channel typescript generator");
}

let codeToRender: string[] = [];
const codeToRender: string[] = [];
const dependencies: string[] = [];
for (const channel of asyncapiDocument!.allChannels().all()) {
const protocolsToUse = generator.protocols;
const parameter = parameters.channelModels[channel.id()];
const parameter = parameters.channelModels[channel.id()] as OutputModel;
if (parameter === undefined) {
throw new Error(`Could not find parameter for ${channel.id()} for channel typescript generator`);
}
const payload = payloads.channelModels[channel.id()];

const parameterGenerator = parameters.generator as TypescriptParametersGenerator;
const parameterImportPath = path.relative(context.generator.outputPath, path.resolve(parameterGenerator.outputPath, parameter.modelName));

dependencies.push(`import {${parameter.modelName}} from '${parameterImportPath}';`);
const payload = payloads.channelModels[channel.id()] as OutputModel;
if (payload === undefined) {
throw new Error(`Could not find payload for ${channel.id()} for channel typescript generator`);
}
const payloadGenerator = payloads.generator as TypeScriptPayloadGenerator;
const payloadImportPath = path.relative(context.generator.outputPath, path.resolve(payloadGenerator.outputPath, payload.modelName));
dependencies.push(`import {${payload.modelName}} from '${payloadImportPath}';`);

for (const protocol of protocolsToUse) {
const simpleContext = {topic: channel.address()!, channelParameters: parameter.model, message: payload.model, };
const simpleContext = {topic: channel.address()!, channelParameters: parameter.model as any, message: payload.model as any};
switch (protocol) {
case 'nats': {
// AsyncAPI v2 explicitly say to use RFC 6570 URI template, NATS JetStream does not support '/' subjects.
let topic = simpleContext.topic;
topic = topic.replace(/\//g, '.');
const natsContext = {...simpleContext, topic};
const renders = [
//renderJetstreamFetch(simpleContext),
renderJetstreamPublish(simpleContext),
renderJetstreamPullSubscribe(simpleContext)
renderJetstreamPublish(natsContext),
renderJetstreamPullSubscribe(natsContext)
];
codeToRender = renders.map((value) => value.code);
codeToRender.push(...renders.map((value) => value.code));
const deps = renders.map((value) => value.dependencies).flat(Infinity);
dependencies.push(...new Set(deps) as any);
break;
}

Expand All @@ -87,5 +105,5 @@ export async function generateTypeScriptChannels(context: TypeScriptChannelsCont
}
}
await mkdir(context.generator.outputPath, { recursive: true });
await writeFile(path.resolve(context.generator.outputPath, 'index.ts'), codeToRender.join('\n\n'), {});
await writeFile(path.resolve(context.generator.outputPath, 'index.ts'), `${dependencies.join('\n')}\n${codeToRender.join('\n\n')}`, {});
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export function renderJetstreamPublish({
functionName?: string
}): SingleFunctionRenderType {
const hasNullPayload = message.type === 'null';
const addressToUse = channelParameters !== undefined ? 'parameters.getChannelWithParameters()' : topic;
const addressToUse = channelParameters !== undefined ? `parameters.getChannelWithParameters('${topic}')` : topic;
const dependencies = [`import * as Nats from 'nats';`];
// Determine the publish operation based on whether the message type is null
let publishOperation = `await js.publish(${addressToUse}, Nats.Empty);`;
if (!hasNullPayload) {
Expand All @@ -31,8 +32,8 @@ js.publish(${addressToUse}, dataToSend, options);`;
}
functionParameters.push(...[
{parameter: 'js: Nats.JetStreamClient', jsDoc: '* @param js the JetStream client to publish from'},
{parameter: 'codec?: any = Nats.JSONCodec()', jsDoc: '* @param codec the serialization codec to use while transmitting the message'},
{parameter: 'options?: Nats.PublishOptions', jsDoc: '* @param options to use while publishing the message'},
{parameter: 'codec: any = Nats.JSONCodec()', jsDoc: '* @param codec the serialization codec to use while transmitting the message'},
{parameter: 'options: Partial<Nats.JetStreamPublishOptions> = {}', jsDoc: '* @param options to use while publishing the message'},
]);

const jsDocParameters = functionParameters.map((parameter) => parameter.jsDoc);
Expand All @@ -57,6 +58,7 @@ export function ${functionName}(
}`;
return {
code,
functionName
functionName,
dependencies
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,22 @@ export function renderJetstreamPullSubscribe({
message,
channelParameters,
functionName = `jetStreamPullSubscribeTo${pascalCase(topic)}`
}: {
}: {
topic: string,
message: ConstrainedMetaModel,
channelParameters: ConstrainedObjectModel | undefined,
functionName?: string
}): SingleFunctionRenderType {
const hasNullPayload = message.type === 'null';
const addressToUse = channelParameters !== undefined ? 'parameters.getChannelWithParameters()' : topic;
const addressToUse = channelParameters !== undefined ? `parameters.getChannelWithParameters('${topic}')` : topic;
const dependencies = [`import * as Nats from 'nats';`];

const callbackFunctionParameters = [
{parameter: 'err?: NatsTypescriptTemplateError', jsDoc: '* @param err if any error occurred this will be sat'},
{parameter: 'err?: Error', jsDoc: '* @param err if any error occurred this will be sat'},
{parameter: `msg?: ${message.type}`, jsDoc: '* @param msg that was received'},
];
if (channelParameters !== undefined) {
callbackFunctionParameters.push({parameter: `parameters: ${channelParameters.type}`, jsDoc: '* @param parameters that was received in the topic'});
callbackFunctionParameters.push({parameter: `parameters?: ${channelParameters.type}`, jsDoc: '* @param parameters that was received in the topic'});
}
callbackFunctionParameters.push(...[
{parameter: 'jetstreamMsg?: Nats.JsMsg', jsDoc: '* @param jetstreamMsg '},
Expand All @@ -36,16 +37,26 @@ export function renderJetstreamPullSubscribe({
functionParameters.push({parameter: `parameters: ${channelParameters.type}`, jsDoc: '* @param parameters for topic substitution'});
}
functionParameters.push(...[
{parameter: 'js: Nats.JetStreamClient', jsDoc: '* @param flush ensure client is force flushed after subscribing'}
{parameter: 'js: Nats.JetStreamClient', jsDoc: '* @param flush ensure client is force flushed after subscribing'},
{parameter: 'codec: any = Nats.JSONCodec()', jsDoc: '* @param codec the serialization codec to use while transmitting the message'},
{parameter: 'options: Nats.ConsumerOptsBuilder | Partial<Nats.ConsumerOpts> = {}', jsDoc: '* @param options when setting up the subscription'}
]);

// Determine the callback process when receiving messages.
// If the message payload is null no hooks are called to process the received data.
let whenReceivingMessage = `onDataCallback(undefined, null, null, msg);`;
if (!hasNullPayload) {
whenReceivingMessage = `let receivedData: any = codec.decode(msg.data);
let whenReceivingMessage = `onDataCallback(undefined, null, parameters, msg);`;
if (channelParameters !== undefined) {
if (!hasNullPayload) {
whenReceivingMessage = `let receivedData: any = codec.decode(msg.data);
onDataCallback(undefined, ${message.type}.unmarshal(receivedData), parameters, msg);`;
}
} else {
let whenReceivingMessage = `onDataCallback(undefined, null, msg);`;
if (!hasNullPayload) {
whenReceivingMessage = `let receivedData: any = codec.decode(msg.data);
onDataCallback(undefined, ${message.type}.unmarshal(receivedData), msg);`;
}
}
}

const jsDocParameters = functionParameters.map((parameter) => parameter.jsDoc);
const parameters = functionParameters.map((parameter) => parameter.parameter);
Expand All @@ -65,27 +76,24 @@ export function ${functionName}(
${parameters.join(', ')}
): Promise<Nats.JetStreamPullSubscription> {
return new Promise(async (resolve, reject) => {
if (js !== undefined && !js.isClosed()) {
try {
const subscription = await js.pullSubscribe(${addressToUse}, options);
(async () => {
for await (const msg of subscription) {
${channelParameters !== undefined ? unwrap(addressToUse, channelParameters) : ''}
${whenReceivingMessage}
}
})();
resolve(subscription);
} catch (e: any) {
reject(e);
}
} else {
reject(new Error("Client not connected, cannot pull subscribe to ${addressToUse}"));
try {
const subscription = await js.pullSubscribe(${addressToUse}, options);
(async () => {
for await (const msg of subscription) {
${channelParameters !== undefined ? unwrap(topic, channelParameters) : ''}
${whenReceivingMessage}
}
})();
resolve(subscription);
} catch (e: any) {
reject(e);
}
});
}`;
return {
code,
functionName
functionName,
dependencies
};
}
12 changes: 5 additions & 7 deletions src/codegen/generators/typescript/parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,15 @@ export async function generateTypescriptParameters(context: TypescriptParameters
{
class: {
additionalContent: ({content, model, renderer}) => {
const address = model.originalInput['x-channel-address'];
const parameters = Object.entries(model.properties).map(([,parameter]) => {
return `channel.replace(/${parameter.unconstrainedPropertyName}/g, this.${parameter.propertyName})`;
return `channel = channel.replace(/\\{${parameter.unconstrainedPropertyName}\\}/g, this.${parameter.propertyName})`;
});
return `${content}
/**
* Realize the channel/topic with the parameters added to this class.
*/
public getChannelWithParameters() {
let channel = '${address}';
${renderer.renderBlock(parameters)}
public getChannelWithParameters(channel: string) {
${renderer.renderBlock(parameters)};
return channel;
}`;
}
Expand Down Expand Up @@ -86,6 +83,7 @@ public getChannelWithParameters() {
}

return {
channelModels: returnType
channelModels: returnType,
generator
};
}
3 changes: 2 additions & 1 deletion src/codegen/generators/typescript/payloads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export async function generateTypescriptPayload(context: TypeScriptPayloadContex
generator.outputPath,
{ exportType: 'named'},
true,
)
),
generator
);
}
Loading

0 comments on commit 22271b4

Please sign in to comment.