Skip to content

Commit

Permalink
feat!: enable usage of generated app as a library without its code mo…
Browse files Browse the repository at this point in the history
…dification (#220)

Co-authored-by: Lukasz Gornicki <lpgornicki@gmail.com>
  • Loading branch information
kaushik-rishi and derberg authored Oct 3, 2023
1 parent 8e0317e commit c120307
Show file tree
Hide file tree
Showing 8 changed files with 477 additions and 88 deletions.
103 changes: 103 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- [Supported protocols](#supported-protocols)
- [How to use the template](#how-to-use-the-template)
* [CLI](#cli)
* [Adding custom code](#adding-custom-code--handlers)
- [Template configuration](#template-configuration)
- [Development](#development)
- [Contributors](#contributors)
Expand Down Expand Up @@ -100,6 +101,108 @@ $ mqtt pub -t 'smartylighting/streetlights/1/0/event/123/lighting/measured' -h '
#Notice that the server automatically validates incoming messages and logs out validation errors
```

### Adding custom code / handlers

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.

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.

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.

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.

> The AsyncAPI file used for the example is [here](https://bit.ly/asyncapi)
```js
// output refers to the generated template folder
// You require the generated server. Running this code starts the server
// App exposes API to send messages
const { client } = require("./output");

// to start the app
client.init();

// Generated handlers that we use to react on consumer / produced messages are attached to the client
// through which we can register middleware functions

/**
*
*
* Example of how to process a message before it is sent to the broker
*
*
*/
function testPublish() {
// mosquitto_sub -h test.mosquitto.org -p 1883 -t "smartylighting/streetlights/1/0/action/12/turn/on"

// Registering your custom logic in a channel-specific handler
// the passed handler function is called once the app sends a message to the channel
// For example `client.app.send` sends a message to some channel using and before it is sent, you want to perform some other actions
// in such a case, you can register middlewares like below
client.registerTurnOnMiddleware((message) => { // `turnOn` is the respective operationId
console.log("hitting the middleware before publishing the message");
console.log(
`sending turn on message to streetlight ${message.params.streetlightId}`,
message.payload
);
});

client.app.send(
{ command: "off" },
{},
"smartylighting/streetlights/1/0/action/12/turn/on"
);
}


/**
*
*
* Example of how to work with generated code as a consumer
*
*
*/
function testSubscribe() {
// mosquitto_pub -h test.mosquitto.org -p 1883 -t "smartylighting/streetlights/1/0/event/101/lighting/measured" -m '{"lumens": 10}'

// Writing your custom logic that should be triggered when your app receives as message from a given channel
// Registering your custom logic in a channel-specific handler
// the passed handler functions are called once the app gets message sent to the channel

client.registerReceiveLightMeasurementMiddleware((message) => { // `recieveLightMeasurement` is the respective operationId
console.log("recieved in middleware 1", message.payload);
});

client.registerReceiveLightMeasurementMiddleware((message) => {
console.log("recieved in middleware 2", message.payload);
});
}

testPublish();
testSubscribe();

/**
*
*
* Example of how to produce a message using API of generated app independently from the handlers
*
*
*/

(function myLoop (i) {
setTimeout(() => {
console.log('producing custom message');
client.app.send({percentage: 1}, {}, 'smartylighting/streetlights/1/0/action/1/turn/on');
if (--i) myLoop(i);
}, 1000);
}(3));
```

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
`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"}'`
or
`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)

## Template configuration

You can configure this template by passing different parameters in the Generator CLI: `-p PARAM1_NAME=PARAM1_VALUE -p PARAM2_NAME=PARAM2_VALUE`
Expand Down
6 changes: 6 additions & 0 deletions filters/all.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ function trimLastChar(string) {
}
filter.trimLastChar = trimLastChar;

function convertOpertionIdToMiddlewareFn(operationId) {
const capitalizedOperationId = operationId.charAt(0).toUpperCase() + operationId.slice(1);
return `register${ capitalizedOperationId }Middleware`;
}
filter.convertOpertionIdToMiddlewareFn = convertOpertionIdToMiddlewareFn;

function toJS(objFromJSON, indent = 2) {
if (typeof objFromJSON !== 'object' || Array.isArray(objFromJSON)) {
// not an object, stringify using native function
Expand Down
1 change: 1 addition & 0 deletions template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "{{ asyncapi.info().title() | kebabCase }}",
"description": "{{ asyncapi.info().description() | oneLine }}",
"version": "{{ asyncapi.info().version() }}",
"main": "./src/api",
"scripts": {
"start": "node src/api/index.js"
},
Expand Down
70 changes: 56 additions & 14 deletions template/src/api/handlers/$$channel$$.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
{%- if channel.hasPublish() and channel.publish().ext('x-lambda') %}const fetch = require('node-fetch');{%- endif %}
const handler = module.exports = {};

{% if channel.hasPublish() %}
const {{ channel.publish().id() }}Middlewares = [];

/**
* Registers a middleware function for the {{ channel.publish().id() }} operation to be executed during request processing.
*
* Middleware functions have access to options object that you can use to access the message content and other helper functions
*
* @param {function} middlewareFn - The middleware function to be registered.
* @throws {TypeError} If middlewareFn is not a function.
*/
handler.{{ channel.publish().id() | convertOpertionIdToMiddlewareFn }} = (middlewareFn) => {
if (typeof middlewareFn !== 'function') {
throw new TypeError('middlewareFn must be a function');
}
{{ channel.publish().id() }}Middlewares.push(middlewareFn);
}

/**
* {{ channel.publish().summary() }}
*
* @param {object} options
* @param {object} options.message
{%- if channel.publish().message(0).headers() %}
Expand All @@ -16,7 +35,7 @@ const handler = module.exports = {};
{%- endfor %}
{%- endif %}
*/
handler.{{ channel.publish().id() }} = async ({message}) => {
handler._{{ channel.publish().id() }} = async ({message}) => {
{%- if channel.publish().ext('x-lambda') %}
{%- set lambda = channel.publish().ext('x-lambda') %}
fetch('{{ lambda.url }}', {
Expand All @@ -30,29 +49,52 @@ handler.{{ channel.publish().id() }} = async ({message}) => {
.then(json => console.log(json))
.catch(err => { throw err; });
{%- else %}
// Implement your business logic here...
for (const middleware of {{ channel.publish().id() }}Middlewares) {
await middleware(message);
}
{%- endif %}
};

{%- endif %}

{%- if channel.hasSubscribe() %}
const {{ channel.subscribe().id() }}Middlewares = [];

/**
* Registers a middleware function for the {{ channel.subscribe().id() }} operation to be executed during request processing.
*
* Middleware functions have access to options object that you can use to access the message content and other helper functions
*
* @param {function} middlewareFn - The middleware function to be registered.
* @throws {TypeError} If middlewareFn is not a function.
*/
handler.{{ channel.subscribe().id() | convertOpertionIdToMiddlewareFn }} = (middlewareFn) => {
if (typeof middlewareFn !== 'function') {
throw new TypeError('middlewareFn must be a function');
}
{{ channel.subscribe().id() }}Middlewares.push(middlewareFn);
}

/**
* {{ channel.subscribe().summary() }}
*
* @param {object} options
* @param {object} options.message
{%- if channel.subscribe().message(0).headers() %}
{%- for fieldName, field in channel.subscribe().message(0).headers().properties() %}
{{ field | docline(fieldName, 'options.message.headers') }}
{%- endfor %}
{%- endif %}
{%- if channel.subscribe().message(0).payload() %}
{%- for fieldName, field in channel.subscribe().message(0).payload().properties() %}
{{ field | docline(fieldName, 'options.message.payload') }}
{%- endfor %}
{%- endif %}
{%- if channel.subscribe().message(0).headers() %}
{%- for fieldName, field in channel.subscribe().message(0).headers().properties() %}
{{ field | docline(fieldName, 'options.message.headers') }}
{%- endfor %}
{%- endif %}
{%- if channel.subscribe().message(0).payload() %}
{%- for fieldName, field in channel.subscribe().message(0).payload().properties() %}
{{ field | docline(fieldName, 'options.message.payload') }}
{%- endfor %}
{%- endif %}
*/
handler.{{ channel.subscribe().id() }} = async ({message}) => {
// Implement your business logic here...
handler._{{ channel.subscribe().id() }} = async ({message}) => {
for (const middleware of {{ channel.subscribe().id() }}Middlewares) {
await middleware(message);
}
};

{%- endif %}
41 changes: 32 additions & 9 deletions template/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,35 @@ app.useOutbound(errorLogger);
app.useOutbound(logger);
app.useOutbound(json2string);

app
.listen()
.then((adapters) => {
console.log(cyan.underline(`${config.app.name} ${config.app.version}`), gray('is ready!'), '\n');
adapters.forEach(adapter => {
console.log('🔗 ', adapter.name(), gray('is connected!'));
});
})
.catch(console.error);
function init() {
app
.listen()
.then((adapters) => {
console.log(cyan.underline(`${config.app.name} ${config.app.version}`), gray('is ready!'), '\n');
adapters.forEach(adapter => {
console.log('🔗 ', adapter.name(), gray('is connected!'));
});
})
.catch(console.error);
}

const handlers = {
{%- for channelName, channel in asyncapi.channels() -%}
{% if channel.hasPublish() %}
{{ channel.publish().id() | convertOpertionIdToMiddlewareFn }}: require('./handlers/{{ channelName | convertToFilename }}').{{ channel.publish().id() | convertOpertionIdToMiddlewareFn }},
{%- endif -%}
{% if channel.hasSubscribe() %}
{{ channel.subscribe().id() | convertOpertionIdToMiddlewareFn }}: require('./handlers/{{ channelName | convertToFilename }}').{{ channel.subscribe().id() | convertOpertionIdToMiddlewareFn }},
{% endif %}
{%- endfor -%}
};

const client = {
app,
init,
...handlers
};

module.exports = {
client
};
8 changes: 4 additions & 4 deletions template/src/api/routes/$$channel$$.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ router.use('{{ channelName | toHermesTopic }}', async (message, next) => {
} catch { };
{% endfor -%}
if (nValidated === 1) {
await {{ channelName | camelCase }}Handler.{{ channel.publish().id() }}({message});
await {{ channelName | camelCase }}Handler._{{ channel.publish().id() }}({message});
next()
} else {
throw new Error(`${nValidated} of {{ channel.publish().messages().length }} message schemas matched when exactly 1 should match`);
}
{% else %}
await validateMessage(message.payload,'{{ channelName }}','{{ channel.publish().message().name() }}','publish');
await {{ channelName | camelCase }}Handler.{{ channel.publish().id() }}({message});
await {{ channelName | camelCase }}Handler._{{ channel.publish().id() }}({message});
next();
{% endif %}
} catch (e) {
Expand All @@ -61,14 +61,14 @@ router.useOutbound('{{ channelName | toHermesTopic }}', async (message, next) =>
nValidated = await validateMessage(message.payload,'{{ channelName }}','{{ channel.subscribe().message(i).name() }}','subscribe', nValidated);
{% endfor -%}
if (nValidated === 1) {
await {{ channelName | camelCase }}Handler.{{ channel.subscribe().id() }}({message});
await {{ channelName | camelCase }}Handler._{{ channel.subscribe().id() }}({message});
next()
} else {
throw new Error(`${nValidated} of {{ channel.subscribe().messages().length }} message schemas matched when exactly 1 should match`);
}
{% else %}
await validateMessage(message.payload,'{{ channelName }}','{{ channel.subscribe().message().name() }}','subscribe');
await {{ channelName | camelCase }}Handler.{{ channel.subscribe().id() }}({message});
await {{ channelName | camelCase }}Handler._{{ channel.subscribe().id() }}({message});
next();
{% endif %}
} catch (e) {
Expand Down
4 changes: 3 additions & 1 deletion template/src/lib/message-validator.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
const path = require('path');
const AsyncApiValidator = require('asyncapi-validator');

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

Expand Down
Loading

0 comments on commit c120307

Please sign in to comment.