Skip to content

Commit 8594d04

Browse files
feat: add config that sets headers to topic param values on incoming messages. (#143)
* feat: add config that sets headers to topic param values on incoming messages. * Fixes for solarcloud. * Updated README to document the new paramatersAsHeaders parameter. Simplified one generated function. * Added rabbit support for the parametersToHeaders feature. Renamed 'topic' to 'channel' in variable names to keep it generic. * Refactored to pass sonarCloud * feat: add config that sets headers to topic param values on incoming messages. * Fixes for solarcloud. * Updated README to document the new paramatersAsHeaders parameter. Simplified one generated function. * Added rabbit support for the parametersToHeaders feature. Renamed 'topic' to 'channel' in variable names to keep it generic. * Refactored to pass sonarCloud * Updated description of parametersToHeaders in package.json * package.json was missing a comma. Co-authored-by: Michael Davis <[email protected]>
1 parent 3be1d35 commit 8594d04

File tree

6 files changed

+253
-82
lines changed

6 files changed

+253
-82
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ host | | tcp://localhost:55555 | The host connection property. Currently this on
9696
javaPackage | info.x-java-package | | The Java package of the generated classes. If not set then the classes will be in the default package.
9797
msgVpn | | default | The message vpn connection property. Currently this only works with the Solace binder. When other binders are used this parameter is ignored.
9898
password | | default | The client password connection property. Currently this only works with the Solace binder. When other binders are used this parameter is ignored.
99+
parametersToHeaders | | false | If true, this will create headers on the incoming messages for each channel parameter. Currently this only works with messages originating from Solace (using the solace_destination header) and RabbitMQ (using the amqp_receivedRoutingKey header.)
99100
reactive | | false | If true, the generated functions will use the Reactive style and use the Flux class.
100101
solaceSpringCloudVersion | info.x-solace-spring-cloud-version | 2.1.0 | The version of the solace-spring-cloud-bom dependency used when generating an application.
101102
springBootVersion | info.x-spring-boot-version | 2.4.7 | The version of Spring Boot used when generating an application.

filters/all.js

Lines changed: 117 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@ const _ = require('lodash');
44
const ScsLib = require('../lib/scsLib.js');
55
const scsLib = new ScsLib();
66
// To enable debug logging, set the env var DEBUG="type function" with whatever things you want to see.
7-
const debugAppProperties = require('debug')('appProperties');
87
const debugDynamic = require('debug')('dynamic');
98
const debugFunction = require('debug')('function');
109
const debugJavaClass = require('debug')('javaClass');
1110
const debugPayload = require('debug')('payload');
1211
const debugProperty = require('debug')('property');
13-
const debugTopic = require('debug')('topic');
12+
const debugChannel = require('debug')('channel');
1413
const debugType = require('debug')('type');
1514

1615
const stringMap = new Map();
@@ -79,6 +78,8 @@ class SCSFunction {
7978
if (this.type === 'consumer' || (this.type === 'function' && this.dynamic && this.dynamicType === 'streamBridge')) {
8079
if (this.reactive) {
8180
ret = `public Consumer<Flux<${this.subscribePayload}>> ${this.name}()`;
81+
} else if (this.dynamic && this.parametersToHeaders) {
82+
ret = `public Consumer<Message<${this.subscribePayload}>> ${this.name}()`;
8283
} else {
8384
ret = `public Consumer<${this.subscribePayload}> ${this.name}()`;
8485
}
@@ -142,6 +143,13 @@ function appProperties([asyncapi, params]) {
142143
doc.spring.cloud = {};
143144
const cloud = doc.spring.cloud;
144145
cloud.function = {};
146+
147+
// See if we have dynamic functions, and if the parametersToHeaders param is set.
148+
// If so, add the input-header-mapping-expression config to consumers which consume dynamic channels.
149+
if (params.parametersToHeaders) {
150+
handleParametersToHeaders(asyncapi, params, cloud);
151+
}
152+
145153
debugProperty('appProperties getFunctionDefinitions');
146154
cloud.function.definition = getFunctionDefinitions(asyncapi, params);
147155
cloud.stream = {};
@@ -190,6 +198,34 @@ function appProperties([asyncapi, params]) {
190198
}
191199
filter.appProperties = appProperties;
192200

201+
function handleParametersToHeaders(asyncapi, params, cloud) {
202+
const dynamicFuncs = getDynamicFunctions([asyncapi, params]);
203+
204+
if (dynamicFuncs && (params.binder === 'solace' || params.binder === 'rabbit')) {
205+
cloud.function.configuration = {};
206+
const funcs = getFunctionSpecs(asyncapi, params);
207+
208+
funcs.forEach((spec, name, map) => {
209+
if (spec.dynamic && spec.type === 'consumer') {
210+
cloud.function.configuration[name] = {};
211+
cloud.function.configuration[name]['input-header-mapping-expression'] = {};
212+
const headerConfig = cloud.function.configuration[name]['input-header-mapping-expression'];
213+
addHeaderConfigs(params, spec.channelInfo, headerConfig);
214+
}
215+
});
216+
}
217+
}
218+
219+
function addHeaderConfigs(params, channelInfo, headerConfig) {
220+
for (const param of channelInfo.parameters) {
221+
if (params.binder === 'solace') {
222+
headerConfig[param.name] = `headers.solace_destination.getName.split("/")[${param.position}]`;
223+
} else if (params.binder === 'rabbit') {
224+
headerConfig[param.name] = `headers.amqp_receivedRoutingKey.getName.split("/")[${param.position}]`;
225+
}
226+
}
227+
}
228+
193229
function artifactId([info, params]) {
194230
return scsLib.getParamOrDefault(info, params, 'artifactId', 'x-artifact-id');
195231
}
@@ -338,7 +374,7 @@ function functionSpecs([asyncapi, params]) {
338374
}
339375
filter.functionSpecs = functionSpecs;
340376

341-
// This returns the non-SCS type functions for sending to dynamic topics.
377+
// This returns the non-SCS type functions for sending to dynamic channels.
342378
function getDynamicFunctions([asyncapi, params]) {
343379
const functionMap = new Map();
344380
debugDynamic('start:');
@@ -350,10 +386,10 @@ function getDynamicFunctions([asyncapi, params]) {
350386
if (publisher) {
351387
debugDynamic('found publisher:');
352388
debugDynamic(publisher);
353-
const topicInfo = getTopicInfo(channelName, channel);
354-
if (topicInfo.hasParams) {
389+
const channelInfo = getChannelInfo(params, channelName, channel);
390+
if (channelInfo.hasParams) {
355391
const spec = {};
356-
spec.topicInfo = topicInfo;
392+
spec.channelInfo = channelInfo;
357393
spec.payloadClass = getPayloadClass(publisher);
358394
spec.sendMethodName = getSendFunctionName(channelName, publisher);
359395
functionMap.set(spec.sendMethodName, spec);
@@ -548,7 +584,7 @@ function getAdditionalSubs(asyncapi, params) {
548584
let ret;
549585
const funcs = getFunctionSpecs(asyncapi, params);
550586
funcs.forEach((spec, name, map) => {
551-
debugAppProperties(`getAdditionalSubs: ${spec.name} ${spec.isQueueWithSubscription} ${spec.additionalSubscriptions}`);
587+
debugProperty(`getAdditionalSubs: ${spec.name} ${spec.isQueueWithSubscription} ${spec.additionalSubscriptions}`);
552588
// The first additional subscription will be the destination. If there is more than one the rest go here.
553589
if (spec.isQueueWithSubscription && spec.additionalSubscriptions.length > 1) {
554590
if (!ret) {
@@ -653,15 +689,16 @@ function getFunctionSpecs(asyncapi, params) {
653689
functionSpec.type = 'function';
654690
debugFunction('Found existing subscriber, so this is a function.');
655691
} else {
656-
const topicInfo = getTopicInfo(channelName, channel);
692+
const channelInfo = getChannelInfo(params, channelName, channel);
657693
functionSpec = new SCSFunction();
658694
functionSpec.name = name;
659695
functionSpec.type = 'supplier';
660696
functionSpec.reactive = reactive;
661-
functionSpec.dynamic = topicInfo.hasParams;
662-
functionSpec.topicInfo = topicInfo;
697+
functionSpec.dynamic = channelInfo.hasParams;
698+
functionSpec.channelInfo = channelInfo;
663699
functionSpec.sendMethodName = getSendFunctionName(channelName, publish);
664700
functionSpec.dynamicType = params.dynamicType;
701+
functionSpec.parametersToHeaders = params.parametersToHeaders;
665702
functionMap.set(name, functionSpec);
666703
}
667704
const payload = getPayloadClass(publish);
@@ -685,7 +722,7 @@ function getFunctionSpecs(asyncapi, params) {
685722
debugFunction(`This already exists: ${name} isQueueWithSubscription: ${functionSpec.isQueueWithSubscription}`);
686723
if (functionSpec.isQueueWithSubscription) { // This comes from an smf binding to a queue.
687724
debugFunction(functionSpec);
688-
for (const sub of smfBinding.topicSubscriptions) {
725+
for (const sub of smfBinding.channelSubscriptions) {
689726
let foundIt = false;
690727
for (const existingSub of functionSpec.additionalSubscriptions) {
691728
debugFunction(`Comparing ${sub} to ${existingSub}`);
@@ -718,10 +755,15 @@ function getFunctionSpecs(asyncapi, params) {
718755
}
719756
} else {
720757
debugFunction('This is a new one.');
758+
const channelInfo = getChannelInfo(params, channelName, channel);
721759
functionSpec = new SCSFunction();
722760
functionSpec.name = name;
723761
functionSpec.type = 'consumer';
724762
functionSpec.reactive = reactive;
763+
functionSpec.dynamic = channelInfo.hasParams;
764+
functionSpec.channelInfo = channelInfo;
765+
functionSpec.dynamicType = params.dynamicType;
766+
functionSpec.parametersToHeaders = params.parametersToHeaders;
725767
functionMap.set(name, functionSpec);
726768
if (smfBinding && smfBinding.queueName && smfBinding.topicSubscriptions) {
727769
debugFunction(`A new one with subscriptions: ${smfBinding.topicSubscriptions}`);
@@ -749,10 +791,10 @@ function getFunctionSpecs(asyncapi, params) {
749791
functionSpec.subscribeChannel = dest;
750792
} else if (functionSpec.isQueueWithSubscription) {
751793
functionSpec.subscribeChannel = functionSpec.additionalSubscriptions[0];
752-
debugFunction(`Setting subscribeChannel for topicWithSubs: ${functionSpec.subscribeChannel}`);
794+
debugFunction(`Setting subscribeChannel for channelWithSubs: ${functionSpec.subscribeChannel}`);
753795
} else {
754-
const topicInfo = getTopicInfo(channelName, channel);
755-
functionSpec.subscribeChannel = topicInfo.subscribeTopic;
796+
const channelInfo = getChannelInfo(params, channelName, channel);
797+
functionSpec.subscribeChannel = channelInfo.subscribeChannel;
756798
}
757799
}
758800

@@ -811,29 +853,42 @@ function getSolace(params) {
811853
return ret;
812854
}
813855

814-
// This returns an object containing information the template needs to render topic strings.
815-
function getTopicInfo(channelName, channel) {
856+
// This returns an object containing information the template needs to render channel strings.
857+
function getChannelInfo(params, channelName, channel) {
816858
const ret = {};
817-
let publishTopic = String(channelName);
818-
let subscribeTopic = String(channelName);
819-
const params = [];
859+
860+
// This isfor the parameterToHeader feature.
861+
const delimiter = (params.binder === 'rabbit' || params.binder === 'kafka') ? '.' : '/';
862+
const channelParts = channelName.split(delimiter);
863+
864+
let publishChannel = String(channelName);
865+
let subscribeChannel = String(channelName);
866+
const parameters = [];
820867
let functionParamList = '';
821868
let functionArgList = '';
822869
let sampleArgList = '';
823870
let first = true;
824871

825-
debugTopic('params:');
826-
debugTopic(channel.parameters());
872+
debugChannel('parameters:');
873+
debugChannel(channel.parameters());
827874
for (const name in channel.parameters()) {
828-
const nameWithBrackets = `{${ name }}`;
875+
const nameWithBrackets = `{${name}}`;
829876
const parameter = channel.parameter(name);
830877
const schema = parameter.schema();
831878
const type = getType(schema.type(), schema.format());
832879
const param = { name: _.camelCase(name) };
833-
debugTopic(`name: ${name} type:`);
834-
debugTopic(type);
880+
debugChannel(`name: ${name} type:`);
881+
debugChannel(type);
835882
let sampleArg = 1;
836883

884+
// Figure out what position it's in. This is just for the parameterToHeader feature.
885+
for (let i = 0; i < channelParts.length; i++) {
886+
if (channelParts[i] === nameWithBrackets) {
887+
param.position = i;
888+
break;
889+
}
890+
}
891+
837892
if (first) {
838893
first = false;
839894
} else {
@@ -842,51 +897,55 @@ function getTopicInfo(channelName, channel) {
842897
}
843898

844899
sampleArgList += ', ';
845-
846-
if (type) {
847-
debugTopic('It is a type:');
848-
debugTopic(type);
849-
const javaType = type.javaType || typeMap.get(type);
850-
if (!javaType) throw new Error(`topicInfo filter: type not found in typeMap: ${type}`);
851-
param.type = javaType;
852-
const printfArg = type.printFormat;
853-
debugTopic(`printf: ${printfArg}`);
854-
if (!printfArg) throw new Error(`topicInfo filter: printFormat not found in formatMap: ${type}`);
855-
debugTopic(`Replacing ${nameWithBrackets}`);
856-
publishTopic = publishTopic.replace(nameWithBrackets, printfArg);
857-
sampleArg = type.sample;
858-
} else {
859-
const en = schema.enum();
860-
if (en) {
861-
debugTopic(`It is an enum: ${en}`);
862-
param.type = _.upperFirst(name);
863-
param.enum = en;
864-
sampleArg = `Messaging.${param.type}.${en[0]}`;
865-
debugTopic(`Replacing ${nameWithBrackets}`);
866-
publishTopic = publishTopic.replace(nameWithBrackets, '%s');
867-
} else {
868-
throw new Error(`topicInfo filter: Unknown parameter type: ${ JSON.stringify(schema)}`);
869-
}
870-
}
871-
872-
param.sampleArg = sampleArg;
873-
subscribeTopic = subscribeTopic.replace(nameWithBrackets, '*');
900+
[publishChannel, sampleArg] = handleParameterType(name, param, type, publishChannel, schema, nameWithBrackets);
901+
subscribeChannel = subscribeChannel.replace(nameWithBrackets, '*');
874902
functionParamList += `${param.type} ${param.name}`;
875903
functionArgList += param.name;
876904
sampleArgList += sampleArg;
877-
params.push(param);
905+
parameters.push(param);
878906
}
879907
ret.functionArgList = functionArgList;
880908
ret.functionParamList = functionParamList;
881909
ret.sampleArgList = sampleArgList;
882910
ret.channelName = channelName;
883-
ret.params = params;
884-
ret.publishTopic = publishTopic;
885-
ret.subscribeTopic = subscribeTopic;
886-
ret.hasParams = params.length > 0;
911+
ret.parameters = parameters;
912+
ret.publishChannel = publishChannel;
913+
ret.subscribeChannel = subscribeChannel;
914+
ret.hasParams = parameters.length > 0;
887915
return ret;
888916
}
889917

918+
function handleParameterType(name, param, type, publishChannel, schema, nameWithBrackets) {
919+
let sampleArg = 1;
920+
if (type) {
921+
debugChannel('It is a type:');
922+
debugChannel(type);
923+
const javaType = type.javaType || typeMap.get(type);
924+
if (!javaType) throw new Error(`channelInfo filter: type not found in typeMap: ${type}`);
925+
param.type = javaType;
926+
const printfArg = type.printFormat;
927+
debugChannel(`printf: ${printfArg}`);
928+
if (!printfArg) throw new Error(`channelInfo filter: printFormat not found in formatMap: ${type}`);
929+
debugChannel(`Replacing ${nameWithBrackets}`);
930+
publishChannel = publishChannel.replace(nameWithBrackets, printfArg);
931+
sampleArg = type.sample;
932+
} else {
933+
const en = schema.enum();
934+
if (en) {
935+
debugChannel(`It is an enum: ${en}`);
936+
param.type = _.upperFirst(name);
937+
param.enum = en;
938+
sampleArg = `Messaging.${param.type}.${en[0]}`;
939+
debugChannel(`Replacing ${nameWithBrackets}`);
940+
publishChannel = publishChannel.replace(nameWithBrackets, '%s');
941+
} else {
942+
throw new Error(`channelInfo filter: Unknown parameter type: ${ JSON.stringify(schema)}`);
943+
}
944+
}
945+
param.sampleArg = sampleArg;
946+
return [publishChannel, sampleArg];
947+
}
948+
890949
function indent(numTabs) {
891950
return '\t'.repeat(numTabs);
892951
}

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@
111111
"required": false,
112112
"default": "default"
113113
},
114+
"parametersToHeaders": {
115+
"description": "If true, this will create headers on the incoming messages for each channel parameter. Currently this only works with messages originating from Solace (using the solace_destination header) and RabbitMQ (using the amqp_receivedRoutingKey header.)",
116+
"required": false,
117+
"default": false
118+
},
114119
"password": {
115120
"description": "The client password connection property. Currently this only works with the Solace binder.",
116121
"required": false,

0 commit comments

Comments
 (0)