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: upgrade @asyncapi/parser to v2.0.0 and use the new parser-api #960

Merged
merged 25 commits into from
May 18, 2023
Merged
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
9 changes: 6 additions & 3 deletions cli.js
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ const path = require('path');
const os = require('os');
const program = require('commander');
const xfs = require('fs.extra');
const { DiagnosticSeverity } = require('@asyncapi/parser/cjs');
const packageInfo = require('./package.json');
const Generator = require('./lib/generator');
const Watcher = require('./lib/watcher');
@@ -64,8 +65,10 @@ const mapBaseUrlParser = v => {
const showError = err => {
console.error(red('Something went wrong:'));
console.error(red(err.stack || err.message));
if (err.errors) console.error(red(JSON.stringify(err.errors)));
if (err.validationErrors) console.error(red(JSON.stringify(err.validationErrors, null, 4)));
if (err.diagnostics) {
const errorDiagnostics = err.diagnostics.filter(diagnostic => diagnostic.severity === DiagnosticSeverity.Error);
console.error(red(`Errors:\n${JSON.stringify(errorDiagnostics, undefined, 2)}`));
}
};
const showErrorAndExit = err => {
showError(err);
@@ -127,7 +130,7 @@ xfs.mkdirp(program.output, async err => {
console.warn(`WARNING: ${template} is a remote template. Changes may be lost on subsequent installations.`);
}

watcher.watch(watcherHandler, (paths) => {
await watcher.watch(watcherHandler, (paths) => {
showErrorAndExit({ message: `[WATCHER] Could not find the file path ${paths}, are you sure it still exists? If it has been deleted or moved please rerun the generator.` });
});
}
2 changes: 2 additions & 0 deletions docs/configuration-file.md
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ The `generator` property from `package.json` file must contain a JSON object tha
|Name|Type|Description|
|---|---|---|
|`renderer`| String | Its value can be either `react` or `nunjucks` (default).
|`apiVersion`| String | Determines which **major** version of the [Parser-API](https://github.com/asyncapi/parser-api) the template uses. For example, `v1` for `v1.x.x`. If not specified, the Generator assumes the template is not compatible with the [Parser-API](https://github.com/asyncapi/parser-api) so it will use the [Parser-JS v1 API](https://github.com/asyncapi/parser-js/tree/v1.18.1#api-documentation). If the template uses a version of the [Parser-API](https://github.com/asyncapi/parser-api) that is not supported by the Generator, the Generator will throw an error.
derberg marked this conversation as resolved.
Show resolved Hide resolved
smoya marked this conversation as resolved.
Show resolved Hide resolved
|`supportedProtocols`| [String] | A list with all the protocols this template supports.
|`parameters`| Object[String, Object] | An object with all the parameters that can be passed when generating the template. When using the command line, it's done by indicating `--param name=value` or `-p name=value`.
|`parameters[param].description`| String | A user-friendly description about the parameter.
@@ -27,6 +28,7 @@ The `generator` property from `package.json` file must contain a JSON object tha
"generator":
{
"renderer": "react",
"apiVersion": "v1",
"supportedProtocols": ["amqp", "mqtt"],
"parameters": {
"server": {
4 changes: 4 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
module.exports = {
clearMocks: true,
moduleNameMapper: {
'^nimma/legacy$': '<rootDir>/node_modules/nimma/dist/legacy/cjs/index.js',
'^nimma/(.*)': '<rootDir>/node_modules/nimma/dist/cjs/$1',
},
};
123 changes: 69 additions & 54 deletions lib/generator.js
Original file line number Diff line number Diff line change
@@ -2,18 +2,15 @@ const path = require('path');
const fs = require('fs');
const xfs = require('fs.extra');
const minimatch = require('minimatch');
const parser = require('@asyncapi/parser');
const { configureReact, renderReact, saveRenderedReactContent } = require('./renderer/react');
const { configureNunjucks, renderNunjucks } = require('./renderer/nunjucks');
const { parse, AsyncAPIDocument } = parser;
const ramlDtParser = require('@asyncapi/raml-dt-schema-parser');
const openapiSchemaParser = require('@asyncapi/openapi-schema-parser');
const avroSchemaParser = require('@asyncapi/avro-schema-parser');
const jmespath = require('jmespath');
const filenamify = require('filenamify');
const git = require('simple-git');
const log = require('loglevel');
const Arborist = require('@npmcli/arborist');
const { isAsyncAPIDocument } = require('@asyncapi/parser/cjs/document');

const { configureReact, renderReact, saveRenderedReactContent } = require('./renderer/react');
const { configureNunjucks, renderNunjucks } = require('./renderer/nunjucks');
const { validateTemplateConfig } = require('./templateConfigValidator');
const {
convertMapToObject,
@@ -30,8 +27,9 @@ const {
registerSourceMap,
registerTypeScript,
getTemplateDetails,
getMapBaseUrlToFolderResolver
convertCollectionToObject,
} = require('./utils');
const { parse, usesNewAPI, getProperApiDocument } = require('./parser');
const { registerFilters } = require('./filtersRegistry');
const { registerHooks } = require('./hooksRegistry');

@@ -57,9 +55,6 @@ const shouldIgnoreDir = dirPath =>
dirPath === '.git'
|| dirPath.startsWith(`.git${path.sep}`);

parser.registerSchemaParser(openapiSchemaParser);
parser.registerSchemaParser(ramlDtParser);
parser.registerSchemaParser(avroSchemaParser);
registerSourceMap();
registerTypeScript();

@@ -164,10 +159,9 @@ class Generator {
* @return {Promise}
*/
async generate(asyncapiDocument) {
if (!(asyncapiDocument instanceof AsyncAPIDocument)) throw new Error('Parameter "asyncapiDocument" must be an AsyncAPIDocument object.');
if (!isAsyncAPIDocument(asyncapiDocument)) throw new Error('Parameter "asyncapiDocument" must be an AsyncAPIDocument object.');

this.asyncapi = asyncapiDocument;

if (this.output === 'fs') {
xfs.mkdirpSync(this.targetDir);
if (!this.forceWrite) await this.verifyTargetDir(this.targetDir);
@@ -185,6 +179,9 @@ class Generator {
validateTemplateConfig(this.templateConfig, this.templateParams, asyncapiDocument);
await this.configureTemplate();

// use new or old document API based on `templateConfig.apiVersion` value
this.asyncapi = asyncapiDocument = getProperApiDocument(asyncapiDocument, this.templateConfig);

if (!isReactTemplate(this.templateConfig)) {
await registerFilters(this.nunjucks, this.templateConfig, this.templateDir, FILTERS_DIRNAME);
}
@@ -252,18 +249,23 @@ class Generator {
* }
*
* @param {String} asyncapiString AsyncAPI string to use as source.
* @param {Object} [parserOptions={}] AsyncAPI parser options. Check out {@link https://www.github.com/asyncapi/parser-js|@asyncapi/parser} for more information.
* @param {Object} [parseOptions={}] AsyncAPI Parser parse options. Check out {@link https://www.github.com/asyncapi/parser-js|@asyncapi/parser} for more information.
* @return {Promise}
*/
async generateFromString(asyncapiString, parserOptions = {}) {
async generateFromString(asyncapiString, parseOptions = {}) {
if (!asyncapiString || typeof asyncapiString !== 'string') throw new Error('Parameter "asyncapiString" must be a non-empty string.');

/** @type {String} AsyncAPI string to use as a source. */
this.originalAsyncAPI = asyncapiString;

/** @type {AsyncAPIDocument} Parsed AsyncAPI schema. See {@link https://github.com/asyncapi/parser-js/blob/master/API.md#module_@asyncapi/parser+AsyncAPIDocument|AsyncAPIDocument} for details on object structure. */
const asyncapi = await parse(asyncapiString, parserOptions);
return this.generate(asyncapi);
const { document, diagnostics } = await parse(asyncapiString, parseOptions, this);
if (!document) {
const err = new Error('Input is not a corrent AsyncAPI document so it cannot be processed.');
err.diagnostics = diagnostics;
throw err;
}
return this.generate(document);
}

/**
@@ -290,12 +292,7 @@ class Generator {
*/
async generateFromURL(asyncapiURL) {
const doc = await fetchSpec(asyncapiURL);
const parserOptions = {};
if (this.mapBaseUrlToFolder.url) {
parserOptions.resolve = {resolver: getMapBaseUrlToFolderResolver(this.mapBaseUrlToFolder)};
}

return this.generateFromString(doc, parserOptions);
return this.generateFromString(doc, { path: asyncapiURL });
}

/**
@@ -322,12 +319,7 @@ class Generator {
*/
async generateFromFile(asyncapiFile) {
const doc = await readFile(asyncapiFile, { encoding: 'utf8' });
const parserOptions = { path: asyncapiFile };
if (this.mapBaseUrlToFolder.url) {
parserOptions.resolve = {resolver: getMapBaseUrlToFolderResolver(this.mapBaseUrlToFolder)};
}

return this.generateFromString(doc, parserOptions);
return this.generateFromString(doc, { path: asyncapiFile });
}

/**
@@ -425,18 +417,30 @@ class Generator {
getAllParameters(asyncapiDocument) {
const parameters = new Map();

if (asyncapiDocument.hasChannels()) {
asyncapiDocument.channelNames().forEach(channelName => {
const channel = asyncapiDocument.channel(channelName);
for (const [key, value] of Object.entries(channel.parameters())) {
parameters.set(key, value);
}
if (usesNewAPI(this.templateConfig)) {
asyncapiDocument.channels().all().forEach(channel => {
channel.parameters().all().forEach(parameter => {
parameters.set(parameter.id(), parameter);
});
});
}

if (asyncapiDocument.hasComponents()) {
for (const [key, value] of Object.entries(asyncapiDocument.components().parameters())) {
parameters.set(key, value);
asyncapiDocument.components().channelParameters().all().forEach(parameter => {
parameters.set(parameter.id(), parameter);
});
} else {
if (asyncapiDocument.hasChannels()) {
asyncapiDocument.channelNames().forEach(channelName => {
const channel = asyncapiDocument.channel(channelName);
for (const [key, value] of Object.entries(channel.parameters())) {
parameters.set(key, value);
}
});
}

if (asyncapiDocument.hasComponents()) {
for (const [key, value] of Object.entries(asyncapiDocument.components().parameters())) {
parameters.set(key, value);
}
}
}

@@ -451,9 +455,6 @@ class Generator {
* @return {Promise}
*/
generateDirectoryStructure(asyncapiDocument) {
const objectMap = {};
asyncapiDocument.allSchemas().forEach((schema, schemaId) => { if (schema.type() === 'object') objectMap[schemaId] = schema; });

return new Promise((resolve, reject) => {
xfs.mkdirpSync(this.targetDir);

@@ -463,7 +464,7 @@ class Generator {

walker.on('file', async (root, stats, next) => {
try {
await this.filesGenerationHandler(asyncapiDocument, objectMap, root, stats, next);
await this.filesGenerationHandler(asyncapiDocument, root, stats, next);
} catch (e) {
reject(e);
}
@@ -514,16 +515,31 @@ class Generator {
* @param {String} stats Information about the file.
* @param {Function} next Callback function
*/
async filesGenerationHandler(asyncapiDocument, objectMap, root, stats, next) {
const fileNamesForSeparation = {
channel: asyncapiDocument.channels(),
message: convertMapToObject(asyncapiDocument.allMessages()),
securityScheme: asyncapiDocument.components() ? asyncapiDocument.components().securitySchemes() : {},
schema: asyncapiDocument.components() ? asyncapiDocument.components().schemas() : {},
objectSchema: objectMap,
parameter: convertMapToObject(this.getAllParameters(asyncapiDocument)),
everySchema: convertMapToObject(asyncapiDocument.allSchemas()),
};
async filesGenerationHandler(asyncapiDocument, root, stats, next) {
let fileNamesForSeparation = {};
if (usesNewAPI(this.templateConfig)) {
fileNamesForSeparation = {
channel: convertCollectionToObject(asyncapiDocument.channels().all(), 'address'),
message: convertCollectionToObject(asyncapiDocument.messages().all(), 'id'),
securityScheme: convertCollectionToObject(asyncapiDocument.components().securitySchemes().all(), 'id'),
schema: convertCollectionToObject(asyncapiDocument.components().schemas().all(), 'id'),
objectSchema: convertCollectionToObject(asyncapiDocument.schemas().all().filter(schema => schema.type() === 'object'), 'id'),
parameter: convertMapToObject(this.getAllParameters(asyncapiDocument)),
everySchema: convertCollectionToObject(asyncapiDocument.schemas().all(), 'id'),
};
} else {
const objectSchema = {};
asyncapiDocument.allSchemas().forEach((schema, schemaId) => { if (schema.type() === 'object') objectSchema[schemaId] = schema; });
fileNamesForSeparation = {
channel: asyncapiDocument.channels(),
message: convertMapToObject(asyncapiDocument.allMessages()),
securityScheme: asyncapiDocument.components() ? asyncapiDocument.components().securitySchemes() : {},
schema: asyncapiDocument.components() ? asyncapiDocument.components().schemas() : {},
objectSchema,
parameter: convertMapToObject(this.getAllParameters(asyncapiDocument)),
everySchema: convertMapToObject(asyncapiDocument.allSchemas()),
};
}

// Check if the filename dictates it should be separated
let wasSeparated = false;
@@ -663,7 +679,6 @@ class Generator {
}

if (this.isNonRenderableFile(relativeSourceFile)) return await copyFile(sourceFile, targetFile);

await this.renderAndWriteToFile(asyncapiDocument, sourceFile, targetFile);
}

Loading