Skip to content

Commit db808d4

Browse files
fix: dynamic topic params out of order and non-bean methods added to application.yml (#286)
* feat: Added file names to generated test folders; only beans are included in the generated application.yml file; order of variables in topic are fixed * chore: remove commented out code * chore: linting
1 parent 610f701 commit db808d4

File tree

4 files changed

+169
-32
lines changed

4 files changed

+169
-32
lines changed

filters/all.js

+45-25
Original file line numberDiff line numberDiff line change
@@ -691,8 +691,8 @@ function getBrokerSettings(asyncapi, params) {
691691
function getBindings(asyncapi, params) {
692692
const ret = {};
693693
const funcs = getFunctionSpecs(asyncapi, params);
694-
695-
funcs.forEach((spec, name, map) => {
694+
const functionsThatAreBeans = filterOutNonBeanFunctionSpecs(funcs);
695+
functionsThatAreBeans.forEach((spec, name, map) => {
696696
if (spec.isPublisher) {
697697
ret[spec.publishBindingName] = {};
698698
ret[spec.publishBindingName].destination = spec.publishChannel;
@@ -736,11 +736,31 @@ function getFunctionName(channelName, operation, isSubscriber) {
736736
return ret;
737737
}
738738

739+
function filterOutNonBeanFunctionSpecs(funcs) {
740+
const beanFunctionSpecs = new Map();
741+
const entriesIterator = funcs.entries();
742+
743+
let iterValue = entriesIterator.next();
744+
while (!iterValue.done) {
745+
const funcSpec = iterValue.value[1];
746+
747+
// This is the inverse of the condition in the Application template file
748+
// Add it if its not dynamic (hasParams = true, isPublisher = true) and type is supplier or streamBridge
749+
if (!(funcSpec.isPublisher && funcSpec.channelInfo.hasParams &&
750+
(funcSpec.type === 'supplier' || funcSpec.dynamicType === 'streamBridge'))) {
751+
beanFunctionSpecs.set(iterValue.value[0], iterValue.value[1]);
752+
}
753+
iterValue = entriesIterator.next();
754+
}
755+
return beanFunctionSpecs;
756+
}
757+
739758
// This returns the string that gets rendered in the function.definition part of application.yaml.
740759
function getFunctionDefinitions(asyncapi, params) {
741760
let ret = '';
742761
const funcs = getFunctionSpecs(asyncapi, params);
743-
const names = funcs.keys();
762+
const functionsThatAreBeans = filterOutNonBeanFunctionSpecs(funcs);
763+
const names = functionsThatAreBeans.keys();
744764
ret = Array.from(names).join(';');
745765
return ret;
746766
}
@@ -953,6 +973,14 @@ function getMessagePayloadType(message) {
953973
return ret;
954974
}
955975

976+
function sortParametersUsingChannelName(parameters, channelName) {
977+
// This doesnt work if theres two of the same variables in the channel name (scenario unlikely)
978+
parameters.forEach(param => {
979+
param.indexInChannelName = channelName.indexOf(param.rawName);
980+
});
981+
return _.sortBy(parameters, ['indexInChannelName']);
982+
}
983+
956984
// This returns the connection properties for a solace binder, for application.yaml.
957985
function getSolace(params) {
958986
const ret = {};
@@ -975,10 +1003,6 @@ function getChannelInfo(params, channelName, channel) {
9751003
let publishChannel = String(channelName);
9761004
let subscribeChannel = String(channelName);
9771005
const parameters = [];
978-
let functionParamList = '';
979-
let functionArgList = '';
980-
let sampleArgList = '';
981-
let first = true;
9821006

9831007
debugChannel('parameters:');
9841008
debugChannel(channel.parameters());
@@ -987,39 +1011,35 @@ function getChannelInfo(params, channelName, channel) {
9871011
const parameter = channel.parameter(name);
9881012
const schema = parameter.schema();
9891013
const type = getType(schema.type(), schema.format());
990-
const param = { name: _.camelCase(name) };
1014+
const param = {
1015+
name: _.camelCase(name),
1016+
rawName: name
1017+
};
9911018
debugChannel(`name: ${name} type:`);
9921019
debugChannel(type);
9931020
let sampleArg = 1;
9941021

995-
// Figure out what position it's in. This is just for the parameterToHeader feature.
1022+
subscribeChannel = subscribeChannel.replace(nameWithBrackets, '*');
1023+
1024+
// Figure out what channel part it's in. This is just for the parameterToHeader feature.
9961025
for (let i = 0; i < channelParts.length; i++) {
9971026
if (channelParts[i] === nameWithBrackets) {
9981027
param.position = i;
9991028
break;
10001029
}
10011030
}
10021031

1003-
if (first) {
1004-
first = false;
1005-
} else {
1006-
functionParamList += ', ';
1007-
functionArgList += ', ';
1008-
}
1009-
1010-
sampleArgList += ', ';
10111032
[publishChannel, sampleArg] = handleParameterType(name, param, type, publishChannel, schema, nameWithBrackets);
1012-
subscribeChannel = subscribeChannel.replace(nameWithBrackets, '*');
1013-
functionParamList += `${param.type} ${param.name}`;
1014-
functionArgList += param.name;
1015-
sampleArgList += sampleArg;
1033+
param.sampleArg = sampleArg;
10161034
parameters.push(param);
10171035
}
1018-
ret.functionArgList = functionArgList;
1019-
ret.functionParamList = functionParamList;
1020-
ret.sampleArgList = sampleArgList;
1036+
// The channel parameters aren't in any particular order when they come in.
1037+
// This means, to be safe, we need to order them like how it is in the channel name.
1038+
ret.parameters = sortParametersUsingChannelName(parameters, channelName);
1039+
ret.functionArgList = ret.parameters.map(param => param.name).join(', ');
1040+
ret.functionParamList = ret.parameters.map(param => `${param.type} ${param.name}`).join(', ');
1041+
ret.sampleArgList = ret.parameters.map(param => param.sampleArg).join(', ');
10211042
ret.channelName = channelName;
1022-
ret.parameters = parameters;
10231043
ret.publishChannel = publishChannel;
10241044
ret.subscribeChannel = subscribeChannel;
10251045
ret.hasParams = parameters.length > 0;

test/__snapshots__/integration.test.js.snap

+55-6
Original file line numberDiff line numberDiff line change
@@ -481,11 +481,9 @@ exports[`template integration tests using the generator should generate applicat
481481
"spring:
482482
cloud:
483483
function:
484-
definition: testLevel1MessageIdOperationSupplier;testLevel1MessageIdOperationConsumer
484+
definition: testLevel1MessageIdOperationConsumer
485485
stream:
486486
bindings:
487-
testLevel1MessageIdOperationSupplier-out-0:
488-
destination: 'testLevel1/{messageId}/{operation}'
489487
testLevel1MessageIdOperationConsumer-in-0:
490488
destination: testLevel1/*/*
491489
binders:
@@ -985,11 +983,9 @@ exports[`template integration tests using the generator should generate extra co
985983
input-header-mapping-expression:
986984
messageId: 'headers.solace_destination.getName.split(\\"/\\")[1]'
987985
operation: 'headers.solace_destination.getName.split(\\"/\\")[2]'
988-
definition: testLevel1MessageIdOperationSupplier;testLevel1MessageIdOperationConsumer
986+
definition: testLevel1MessageIdOperationConsumer
989987
stream:
990988
bindings:
991-
testLevel1MessageIdOperationSupplier-out-0:
992-
destination: 'testLevel1/{messageId}/{operation}'
993989
testLevel1MessageIdOperationConsumer-in-0:
994990
destination: testLevel1/*/*
995991
binders:
@@ -1599,6 +1595,22 @@ public class Debtor {
15991595
"
16001596
`;
16011597
1598+
exports[`template integration tests using the generator should not populate application yml with functions that are not beans 1`] = `
1599+
"spring:
1600+
cloud:
1601+
function:
1602+
definition: ''
1603+
stream:
1604+
bindings: {}
1605+
logging:
1606+
level:
1607+
root: info
1608+
org:
1609+
springframework: info
1610+
1611+
"
1612+
`;
1613+
16021614
exports[`template integration tests using the generator should package and import schemas in another avro namespace 1`] = `
16031615
"
16041616
@@ -1802,6 +1814,43 @@ public class JobAcknowledge {
18021814
"
18031815
`;
18041816
1817+
exports[`template integration tests using the generator should place the topic variables in the correct order 1`] = `
1818+
"
1819+
1820+
1821+
import org.slf4j.Logger;
1822+
import org.slf4j.LoggerFactory;
1823+
import org.springframework.beans.factory.annotation.Autowired;
1824+
import org.springframework.boot.SpringApplication;
1825+
import org.springframework.boot.autoconfigure.SpringBootApplication;
1826+
import org.springframework.cloud.stream.function.StreamBridge;
1827+
import org.springframework.messaging.Message;
1828+
import org.springframework.messaging.support.MessageBuilder;
1829+
1830+
@SpringBootApplication
1831+
public class Application {
1832+
1833+
private static final Logger logger = LoggerFactory.getLogger(Application.class);
1834+
1835+
@Autowired
1836+
private StreamBridge streamBridge;
1837+
1838+
public static void main(String[] args) {
1839+
SpringApplication.run(Application.class);
1840+
}
1841+
1842+
1843+
public void sendAcmeBillingReceiptsReceiptIdCreatedVersionRegionsRegionChargifyRideId(
1844+
RideReceipt payload, String receiptId, String version, String region, String rideId
1845+
) {
1846+
String topic = String.format(\\"acme/billing/receipts/%s/created/%s/regions/%s/chargify/%s\\",
1847+
receiptId, version, region, rideId);
1848+
streamBridge.send(topic, payload);
1849+
}
1850+
}
1851+
"
1852+
`;
1853+
18051854
exports[`template integration tests using the generator should return object when avro union type is used specifying many possible types 1`] = `
18061855
"package com.example.api.jobOrder;
18071856

test/integration.test.js

+24-1
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ const Generator = require('@asyncapi/generator');
33
const { readFile } = require('fs').promises;
44
const crypto = require('crypto');
55

6+
const TEST_SUITE_NAME = 'template integration tests using the generator';
67
// Constants not overridden per test
78
const TEST_FOLDER_NAME = 'test';
89
const MAIN_TEST_RESULT_PATH = path.join(TEST_FOLDER_NAME, 'temp', 'integrationTestResult');
910

11+
// Unfortunately, the test suite name must be a hard coded string
1012
describe('template integration tests using the generator', () => {
1113
jest.setTimeout(30000);
1214

@@ -18,7 +20,8 @@ describe('template integration tests using the generator', () => {
1820

1921
const generateFolderName = () => {
2022
// we always want to generate to new directory to make sure test runs in clear environment
21-
return path.resolve(MAIN_TEST_RESULT_PATH, crypto.randomBytes(4).toString('hex'));
23+
const testName = expect.getState().currentTestName.substring(TEST_SUITE_NAME.length + 1);
24+
return path.resolve(MAIN_TEST_RESULT_PATH, `${testName } - ${ crypto.randomBytes(4).toString('hex')}`);
2225
};
2326

2427
const generate = (asyncApiFilePath, params) => {
@@ -192,4 +195,24 @@ describe('template integration tests using the generator', () => {
192195
];
193196
await assertExpectedFiles(validatedFiles);
194197
});
198+
199+
it('should place the topic variables in the correct order', async () => {
200+
// For a topic of test/{var1}/{var2}, the listed params in the asyncapi document can be in any order
201+
await generate('mocks/multivariable-topic.yaml');
202+
203+
const validatedFiles = [
204+
'src/main/java/Application.java'
205+
];
206+
await assertExpectedFiles(validatedFiles);
207+
});
208+
209+
it('should not populate application yml with functions that are not beans', async () => {
210+
// If the function is a supplier or using a stream bridge, the function isn't a bean and shouldnt be in application.yml
211+
await generate('mocks/multivariable-topic.yaml');
212+
213+
const validatedFiles = [
214+
'src/main/resources/application.yml'
215+
];
216+
await assertExpectedFiles(validatedFiles);
217+
});
195218
});

test/mocks/multivariable-topic.yaml

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
components:
2+
schemas:
3+
RideReceipt:
4+
$schema: 'http://json-schema.org/draft-07/schema#'
5+
type: object
6+
title: This schema is irrelevant
7+
$id: 'http://example.com/root.json'
8+
messages:
9+
Billing Receipt Created:
10+
payload:
11+
$ref: '#/components/schemas/RideReceipt'
12+
schemaFormat: application/vnd.aai.asyncapi+json;version=2.0.0
13+
contentType: application/json
14+
channels:
15+
'acme/billing/receipts/{receipt_id}/created/{version}/regions/{region}/chargify/{ride_id}':
16+
subscribe:
17+
bindings:
18+
solace:
19+
bindingVersion: 0.1.0
20+
destinations:
21+
- destinationType: topic
22+
message:
23+
$ref: '#/components/messages/Billing Receipt Created'
24+
parameters:
25+
version:
26+
schema:
27+
type: string
28+
receipt_id:
29+
schema:
30+
type: string
31+
ride_id:
32+
schema:
33+
type: string
34+
region:
35+
schema:
36+
type: string
37+
enum:
38+
- US
39+
- UK
40+
- CA
41+
- MX
42+
asyncapi: 2.0.0
43+
info:
44+
title: ExpenseReportingIntegrationApplication
45+
version: 0.0.1

0 commit comments

Comments
 (0)