Skip to content

Commit

Permalink
feat: add support for AsyncAPI v3 (#186)
Browse files Browse the repository at this point in the history
* chore: upgrade AsyncAPI generator dependency

As a first step towards updating the template to support
AsyncAPI v3 documents, this commit upgrades to the current
version of the generator.

Signed-off-by: Dale Lane <dale.lane@uk.ibm.com>

* feat: v3 support

Support for generating Java projects from v3 documents, using
the streetlights example from the spec repo.

Signed-off-by: Dale Lane <dale.lane@uk.ibm.com>

* fix: reduce complexity to satisfy linter

Moving the security protocol out to a separate function

Signed-off-by: Dale Lane <dale.lane@uk.ibm.com>

* chore: linter updates

Running the linter on the modified files

Signed-off-by: Dale Lane <dale.lane@uk.ibm.com>

* refactor: reduce duplicate code in modified test files

Signed-off-by: Dale Lane <dale.lane@uk.ibm.com>

* chore: move setTimeout outside of async functions

ref:
jestjs/jest#11543

Signed-off-by: Dale Lane <dale.lane@uk.ibm.com>

---------

Signed-off-by: Dale Lane <dale.lane@uk.ibm.com>
  • Loading branch information
dalelane authored Mar 8, 2024
1 parent e6256a8 commit 8ba6992
Show file tree
Hide file tree
Showing 29 changed files with 3,436 additions and 2,161 deletions.
31 changes: 11 additions & 20 deletions components/Common.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,9 @@ public void close() {
}

export function EnvJson({ asyncapi, params }) {
const url = asyncapi.server(params.server).url();
const protocol = asyncapi.server(params.server).protocol();
const server = asyncapi.allServers().get(params.server);
const url = server.url();
const protocol = server.protocol();
let user = params.user;
let password = params.password;

Expand All @@ -126,12 +127,13 @@ export function EnvJson({ asyncapi, params }) {
const host = URLtoHost(url);
const domain = host.split(':', 1);
let cipher = protocol === 'ibmmq-secure' ? 'ANY' : '';
const server = asyncapi.allServers().get(params.server);

if (
protocol === 'ibmmq-secure' &&
asyncapi.server(params.server).bindings().ibmmq.cipherSpec
) {
cipher = MQCipherToJava(asyncapi.server(params.server).bindings().ibmmq.cipherSpec);
protocol === 'ibmmq-secure' &&
server.bindings().get('ibmmq').value().cipherSpec
) {
cipher = MQCipherToJava(server.bindings().get('ibmmq').value().cipherSpec);
}

return `
Expand Down Expand Up @@ -176,20 +178,9 @@ import ${params.package}.models.${messageName};`;

/* Used to resolve a channel object to message name */
export function ChannelToMessage(channel, asyncapi) {
// Get payload from either publish or subscribe
const targetPayloadProperties = Object.prototype.hasOwnProperty.call(channel, 'publish') ?
channel.publish().message().payload().properties() :
channel.subscribe().message().payload().properties();

// Find message name from messages array
const messages = asyncapi.components().messages();
let targetMessageName;

for (const message in messages) {
if (messages[message].payload().properties().toString() === targetPayloadProperties.toString()) {
targetMessageName = message;
}
}
const message = channel.messages().all()[0];
const targetPayloadProperties = message.payload().properties();
const targetMessageName = message.name();

const messageNameTitleCase = targetMessageName.charAt(0).toUpperCase() + targetMessageName.slice(1);

Expand Down
84 changes: 42 additions & 42 deletions components/Connection/MQTLS.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
export function MQCipherToJava(cipher) {
// List in line with Oracle JRE mappings from https://ibm.biz/mq-cipher-mappings
const ciphers = {
ECDHE_ECDSA_3DES_EDE_CBC_SHA256: "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA",
ECDHE_ECDSA_AES_128_CBC_SHA256: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
ECDHE_ECDSA_AES_128_GCM_SHA256: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
ECDHE_ECDSA_AES_256_CBC_SHA384: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
ECDHE_ECDSA_AES_256_GCM_SHA384: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
ECDHE_ECDSA_NULL_SHA256: "TLS_ECDHE_ECDSA_WITH_NULL_SHA",
ECDHE_ECDSA_RC4_128_SHA256: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
ECDHE_RSA_3DES_EDE_CBC_SHA256: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
ECDHE_RSA_AES_128_CBC_SHA256: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
ECDHE_RSA_AES_128_GCM_SHA256: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
ECDHE_RSA_AES_256_CBC_SHA384: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384",
ECDHE_RSA_AES_256_GCM_SHA384: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
ECDHE_RSA_NULL_SHA256: "TLS_ECDHE_RSA_WITH_NULL_SHA",
ECDHE_RSA_RC4_128_SHA256: "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
TLS_RSA_WITH_3DES_EDE_CBC_SHA: "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
TLS_RSA_WITH_AES_128_CBC_SHA: "TLS_RSA_WITH_AES_128_CBC_SHA",
TLS_RSA_WITH_AES_128_CBC_SHA256: "TLS_RSA_WITH_AES_128_CBC_SHA256",
TLS_RSA_WITH_AES_128_GCM_SHA256: "TLS_RSA_WITH_AES_128_GCM_SHA256",
TLS_RSA_WITH_AES_256_CBC_SHA: "TLS_RSA_WITH_AES_256_CBC_SHA",
TLS_RSA_WITH_AES_256_CBC_SHA256: "TLS_RSA_WITH_AES_256_CBC_SHA256",
TLS_RSA_WITH_AES_256_GCM_SHA384: "TLS_RSA_WITH_AES_256_GCM_SHA384",
TLS_RSA_WITH_DES_CBC_SHA: "SSL_RSA_WITH_DES_CBC_SHA",
TLS_RSA_WITH_NULL_SHA256: "TLS_RSA_WITH_NULL_SHA256",
TLS_RSA_WITH_RC4_128_SHA256: "SSL_RSA_WITH_RC4_128_SHA",
ANY_TLS12: "*TLS12",
TLS_AES_128_GCM_SHA256: "TLS_AES_128_GCM_SHA256",
TLS_AES_256_GCM_SHA384: "TLS_AES_256_GCM_SHA384",
TLS_CHACHA20_POLY1305_SHA256: "TLS_CHACHA20_POLY1305_SHA256",
TLS_AES_128_CCM_SHA256: "TLS_AES_128_CCM_SHA256",
TLS_AES_128_CCM_8_SHA256: "TLS_AES_128_CCM_8_SHA256",
ANY: "*ANY",
ANY_TLS13: "*TLS13",
ANY_TLS12_OR_HIGHER: "*TLS12ORHIGHER",
ANY_TLS13_OR_HIGHER: "*TLS13ORHIGHER"
}
// List in line with Oracle JRE mappings from https://ibm.biz/mq-cipher-mappings
const ciphers = {
ECDHE_ECDSA_3DES_EDE_CBC_SHA256: 'TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA',
ECDHE_ECDSA_AES_128_CBC_SHA256: 'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256',
ECDHE_ECDSA_AES_128_GCM_SHA256: 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
ECDHE_ECDSA_AES_256_CBC_SHA384: 'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384',
ECDHE_ECDSA_AES_256_GCM_SHA384: 'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384',
ECDHE_ECDSA_NULL_SHA256: 'TLS_ECDHE_ECDSA_WITH_NULL_SHA',
ECDHE_ECDSA_RC4_128_SHA256: 'TLS_ECDHE_ECDSA_WITH_RC4_128_SHA',
ECDHE_RSA_3DES_EDE_CBC_SHA256: 'TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA',
ECDHE_RSA_AES_128_CBC_SHA256: 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256',
ECDHE_RSA_AES_128_GCM_SHA256: 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256',
ECDHE_RSA_AES_256_CBC_SHA384: 'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384',
ECDHE_RSA_AES_256_GCM_SHA384: 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384',
ECDHE_RSA_NULL_SHA256: 'TLS_ECDHE_RSA_WITH_NULL_SHA',
ECDHE_RSA_RC4_128_SHA256: 'TLS_ECDHE_RSA_WITH_RC4_128_SHA',
TLS_RSA_WITH_3DES_EDE_CBC_SHA: 'TLS_RSA_WITH_3DES_EDE_CBC_SHA',
TLS_RSA_WITH_AES_128_CBC_SHA: 'TLS_RSA_WITH_AES_128_CBC_SHA',
TLS_RSA_WITH_AES_128_CBC_SHA256: 'TLS_RSA_WITH_AES_128_CBC_SHA256',
TLS_RSA_WITH_AES_128_GCM_SHA256: 'TLS_RSA_WITH_AES_128_GCM_SHA256',
TLS_RSA_WITH_AES_256_CBC_SHA: 'TLS_RSA_WITH_AES_256_CBC_SHA',
TLS_RSA_WITH_AES_256_CBC_SHA256: 'TLS_RSA_WITH_AES_256_CBC_SHA256',
TLS_RSA_WITH_AES_256_GCM_SHA384: 'TLS_RSA_WITH_AES_256_GCM_SHA384',
TLS_RSA_WITH_DES_CBC_SHA: 'SSL_RSA_WITH_DES_CBC_SHA',
TLS_RSA_WITH_NULL_SHA256: 'TLS_RSA_WITH_NULL_SHA256',
TLS_RSA_WITH_RC4_128_SHA256: 'SSL_RSA_WITH_RC4_128_SHA',
ANY_TLS12: '*TLS12',
TLS_AES_128_GCM_SHA256: 'TLS_AES_128_GCM_SHA256',
TLS_AES_256_GCM_SHA384: 'TLS_AES_256_GCM_SHA384',
TLS_CHACHA20_POLY1305_SHA256: 'TLS_CHACHA20_POLY1305_SHA256',
TLS_AES_128_CCM_SHA256: 'TLS_AES_128_CCM_SHA256',
TLS_AES_128_CCM_8_SHA256: 'TLS_AES_128_CCM_8_SHA256',
ANY: '*ANY',
ANY_TLS13: '*TLS13',
ANY_TLS12_OR_HIGHER: '*TLS12ORHIGHER',
ANY_TLS13_OR_HIGHER: '*TLS13ORHIGHER'
};

if (ciphers[cipher] === undefined) {
throw new Error('An invalid cipher spec was provided. Please see https://ibm.biz/mq-cipher-mappings');
}
if (ciphers[cipher] === undefined) {
throw new Error('An invalid cipher spec was provided. Please see https://ibm.biz/mq-cipher-mappings');
}

return ciphers[cipher];
}
return ciphers[cipher];
}
3 changes: 2 additions & 1 deletion components/Connection/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const connectionModuleMap = [
];

export default function({ asyncapi, params }) {
const protocol = asyncapi.server(params.server).protocol();
const server = asyncapi.allServers().get(params.server);
const protocol = server.protocol();
const foundModule = connectionModuleMap.find(item => item.protocols.includes(protocol));
if (!foundModule) {
throw new Error(`This template does not currently support the protocol ${protocol}`);
Expand Down
42 changes: 22 additions & 20 deletions components/ConnectionHelper/KafkaConnectionHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,36 @@
* limitations under the License.
*/

function getSecurityProtocol(protocol, securitySchemeType) {
if (protocol === 'kafka') {
if (securitySchemeType) {
return 'SASL_PLAINTEXT';
}
return 'PLAINTEXT';
} else if (protocol === 'kafka-secure') {
if (securitySchemeType) {
return 'SASL_SSL';
}
return 'SSL';
}
}

function getSecurityConfig({ asyncapi, params }) {
const server = asyncapi.server(params.server);
const server = asyncapi.allServers().get(params.server);
const protocol = server.protocol();
const security = server.security();

let securitySchemeType;
if (security && security.length > 0 && asyncapi.hasComponents()) {
const securitySchemeName = Object.keys(security[0].json())[0];
const securityScheme = asyncapi.components().securityScheme(securitySchemeName);
if (securityScheme) {
securitySchemeType = securityScheme.json().type;
if (security && security.length > 0) {
const securityReq = security[0].all();
if (securityReq && securityReq.length > 0) {
securitySchemeType = securityReq[0].scheme().type();
}
}

let securityProtocol, saslMechanism, authModule;
if (protocol === 'kafka') {
if (securitySchemeType) {
securityProtocol = 'SASL_PLAINTEXT';
} else {
securityProtocol = 'PLAINTEXT';
}
} else if (protocol === 'kafka-secure') {
if (securitySchemeType) {
securityProtocol = 'SASL_SSL';
} else {
securityProtocol = 'SSL';
}
}
let securityProtocol = getSecurityProtocol(protocol, securitySchemeType);

let saslMechanism, authModule;
if (securitySchemeType) {
switch (securitySchemeType) {
case 'plain':
Expand Down
3 changes: 2 additions & 1 deletion components/ConnectionHelper/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const connectionModuleMap = [
];

export default function({ asyncapi, params }) {
const protocol = asyncapi.server(params.server).protocol();
const server = asyncapi.allServers().get(params.server);
const protocol = server.protocol();
const foundModule = connectionModuleMap.find(item => item.protocols.includes(protocol));
if (!foundModule) {
throw new Error(`This template does not currently support the protocol ${protocol}`);
Expand Down
6 changes: 4 additions & 2 deletions components/Consumer/KafkaConsumer.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function ConsumerDeclaration() {
}

export function ConsumerImports({ params, message }) {
const id = message.id() || message.name();
return `
import java.time.Duration;
import java.util.logging.*;
Expand All @@ -38,7 +39,7 @@ import ${params.package}.Connection;
import ${params.package}.PubSubBase;
import ${params.package}.models.ModelContract;
import ${params.package}.models.${toJavaClassName(message.uid())};
import ${params.package}.models.${toJavaClassName(id)};
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
Expand All @@ -47,6 +48,7 @@ import org.apache.kafka.clients.consumer.KafkaConsumer;
}

export function ReceiveMessage({ message }) {
const id = message.id() || message.name();
return `
public void receive(int requestTimeout) {
boolean continueProcessing = true;
Expand All @@ -59,7 +61,7 @@ export function ReceiveMessage({ message }) {
for (ConsumerRecord<String, String> record : records) {
logger.info("Received message: " + record.value());
${toJavaClassName(message.uid())} receivedObject = new ObjectMapper().readValue(record.value(), ${toJavaClassName(message.uid())}.class);
${toJavaClassName(id)} receivedObject = new ObjectMapper().readValue(record.value(), ${toJavaClassName(id)}.class);
logger.info("Received message type: " + receivedObject.getClass().getName());
/*
Expand Down
5 changes: 3 additions & 2 deletions components/Consumer/MQConsumer.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,12 @@ import ${params.package}.Connection;
import ${params.package}.PubSubBase;
import ${params.package}.models.ModelContract;
import ${params.package}.models.${toJavaClassName(message.uid())};
import ${params.package}.models.${toJavaClassName(message.id())};
`;
}

export function ReceiveMessage({ message }) {
const id = message.id() || message.name();
return `
public void receive(int requestTimeout) {
boolean continueProcessing = true;
Expand All @@ -73,7 +74,7 @@ export function ReceiveMessage({ message }) {
TextMessage textMessage = (TextMessage) receivedMessage;
try {
logger.info("Received message: " + textMessage.getText());
${toJavaClassName(message.uid())} receivedObject = new ObjectMapper().readValue(textMessage.getText(), ${toJavaClassName(message.uid())}.class);
${toJavaClassName(id)} receivedObject = new ObjectMapper().readValue(textMessage.getText(), ${toJavaClassName(id)}.class);
logger.info("Received message type: " + receivedObject.getClass().getName());
/*
Expand Down
3 changes: 2 additions & 1 deletion components/Consumer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const consumerModuleMap = [
];

function getModule({ asyncapi, params }) {
const protocol = asyncapi.server(params.server).protocol();
const server = asyncapi.allServers().get(params.server);
const protocol = server.protocol();
const foundModule = consumerModuleMap.find(item => item.protocols.includes(protocol));
if (!foundModule) {
throw new Error(`This template does not currently support the protocol ${protocol}`);
Expand Down
23 changes: 12 additions & 11 deletions components/Demo/Demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,24 @@ import { createJavaConstructorArgs } from '../../utils/Types.utils';
import { PackageDeclaration } from '../Common';

export function Demo(asyncapi, params) {
const channels = Object.entries(asyncapi.channels()).map(([key, value]) => ({key,value}));

const foundPubAndSub = channels.filter((el) => {
return el.value.hasPublish() && el.value.hasSubscribe();
const foundPubAndSub = asyncapi.allChannels().filterBy((chan) => {
return chan.operations().filterBySend().length > 0 &&
chan.operations().filterByReceive().length > 0;
});

const foundPubOrSub = channels.filter((el) => {
return el.value.hasPublish() || el.value.hasSubscribe();
const foundPubOrSub = asyncapi.allChannels().filterBy((chan) => {
return chan.operations().filterBySend().length > 0 ||
chan.operations().filterByReceive().length > 0;
});

// Prioritise channel with both, fallback to an OR
const channel = foundPubAndSub.length ? foundPubAndSub[0] : foundPubOrSub[0];
const channelName = channel.key;
const channelName = channel.id();

// Get payload from either publish or subscribe
const targetMessageName = channel.value.hasPublish() ? channel.value.publish().message().uid() : channel.value.subscribe().message().uid();
const targetPayloadProperties = channel.value.hasPublish() ? channel.value.publish().message().payload().properties() : channel.value.subscribe().message().payload().properties();
const message = channel.messages().all()[0];
const targetMessageName = message.id() || message.name();
const targetPayloadProperties = message.payload().properties();

const messageNameTitleCase = toJavaClassName(targetMessageName);

Expand All @@ -49,15 +50,15 @@ export function Demo(asyncapi, params) {

const constructorArgs = createJavaConstructorArgs(targetPayloadProperties).join(', ');
const generatedClasses = [];
if (channel.value.hasPublish()) {
if (channel.operations().filterBySend().length > 0) {
generatedClasses.push(
<File name={producerPath}>
<PackageDeclaration path={params.package} />
<DemoProducer params={params} messageName={messageNameTitleCase} className={className} constructorArgs={constructorArgs}></DemoProducer>
</File>
);
}
if (channel.value.hasSubscribe()) {
if (channel.operations().filterByReceive().length > 0) {
generatedClasses.push(
<File name={subscriberPath}>
<PackageDeclaration path={params.package} />
Expand Down
14 changes: 7 additions & 7 deletions components/Files/Consumers.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,22 @@ import { ConsumerDeclaration, ConsumerImports, ConsumerConstructor, ReceiveMessa
import { toJavaClassName, javaPackageToPath } from '../../utils/String.utils';

export function Consumers(asyncapi, channels, params) {
return Object.entries(channels).map(([channelName, channel]) => {
const name = channelName;
const className = `${toJavaClassName(channelName)}Subscriber`;
return channels.map((channel) => {
if (channel.operations().filterByReceive().length > 0) {
const name = channel.id();
const className = `${toJavaClassName(name)}Subscriber`;

const packagePath = javaPackageToPath(params.package);
const packagePath = javaPackageToPath(params.package);

if (channel.subscribe()) {
const message = channel.subscribe().message();
const message = channel.messages().all()[0];

return (
<File name={`${packagePath}${className}.java`}>
<PackageDeclaration path={params.package}></PackageDeclaration>
<ConsumerImports asyncapi={asyncapi} params={params} message={message}></ConsumerImports>

<Class name={className} extendsClass="PubSubBase">
<ConsumerDeclaration asyncapi={asyncapi} params={params} name={channelName} />
<ConsumerDeclaration asyncapi={asyncapi} params={params} name={name} />

<ClassConstructor name={className}>
<ConsumerConstructor asyncapi={asyncapi} params={params} name={name}/>
Expand Down
2 changes: 1 addition & 1 deletion components/Files/Models.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function Models(asyncapi, params) {
<File name={`${packagePath}models/${messageNameUpperCase}.java`}>
<PackageDeclaration path={`${params.package}.models`} />
<ImportDeclaration path={`${params.package}.models.ModelContract`} />
<ImportDeclaration path={`java.util.UUID`} />
<ImportDeclaration path={'java.util.UUID'} />

<Class name={messageNameUpperCase} extendsClass="ModelContract">
<Indent size={2} type={IndentationTypes.SPACES}>
Expand Down
10 changes: 5 additions & 5 deletions components/Files/Producers.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ import { ProducerConstructor, SendMessage, ProducerImports, ProducerDeclaration,
import { toJavaClassName, javaPackageToPath } from '../../utils/String.utils';

export function Producers(asyncapi, channels, params) {
return Object.entries(channels).map(([channelName, channel]) => {
const name = channelName;
const className = `${toJavaClassName(channelName)}Producer`;
const packagePath = javaPackageToPath(params.package);
return channels.map((channel) => {
if (channel.operations().filterBySend().length > 0) {
const name = channel.id();
const className = `${toJavaClassName(name)}Producer`;
const packagePath = javaPackageToPath(params.package);

if (channel.publish()) {
return (
<File name={`${packagePath}${className}.java`}>

Expand Down
Loading

0 comments on commit 8ba6992

Please sign in to comment.