Skip to content

Commit

Permalink
feat: support filters from external library (#298)
Browse files Browse the repository at this point in the history
Co-Authored-By: Fran Méndez <[email protected]>
  • Loading branch information
derberg and fmvilas authored Apr 29, 2020
1 parent 8f115b4 commit 4d0a951
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 74 deletions.
41 changes: 10 additions & 31 deletions docs/authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The AsyncAPI generator has been built with extensibility in mind. The package us
1. The template can have own dependencies. Just create `package.json` for the template. The generator makes sure to trigger the installation of dependencies.
1. Templates may contain multiple files. Unless stated otherwise, all files will be rendered.
1. The template engine is [Nunjucks](https://mozilla.github.io/nunjucks).
1. Templates may contain `filters` or helper functions. They must be stored in the `.filters` directory under the template directory. [Read more about filters](#filters).
1. Templates may contain [Nunjucks filters or helper functions](https://mozilla.github.io/nunjucks/templating.html#builtin-filters). [Read more about filters](#filters).
1. Templates may contain `hooks` that are functions invoked after the generation. They must be stored in the `.hooks` directory under the template directory. [Read more about hooks](#hooks).
1. Templates may contain `partials` (reusable chunks). They must be stored in the `.partials` directory under the template directory. [Read more about partials](#partials).
1. Templates may have a configuration file. It must be stored in the template directory and its name must be `.tp-config.json`. [Read more about the configuration file](#configuration-file).
Expand Down Expand Up @@ -64,41 +64,16 @@ Schema name is 'people' and properties are:
```
## Filters

A filter is a helper function that you can create to perform complex tasks. They are JavaScript files that register one or many [Nunjuck filters](https://mozilla.github.io/nunjucks/api.html#custom-filters). The generator will parse all the files in the `.filters` directory.
A filter is a helper function that you can create to perform complex tasks. They are JavaScript files that register one or many [Nunjuck filters](https://mozilla.github.io/nunjucks/api.html#custom-filters). The generator parses all the files in the `filters` directory. Functions exported from these files are registered as filters.

Each file must export a function that will receive the following parameters:

* `Nunjucks`: a reference to the [Nunjucks](https://mozilla.github.io/nunjucks) template engine used internally by the generator.
* `_`: a convenient [Lodash](https://www.lodash.com) reference.
* `Markdown`: a reference to the [markdown-it](https://github.com/markdown-it/markdown-it) package. Use it to convert Markdown to HTML.
* `OpenAPISampler`: a reference to the [openapi-sampler](https://github.com/Redocly/openapi-sampler) package. It generates examples from OpenAPI/AsyncAPI schemas.

The common structure for one of these files is the following:

```js
module.exports = ({ Nunjucks, _, Markdown, OpenAPISampler }) => {
Nunjucks.addFilter('yourFilterName', (yourFilterParams) => {
return 'doSomething';
});
};
```

An example filter called `camelCase` which uses the Lodash function `_.camelCase(String)` to convert a template string as camel case:

```js
module.exports = ({ Nunjucks, _, Markdown, OpenAPISampler }) => {
Nunjucks.addFilter('camelCase', (str) => {
return _.camelCase(str);
});
};
```

And then you can use the filter in your template as follows:
You can use the filter function in your template as in the following example:

```js
const {{ channelName | camelCase }} = '{{ channelName }}';
```

In case you have more than one template and want to reuse filters, you can put them in a single library. You can configure such a library in the template configuration under `filters` property. You can also use the official AsyncAPI [filters library](https://github.com/asyncapi/generator-filters). To learn how to add such filters to configuration [read more about the configuration file](#configuration-file).

## Hooks

Hooks are functions called by the generator on a specific moment in the generation process. For now there is one hook supported called `generate:after` that is called at the very end of the generation. The generator will parse all the files in the `.hooks` directory.
Expand Down Expand Up @@ -160,6 +135,7 @@ The `.tp-config.json` file contains a JSON object that may have the following in
|`conditionalFiles[filePath].validation`| Object | The `validation` is a JSON Schema Draft 07 object. This JSON Schema definition will be applied to the JSON value resulting from the `subject` query. If validation doesn't have errors, the condition is met, and therefore the given file will be rendered. Otherwise, the file is ignored.
|`nonRenderableFiles`| [String] | A list of file paths or [globs](https://en.wikipedia.org/wiki/Glob_(programming)) that must be copied "as-is" to the target directory, i.e., without performing any rendering process. This is useful when you want to copy binary files.
|`generator`| [String] | A string representing the Generator version-range the template is compatible with. This value must follow the [semver](https://docs.npmjs.com/misc/semver) syntax. E.g., `>=1.0.0`, `>=1.0.0 <=2.0.0`, `~1.0.0`, `^1.0.0`, `1.0.0`, etc.
|`filters`| [String] | A list of modules containing functions that can be uses as Nunjucks filters. In case of external modules, remember they need to be added as a dependency in `package.json` of your template.

### Example

Expand Down Expand Up @@ -190,7 +166,10 @@ The `.tp-config.json` file contains a JSON object that may have the following in
"src/api/middlewares/*.*",
"lib/lib/config.js"
],
"generator": "<2.0.0"
"generator": "<2.0.0",
"filters": [
"@asyncapi/generator-filters"
]
}
```

Expand Down
90 changes: 90 additions & 0 deletions lib/filtersRegistry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
const path = require('path');
const fs = require('fs');
const xfs = require('fs.extra');
const { isLocalTemplate } = require('./utils');

/**
* Registers all template filters.
* @param {Object} nunjucks Nunjucks environment.
* @param {Object} templateConfig Template configuration.
* @param {String} templateDir Directory where template is located.
* @param {String} filtersDir Directory where local filters are located.
*/
module.exports.registerFilters = async (nunjucks, templateConfig, templateDir, filtersDir) => {
await registerLocalFilters(nunjucks, templateDir, filtersDir);
await registerConfigFilters(nunjucks, templateDir, templateConfig);
};

/**
* Registers the local template filters.
* @private
* @param {Object} nunjucks Nunjucks environment.
* @param {String} templateDir Directory where template is located.
* @param {String} filtersDir Directory where local filters are located.
*/
function registerLocalFilters(nunjucks, templateDir, filtersDir) {
return new Promise((resolve, reject) => {
const localFilters = path.resolve(templateDir, filtersDir);

if (!fs.existsSync(localFilters)) return resolve();

const walker = xfs.walk(localFilters, {
followLinks: false
});

walker.on('file', async (root, stats, next) => {
try {
const filePath = path.resolve(templateDir, path.resolve(root, stats.name));
// If it's a module constructor, inject dependencies to ensure consistent usage in remote templates in other projects or plain directories.
const mod = require(filePath);
addFilters(nunjucks, mod);
next();
} catch (e) {
reject(e);
}
});

walker.on('errors', (root, nodeStatsArray) => {
reject(nodeStatsArray);
});

walker.on('end', async () => {
resolve();
});
});
}

/**
* Registers the additionally configured filters.
* @private
* @param {Object} nunjucks Nunjucks environment.
* @param {String} templateDir Directory where template is located.
* @param {Object} templateConfig Template configuration.
*/
async function registerConfigFilters(nunjucks, templateDir, templateConfig) {
const confFilters = templateConfig.filters;
const DEFAULT_MODULES_DIR = 'node_modules';
if (!Array.isArray(confFilters)) return;

confFilters.forEach(async el => {
const modulePath = await isLocalTemplate(templateDir) ? path.resolve(templateDir, DEFAULT_MODULES_DIR, el) : el;
const mod = require(modulePath);

addFilters(nunjucks, mod);
});
}

/**
* Add filter functions to Nunjucks environment. Only owned functions from the module are added.
* @private
* @param {Object} nunjucks Nunjucks environment.
* @param {Object} filters Module with functions.
*/
function addFilters(nunjucks, filters) {
Object.getOwnPropertyNames(filters).forEach((key) => {
const value = filters[key];
if (!(value instanceof Function)) return;

nunjucks.addFilter(key, value);
});
}
44 changes: 3 additions & 41 deletions lib/generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ const {
readDir,
writeFile,
copyFile,
exists,
exists
} = require('./utils');
const { registerFilters } = require('./filtersRegistry');

const ajv = new Ajv({ allErrors: true });

Expand Down Expand Up @@ -145,7 +146,7 @@ class Generator {
this.configNunjucks();
await this.loadTemplateConfig();
this.registerHooks();
await this.registerFilters();
await registerFilters(this.nunjucks, this.templateConfig, this.templateDir, FILTERS_DIRNAME);

if (this.entrypoint) {
const entrypointPath = path.resolve(this.templateContentDir, this.entrypoint);
Expand Down Expand Up @@ -329,43 +330,6 @@ class Generator {
});
}

/**
* Registers the template filters.
* @private
*/
registerFilters() {
return new Promise((resolve, reject) => {
this.helpersDir = path.resolve(this.templateDir, FILTERS_DIRNAME);
if (!fs.existsSync(this.helpersDir)) return resolve();

const walker = xfs.walk(this.helpersDir, {
followLinks: false
});

walker.on('file', async (root, stats, next) => {
try {
const filePath = path.resolve(this.templateDir, path.resolve(root, stats.name));
// If it's a module constructor, inject dependencies to ensure consistent usage in remote templates in other projects or plain directories.
const mod = require(filePath);
if (typeof mod === 'function') {
mod({ Nunjucks: this.nunjucks });
}
next();
} catch (e) {
reject(e);
}
});

walker.on('errors', (root, nodeStatsArray) => {
reject(nodeStatsArray);
});

walker.on('end', async () => {
resolve();
});
});
}

/**
* Returns all the parameters on the AsyncAPI document.
*
Expand Down Expand Up @@ -647,7 +611,6 @@ class Generator {
*/
configNunjucks() {
this.nunjucks = new Nunjucks.Environment(new Nunjucks.FileSystemLoader(this.templateDir));
this.nunjucks.addFilter('log', console.log);
}

/**
Expand All @@ -667,7 +630,6 @@ class Generator {
} catch (e) {
this.templateConfig = {};
}

await this.validateTemplateConfig();
}

Expand Down
2 changes: 1 addition & 1 deletion lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,4 @@ utils.getLocalTemplateDetails = async (templatePath) => {
link: linkTarget,
resolvedLink: path.resolve(path.dirname(templatePath), linkTarget),
};
};
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"release": "semantic-release",
"docker-build": "docker build -t asyncapi/generator:latest .",
"get-version": "echo $npm_package_version",
"eslint": "eslint --config .eslintrc ."
"lint": "eslint --config .eslintrc ."
},
"preferGlobal": true,
"bugs": {
Expand Down

0 comments on commit 4d0a951

Please sign in to comment.