Skip to content

Commit c120307

Browse files
feat!: enable usage of generated app as a library without its code modification (#220)
Co-authored-by: Lukasz Gornicki <lpgornicki@gmail.com>
1 parent 8e0317e commit c120307

8 files changed

Lines changed: 477 additions & 88 deletions

File tree

README.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
- [Supported protocols](#supported-protocols)
1313
- [How to use the template](#how-to-use-the-template)
1414
* [CLI](#cli)
15+
* [Adding custom code](#adding-custom-code--handlers)
1516
- [Template configuration](#template-configuration)
1617
- [Development](#development)
1718
- [Contributors](#contributors)
@@ -100,6 +101,108 @@ $ mqtt pub -t 'smartylighting/streetlights/1/0/event/123/lighting/measured' -h '
100101
#Notice that the server automatically validates incoming messages and logs out validation errors
101102
```
102103

104+
### Adding custom code / handlers
105+
106+
It's highly recommended to treat the generated template as a library or API for initializing the server and integrating user-written handlers. Instead of directly modifying the template, leveraging it in this manner ensures that its regenerative capability is preserved. Any modifications made directly to the template would be overwritten upon regeneration.
107+
108+
Consider a scenario where you intend to introduce a new channel or section to the AsyncAPI file, followed by a template regeneration. In this case, any modifications applied within the generated code would be overwritten.
109+
110+
To avoid this, user code remains external to the generated code, functioning as an independent entity that consumes the generated code as a library. By adopting this approach, the user code remains unaffected during template regenerations.
111+
112+
Facilitating this separation involves creating handlers and associating them with their respective routes. These handlers can then be seamlessly integrated into the template's workflow by importing the appropriate methods to register the handlers. In doing so, the template's `client.register<operationId>Middleware` method becomes the bridge between the user-written handlers and the generated code. This can be used to register middlewares for specific methods on specific channels.
113+
114+
> The AsyncAPI file used for the example is [here](https://bit.ly/asyncapi)
115+
116+
```js
117+
// output refers to the generated template folder
118+
// You require the generated server. Running this code starts the server
119+
// App exposes API to send messages
120+
const { client } = require("./output");
121+
122+
// to start the app
123+
client.init();
124+
125+
// Generated handlers that we use to react on consumer / produced messages are attached to the client
126+
// through which we can register middleware functions
127+
128+
/**
129+
*
130+
*
131+
* Example of how to process a message before it is sent to the broker
132+
*
133+
*
134+
*/
135+
function testPublish() {
136+
// mosquitto_sub -h test.mosquitto.org -p 1883 -t "smartylighting/streetlights/1/0/action/12/turn/on"
137+
138+
// Registering your custom logic in a channel-specific handler
139+
// the passed handler function is called once the app sends a message to the channel
140+
// For example `client.app.send` sends a message to some channel using and before it is sent, you want to perform some other actions
141+
// in such a case, you can register middlewares like below
142+
client.registerTurnOnMiddleware((message) => { // `turnOn` is the respective operationId
143+
console.log("hitting the middleware before publishing the message");
144+
console.log(
145+
`sending turn on message to streetlight ${message.params.streetlightId}`,
146+
message.payload
147+
);
148+
});
149+
150+
client.app.send(
151+
{ command: "off" },
152+
{},
153+
"smartylighting/streetlights/1/0/action/12/turn/on"
154+
);
155+
}
156+
157+
158+
/**
159+
*
160+
*
161+
* Example of how to work with generated code as a consumer
162+
*
163+
*
164+
*/
165+
function testSubscribe() {
166+
// mosquitto_pub -h test.mosquitto.org -p 1883 -t "smartylighting/streetlights/1/0/event/101/lighting/measured" -m '{"lumens": 10}'
167+
168+
// Writing your custom logic that should be triggered when your app receives as message from a given channel
169+
// Registering your custom logic in a channel-specific handler
170+
// the passed handler functions are called once the app gets message sent to the channel
171+
172+
client.registerReceiveLightMeasurementMiddleware((message) => { // `recieveLightMeasurement` is the respective operationId
173+
console.log("recieved in middleware 1", message.payload);
174+
});
175+
176+
client.registerReceiveLightMeasurementMiddleware((message) => {
177+
console.log("recieved in middleware 2", message.payload);
178+
});
179+
}
180+
181+
testPublish();
182+
testSubscribe();
183+
184+
/**
185+
*
186+
*
187+
* Example of how to produce a message using API of generated app independently from the handlers
188+
*
189+
*
190+
*/
191+
192+
(function myLoop (i) {
193+
setTimeout(() => {
194+
console.log('producing custom message');
195+
client.app.send({percentage: 1}, {}, 'smartylighting/streetlights/1/0/action/1/turn/on');
196+
if (--i) myLoop(i);
197+
}, 1000);
198+
}(3));
199+
```
200+
201+
You can run the above code and test the working of the handlers by sending a message using the mqtt cli / mosquitto broker software to the `smartylighting/streetlights/1/0/event/123/lighting/measured` channel using this command
202+
`mosquitto_pub -h test.mosquitto.org -p 1883 -t "smartylighting/streetlights/1/0/event/101/lighting/measured" -m '{"lumens": 10, "sentAt": "2017-06-07T12:34:32.000Z"}'`
203+
or
204+
`mqtt pub -t 'smartylighting/streetlights/1/0/event/123/lighting/measured' -h 'test.mosquitto.org' -m '{"id": 1, "lumens": 3, }'` (if you are using the mqtt cli)
205+
103206
## Template configuration
104207

105208
You can configure this template by passing different parameters in the Generator CLI: `-p PARAM1_NAME=PARAM1_VALUE -p PARAM2_NAME=PARAM2_VALUE`

filters/all.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,12 @@ function trimLastChar(string) {
132132
}
133133
filter.trimLastChar = trimLastChar;
134134

135+
function convertOpertionIdToMiddlewareFn(operationId) {
136+
const capitalizedOperationId = operationId.charAt(0).toUpperCase() + operationId.slice(1);
137+
return `register${ capitalizedOperationId }Middleware`;
138+
}
139+
filter.convertOpertionIdToMiddlewareFn = convertOpertionIdToMiddlewareFn;
140+
135141
function toJS(objFromJSON, indent = 2) {
136142
if (typeof objFromJSON !== 'object' || Array.isArray(objFromJSON)) {
137143
// not an object, stringify using native function

template/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "{{ asyncapi.info().title() | kebabCase }}",
33
"description": "{{ asyncapi.info().description() | oneLine }}",
44
"version": "{{ asyncapi.info().version() }}",
5+
"main": "./src/api",
56
"scripts": {
67
"start": "node src/api/index.js"
78
},
Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,27 @@
11
{%- if channel.hasPublish() and channel.publish().ext('x-lambda') %}const fetch = require('node-fetch');{%- endif %}
22
const handler = module.exports = {};
3+
34
{% if channel.hasPublish() %}
5+
const {{ channel.publish().id() }}Middlewares = [];
6+
7+
/**
8+
* Registers a middleware function for the {{ channel.publish().id() }} operation to be executed during request processing.
9+
*
10+
* Middleware functions have access to options object that you can use to access the message content and other helper functions
11+
*
12+
* @param {function} middlewareFn - The middleware function to be registered.
13+
* @throws {TypeError} If middlewareFn is not a function.
14+
*/
15+
handler.{{ channel.publish().id() | convertOpertionIdToMiddlewareFn }} = (middlewareFn) => {
16+
if (typeof middlewareFn !== 'function') {
17+
throw new TypeError('middlewareFn must be a function');
18+
}
19+
{{ channel.publish().id() }}Middlewares.push(middlewareFn);
20+
}
21+
422
/**
523
* {{ channel.publish().summary() }}
24+
*
625
* @param {object} options
726
* @param {object} options.message
827
{%- if channel.publish().message(0).headers() %}
@@ -16,7 +35,7 @@ const handler = module.exports = {};
1635
{%- endfor %}
1736
{%- endif %}
1837
*/
19-
handler.{{ channel.publish().id() }} = async ({message}) => {
38+
handler._{{ channel.publish().id() }} = async ({message}) => {
2039
{%- if channel.publish().ext('x-lambda') %}
2140
{%- set lambda = channel.publish().ext('x-lambda') %}
2241
fetch('{{ lambda.url }}', {
@@ -30,29 +49,52 @@ handler.{{ channel.publish().id() }} = async ({message}) => {
3049
.then(json => console.log(json))
3150
.catch(err => { throw err; });
3251
{%- else %}
33-
// Implement your business logic here...
52+
for (const middleware of {{ channel.publish().id() }}Middlewares) {
53+
await middleware(message);
54+
}
3455
{%- endif %}
3556
};
3657

3758
{%- endif %}
59+
3860
{%- if channel.hasSubscribe() %}
61+
const {{ channel.subscribe().id() }}Middlewares = [];
62+
63+
/**
64+
* Registers a middleware function for the {{ channel.subscribe().id() }} operation to be executed during request processing.
65+
*
66+
* Middleware functions have access to options object that you can use to access the message content and other helper functions
67+
*
68+
* @param {function} middlewareFn - The middleware function to be registered.
69+
* @throws {TypeError} If middlewareFn is not a function.
70+
*/
71+
handler.{{ channel.subscribe().id() | convertOpertionIdToMiddlewareFn }} = (middlewareFn) => {
72+
if (typeof middlewareFn !== 'function') {
73+
throw new TypeError('middlewareFn must be a function');
74+
}
75+
{{ channel.subscribe().id() }}Middlewares.push(middlewareFn);
76+
}
77+
3978
/**
4079
* {{ channel.subscribe().summary() }}
80+
*
4181
* @param {object} options
4282
* @param {object} options.message
43-
{%- if channel.subscribe().message(0).headers() %}
44-
{%- for fieldName, field in channel.subscribe().message(0).headers().properties() %}
45-
{{ field | docline(fieldName, 'options.message.headers') }}
46-
{%- endfor %}
47-
{%- endif %}
48-
{%- if channel.subscribe().message(0).payload() %}
49-
{%- for fieldName, field in channel.subscribe().message(0).payload().properties() %}
50-
{{ field | docline(fieldName, 'options.message.payload') }}
51-
{%- endfor %}
52-
{%- endif %}
83+
{%- if channel.subscribe().message(0).headers() %}
84+
{%- for fieldName, field in channel.subscribe().message(0).headers().properties() %}
85+
{{ field | docline(fieldName, 'options.message.headers') }}
86+
{%- endfor %}
87+
{%- endif %}
88+
{%- if channel.subscribe().message(0).payload() %}
89+
{%- for fieldName, field in channel.subscribe().message(0).payload().properties() %}
90+
{{ field | docline(fieldName, 'options.message.payload') }}
91+
{%- endfor %}
92+
{%- endif %}
5393
*/
54-
handler.{{ channel.subscribe().id() }} = async ({message}) => {
55-
// Implement your business logic here...
94+
handler._{{ channel.subscribe().id() }} = async ({message}) => {
95+
for (const middleware of {{ channel.subscribe().id() }}Middlewares) {
96+
await middleware(message);
97+
}
5698
};
5799

58100
{%- endif %}

template/src/api/index.js

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,35 @@ app.useOutbound(errorLogger);
5454
app.useOutbound(logger);
5555
app.useOutbound(json2string);
5656

57-
app
58-
.listen()
59-
.then((adapters) => {
60-
console.log(cyan.underline(`${config.app.name} ${config.app.version}`), gray('is ready!'), '\n');
61-
adapters.forEach(adapter => {
62-
console.log('🔗 ', adapter.name(), gray('is connected!'));
63-
});
64-
})
65-
.catch(console.error);
57+
function init() {
58+
app
59+
.listen()
60+
.then((adapters) => {
61+
console.log(cyan.underline(`${config.app.name} ${config.app.version}`), gray('is ready!'), '\n');
62+
adapters.forEach(adapter => {
63+
console.log('🔗 ', adapter.name(), gray('is connected!'));
64+
});
65+
})
66+
.catch(console.error);
67+
}
68+
69+
const handlers = {
70+
{%- for channelName, channel in asyncapi.channels() -%}
71+
{% if channel.hasPublish() %}
72+
{{ channel.publish().id() | convertOpertionIdToMiddlewareFn }}: require('./handlers/{{ channelName | convertToFilename }}').{{ channel.publish().id() | convertOpertionIdToMiddlewareFn }},
73+
{%- endif -%}
74+
{% if channel.hasSubscribe() %}
75+
{{ channel.subscribe().id() | convertOpertionIdToMiddlewareFn }}: require('./handlers/{{ channelName | convertToFilename }}').{{ channel.subscribe().id() | convertOpertionIdToMiddlewareFn }},
76+
{% endif %}
77+
{%- endfor -%}
78+
};
79+
80+
const client = {
81+
app,
82+
init,
83+
...handlers
84+
};
85+
86+
module.exports = {
87+
client
88+
};

template/src/api/routes/$$channel$$.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ router.use('{{ channelName | toHermesTopic }}', async (message, next) => {
2929
} catch { };
3030
{% endfor -%}
3131
if (nValidated === 1) {
32-
await {{ channelName | camelCase }}Handler.{{ channel.publish().id() }}({message});
32+
await {{ channelName | camelCase }}Handler._{{ channel.publish().id() }}({message});
3333
next()
3434
} else {
3535
throw new Error(`${nValidated} of {{ channel.publish().messages().length }} message schemas matched when exactly 1 should match`);
3636
}
3737
{% else %}
3838
await validateMessage(message.payload,'{{ channelName }}','{{ channel.publish().message().name() }}','publish');
39-
await {{ channelName | camelCase }}Handler.{{ channel.publish().id() }}({message});
39+
await {{ channelName | camelCase }}Handler._{{ channel.publish().id() }}({message});
4040
next();
4141
{% endif %}
4242
} catch (e) {
@@ -61,14 +61,14 @@ router.useOutbound('{{ channelName | toHermesTopic }}', async (message, next) =>
6161
nValidated = await validateMessage(message.payload,'{{ channelName }}','{{ channel.subscribe().message(i).name() }}','subscribe', nValidated);
6262
{% endfor -%}
6363
if (nValidated === 1) {
64-
await {{ channelName | camelCase }}Handler.{{ channel.subscribe().id() }}({message});
64+
await {{ channelName | camelCase }}Handler._{{ channel.subscribe().id() }}({message});
6565
next()
6666
} else {
6767
throw new Error(`${nValidated} of {{ channel.subscribe().messages().length }} message schemas matched when exactly 1 should match`);
6868
}
6969
{% else %}
7070
await validateMessage(message.payload,'{{ channelName }}','{{ channel.subscribe().message().name() }}','subscribe');
71-
await {{ channelName | camelCase }}Handler.{{ channel.subscribe().id() }}({message});
71+
await {{ channelName | camelCase }}Handler._{{ channel.subscribe().id() }}({message});
7272
next();
7373
{% endif %}
7474
} catch (e) {

template/src/lib/message-validator.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
const path = require('path');
12
const AsyncApiValidator = require('asyncapi-validator');
23

34
// Try to parse the payload, and increment nValidated if parsing was successful.
45
module.exports.validateMessage = async (payload, channelName, messageName, operation, nValidated=0) => {
5-
const va = await AsyncApiValidator.fromSource('./asyncapi.yaml', {msgIdentifier: 'name'});
6+
const asyncApiFilePath = path.resolve(__dirname, '../../asyncapi.yaml');
7+
const va = await AsyncApiValidator.fromSource(asyncApiFilePath, {msgIdentifier: 'name'});
68
va.validate(messageName, payload, channelName, operation);
79
nValidated++;
810

0 commit comments

Comments
 (0)