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
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.` });
});
}
Expand Down
2 changes: 2 additions & 0 deletions docs/configuration-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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 that is not supported by the Generator, the Generator will throw an error.
|`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.
Expand All @@ -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": {
Expand Down
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
Expand Up @@ -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,
Expand All @@ -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');

Expand All @@ -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();

Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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 });
}

/**
Expand All @@ -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 });
}

/**
Expand Down Expand Up @@ -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);
}
}
}

Expand All @@ -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);

Expand All @@ -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);
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -663,7 +679,6 @@ class Generator {
}

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

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

Expand Down
Loading