Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: enable usage of generated app as a library without its code modification #220

Merged
merged 17 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.<operationId>` 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.turnOn((message) => { // `turnOn` is the respective operationId
kaushik-rishi marked this conversation as resolved.
Show resolved Hide resolved
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.receiveLightMeasurement((message) => { // `recieveLightMeasurement` is the respective operationId
console.log("recieved in middleware 1", message.payload);
});

client.receiveLightMeasurement((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
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() }} = (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 %}
kaushik-rishi marked this conversation as resolved.
Show resolved Hide resolved
*/
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() }} = (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() }}: require('./handlers/{{ channelName | convertToFilename }}').{{ channel.publish().id() }},
{%- endif -%}
{% if channel.hasSubscribe() %}
{{ channel.subscribe().id() }}: require('./handlers/{{ channelName | convertToFilename }}').{{ channel.subscribe().id() }},
{% 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