Skip to content

Commit

Permalink
feat: move template config to package.json (#349)
Browse files Browse the repository at this point in the history
  • Loading branch information
derberg authored May 22, 2020
1 parent ccede28 commit 86a0d35
Show file tree
Hide file tree
Showing 9 changed files with 299 additions and 122 deletions.
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);
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

0 comments on commit 86a0d35

Please sign in to comment.