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: move template config to package.json #349

Merged
merged 5 commits into from
May 22, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ const showErrorAndExit = err => {
program
.version(packageInfo.version)
.arguments('<asyncapi> <template>')
.action((path, tmpl) => {
asyncapiDocPath = path;
.action((fileLoc, tmpl) => {
asyncapiDocPath = fileLoc;
template = tmpl;
})
.option('-d, --disable-hook <hookType>', 'disable a specific hook type', disableHooksParser)
Expand Down
5 changes: 3 additions & 2 deletions docs/authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The AsyncAPI generator has been built with extensibility in mind. The package us
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 during specific moment of the generation. In the template, they must be stored in the `.hooks` directory under the template directory. They can also be stored in other modules and external libraries and configured inside the template [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).
1. Templates can be configured. Configuration must be stored in the `package.json` file under custom `generator` property. [Read more about the configuration file](#configuration-file).
1. There are parameters with special meaning. [Read more about special parameters](#special-parameters).
1. The default variables you have access to in any the template file are the following:
- `asyncapi` that is a parsed spec file object. Read the [API](https://github.com/asyncapi/parser-js/blob/master/API.md#AsyncAPIDocument) of the Parser to understand to what structure you have access in this parameter.
Expand Down Expand Up @@ -176,7 +176,7 @@ Files from the `.partials` directory do not end up with other generated files in

## Configuration File

The `.tp-config.json` file contains a JSON object that may have the following information:
The `generator` property from `package.json` file must contain a JSON object that may have the following information:

|Name|Type|Description|
|---|---|---|
Expand All @@ -196,6 +196,7 @@ The `.tp-config.json` file contains a JSON object that may have the following in
### Example

```json
"generator":
{
"supportedProtocols": ["amqp", "mqtt"],
"parameters": {
Expand Down
3 changes: 3 additions & 0 deletions lib/__mocks__/templateConfigValidator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const templateConfigValidator = jest.genMockFromModule('../templateConfigValidator');

module.exports = templateConfigValidator;
3 changes: 3 additions & 0 deletions lib/__mocks__/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@ utils.getLocalTemplateDetails = jest.fn(async () => ({
resolvedLink: utils.__getLocalTemplateDetailsResolvedLinkValue || '',
}));

utils.__generatorVersion = '';
utils.getGeneratorVersion = jest.fn(() => utils.__generatorVersion);

module.exports = utils;
75 changes: 7 additions & 68 deletions lib/generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ const ramlDtParser = require('@asyncapi/raml-dt-schema-parser');
const openapiSchemaParser = require('@asyncapi/openapi-schema-parser');
const Nunjucks = require('nunjucks');
const jmespath = require('jmespath');
const Ajv = require('ajv');
const filenamify = require('filenamify');
const git = require('simple-git/promise');
const npmi = require('npmi');
const semver = require('semver');
const packageJson = require('../package.json');
const { validateTemplateConfig } = require('./templateConfigValidator');
const {
convertMapToObject,
isFileSystemPath,
Expand All @@ -30,8 +28,6 @@ const {
const { registerFilters } = require('./filtersRegistry');
const { registerHooks } = require('./hooksRegistry');

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

parser.registerSchemaParser([
'application/vnd.oai.openapi;version=3.0.0',
'application/vnd.oai.openapi+json;version=3.0.0',
Expand All @@ -44,7 +40,7 @@ parser.registerSchemaParser([

const FILTERS_DIRNAME = 'filters';
const HOOKS_DIRNAME = 'hooks';
const CONFIG_FILENAME = '.tp-config.json';
const CONFIG_FILENAME = 'package.json';
const PACKAGE_JSON_FILENAME = 'package.json';
const ROOT_DIR = path.resolve(__dirname, '..');
const DEFAULT_TEMPLATES_DIR = path.resolve(ROOT_DIR, 'node_modules');
Expand Down Expand Up @@ -122,7 +118,7 @@ class Generator {
enumerable: true,
get() {
if (!self.templateConfig.parameters || !self.templateConfig.parameters[key]) {
throw new Error(`Template parameter "${key}" has not been defined in the .tp-config.json file. Please make sure it's listed there before you use it in your template.`);
throw new Error(`Template parameter "${key}" has not been defined in the package.json file under generator property. Please make sure it's listed there before you use it in your template.`);
}
return templateParams[key];
}
Expand Down Expand Up @@ -168,9 +164,9 @@ class Generator {
this.templateContentDir = path.resolve(this.templateDir, TEMPLATE_CONTENT_DIRNAME);
this.configNunjucks();
await this.loadTemplateConfig();
validateTemplateConfig(this.templateConfig, this.templateParams, asyncapiDocument);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the order of calling validation and removed it from here https://github.com/asyncapi/generator/pull/349/files#diff-b58e2f1cee2acff55355343774e6d576L646 as I could not come up with an explanation why we were calling validation twice.

also, it is no longer async as I think it should never be

await registerHooks(this.hooks, this.templateConfig, this.templateDir, HOOKS_DIRNAME);
await registerFilters(this.nunjucks, this.templateConfig, this.templateDir, FILTERS_DIRNAME);
await this.validateTemplateConfig(asyncapiDocument);
await this.launchHook('generate:before');

if (this.entrypoint) {
Expand Down Expand Up @@ -667,8 +663,9 @@ class Generator {
return;
}

const json = fs.readFileSync(configPath, { encoding: 'utf8' });
this.templateConfig = JSON.parse(json);
const json = await readFile(configPath, { encoding: 'utf8' });
const generatorProp = JSON.parse(json).generator;
this.templateConfig = generatorProp || {};
} catch (e) {
this.templateConfig = {};
}
Expand All @@ -694,64 +691,6 @@ class Generator {
);
}

/**
* Validates the template configuration.
*
* @private
* @param {AsyncAPIDocument} [asyncapiDocument] AsyncAPIDocument object to use as source.
* @return {Promise}
*/
async validateTemplateConfig(asyncapiDocument) {
const { parameters, supportedProtocols, conditionalFiles, generator } = this.templateConfig;
let server;

if (typeof generator === 'string' && !semver.satisfies(packageJson.version, generator)) {
throw new Error(`This template is not compatible with the current version of the generator (${packageJson.version}). This template is compatible with the following version range: ${generator}.`);
}

this.verifyParameters(parameters);

if (typeof conditionalFiles === 'object') {
const fileNames = Object.keys(conditionalFiles) || [];
fileNames.forEach(fileName => {
const def = conditionalFiles[fileName];
if (typeof def.subject !== 'string') throw new Error(`Invalid conditional file subject for ${fileName}: ${def.subject}.`);
if (typeof def.validation !== 'object') throw new Error(`Invalid conditional file validation object for ${fileName}: ${def.validation}.`);
conditionalFiles[fileName].validate = ajv.compile(conditionalFiles[fileName].validation);
});
}

if (asyncapiDocument) {
if (typeof this.templateParams.server === 'string') {
server = asyncapiDocument.server(this.templateParams.server);
if (!server) throw new Error(`Couldn't find server with name ${this.templateParams.server}.`);
}

if (server && Array.isArray(supportedProtocols) && !supportedProtocols.includes(server.protocol())) {
throw new Error(`Server "${this.templateParams.server}" uses the ${server.protocol()} protocol but this template only supports the following ones: ${supportedProtocols}.`);
}
}
}

/**
* Checks that all required parameters are set and all specified by user parameters are present in template.
*
* @private
* @param {Array} [parameters] parameters known from template configuration.
*/
verifyParameters(parameters) {
const missingParams = Object.keys(parameters || {})
.filter(key => parameters[key].required === true && this.templateParams[key] === undefined);
if (missingParams.length) {
throw new Error(`This template requires the following missing params: ${missingParams}.`);
}

const wrongParams = Object.keys(this.templateParams || {}).filter(key => parameters[key] === undefined);
if (wrongParams.length) {
console.warn(`Warning: This template doesn't have the following params: ${wrongParams}.`);
}
}

/**
* Launches all the hooks registered at a given hook point/name.
*
Expand Down
112 changes: 112 additions & 0 deletions lib/templateConfigValidator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
const semver = require('semver');
const Ajv = require('ajv');
const { getGeneratorVersion } = require('./utils');

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

/**
* Validates the template configuration.
*
* @param {Object} templateConfig Template configuration.
* @param {Object} templateParams Params specified when running generator.
* @param {AsyncAPIDocument} asyncapiDocument AsyncAPIDocument object to use as source.
* @return {Boolean}
*/
module.exports.validateTemplateConfig = (templateConfig, templateParams, asyncapiDocument) => {
const { parameters, supportedProtocols, conditionalFiles, generator } = templateConfig;

validateConditionalFiles(conditionalFiles);
isTemplateCompatible(generator);

isRequiredParamProvided(parameters, templateParams);
isProvidedParameterSupported(parameters, templateParams);

if (asyncapiDocument) {
const server = asyncapiDocument.server(templateParams.server);
isServerProvidedInDocument(server, templateParams.server);
isServerProtocolSupported(server, supportedProtocols, templateParams.server);
}

return true;
};

/**
* Checks if template is compatible with the version of the generator that is used
* @private
* @param {String} generator Information about supported generator version that is part of the template configuration
*/
function isTemplateCompatible(generator) {
const generatorVersion = getGeneratorVersion();
if (typeof generator === 'string' && !semver.satisfies(generatorVersion, generator)) {
throw new Error(`This template is not compatible with the current version of the generator (${generatorVersion}). This template is compatible with the following version range: ${generator}.`);
}
}

/**
* Checks if parameters described in template configuration as required are passed to the generator
* @private
* @param {Object} configParams Parameters specified in template configuration
* @param {Object} templateParams All parameters provided to generator
*/
function isRequiredParamProvided(configParams, templateParams) {
const missingParams = Object.keys(configParams || {})
.filter(key => configParams[key].required && !templateParams[key]);

if (missingParams.length) {
throw new Error(`This template requires the following missing params: ${missingParams}.`);
}
}

/**
* Checks if parameters provided to generator is supported by the template
* @private
* @param {Object} configParams Parameters specified in template configuration
* @param {Object} templateParams All parameters provided to generator
*/
function isProvidedParameterSupported(configParams, templateParams) {
const wrongParams = Object.keys(templateParams || {}).filter(key => !configParams || !configParams[key]);

if (wrongParams.length) {
console.warn(`Warning: This template doesn't have the following params: ${wrongParams}.`);
}
}

/**
* Checks if given AsyncAPI document has servers with protocol that is supported by the template
* @private
* @param {Object} server Server object from AsyncAPI file
* @param {String[]} supportedProtocols Supported protocols specified in template configuration
* @param {String} paramsServerName Name of the server specified as a param for the generator
*/
function isServerProtocolSupported(server, supportedProtocols, paramsServerName) {
if (server && Array.isArray(supportedProtocols) && !supportedProtocols.includes(server.protocol())) {
throw new Error(`Server "${paramsServerName}" uses the ${server.protocol()} protocol but this template only supports the following ones: ${supportedProtocols}.`);
}
}

/**
* Checks if given AsyncAPI document has servers with protocol that is supported by the template
* @private
* @param {Object} server Server object from AsyncAPI file
* @param {String} paramsServerName Name of the server specified as a param for the generator
*/
function isServerProvidedInDocument(server, paramsServerName) {
if (typeof paramsServerName === 'string' && !server) throw new Error(`Couldn't find server with name ${paramsServerName}.`);
}

/**
* Checks if conditional files are specified properly in the template
* @private
* @param {Object} conditionalFiles conditions specified in the template config
*/
function validateConditionalFiles(conditionalFiles) {
if (typeof conditionalFiles === 'object') {
const fileNames = Object.keys(conditionalFiles) || [];
fileNames.forEach(fileName => {
const def = conditionalFiles[fileName];
if (typeof def.subject !== 'string') throw new Error(`Invalid conditional file subject for ${fileName}: ${def.subject}.`);
if (typeof def.validation !== 'object') throw new Error(`Invalid conditional file validation object for ${fileName}: ${def.validation}.`);
conditionalFiles[fileName].validate = ajv.compile(conditionalFiles[fileName].validation);
});
}
}
20 changes: 16 additions & 4 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const util = require('util');
const path = require('path');
const fetch = require('node-fetch');
const url = require('url');
const packageJson = require('../package.json');

const utils = module.exports;

utils.lstat = util.promisify(fs.lstat);
Expand Down Expand Up @@ -87,12 +89,12 @@ utils.getLocalTemplateDetails = async (templatePath) => {
/**
* Fetches an AsyncAPI document from the given URL and return its content as string
*
* @param {String} url URL where the AsyncAPI document is located.
* @param {String} link URL where the AsyncAPI document is located.
* @returns Promise<String>} Content of fetched file.
*/
utils.fetchSpec = (url) => {
utils.fetchSpec = (link) => {
return new Promise((resolve, reject) => {
fetch(url)
fetch(link)
.then(res => resolve(res.text()))
.catch(reject);
});
Expand All @@ -106,4 +108,14 @@ utils.fetchSpec = (url) => {
*/
utils.isFilePath = (str) => {
return !url.parse(str).hostname;
};
};

/**
* Get version of the generator
*
* @returns {String}
*/
utils.getGeneratorVersion = () => {
return packageJson.version;
};

Loading