Befor generating

This commit is contained in:
marys
2026-06-01 13:17:37 +02:00
parent 3383f4bf4a
commit 1aa1b5f625
6756 changed files with 649946 additions and 1 deletions
+128
View File
@@ -0,0 +1,128 @@
const {xParserMessageName, xParserSchemaId} = require('./constants');
const {traverseAsyncApiDocument} = require('./iterators');
/**
* Assign message keys as message name to all the component messages.
*
* @private
* @param {AsyncAPIDocument} doc
*/
function assignNameToComponentMessages(doc) {
if (doc.hasComponents()) {
for (const [key, m] of Object.entries(doc.components().messages())) {
if (m.name() === undefined) {
m.json()[String(xParserMessageName)] = key;
}
}
}
}
/**
* Assign ids based on parameter keys.
*
* @private
* @param {Record<string,Schema>} parameterObject
*/
function assignIdToParameters(parameterObject) {
for (const [parameterKey, parameter] of Object.entries(parameterObject)) {
if (parameter.schema()) {
parameter.schema().json()[String(xParserSchemaId)] = parameterKey;
}
}
}
/**
* Assign parameter keys as uid for the parameter schema.
*
* @private
* @param {AsyncAPIDocument} doc
*/
function assignUidToParameterSchemas(doc) {
doc.channelNames().forEach(channelName => {
const channel = doc.channel(channelName);
assignIdToParameters(channel.parameters());
});
}
/**
* Assign uid to component schemas.
*
* @private
* @param {AsyncAPIDocument} doc
*/
function assignUidToComponentSchemas(doc) {
if (doc.hasComponents()) {
for (const [key, s] of Object.entries(doc.components().schemas())) {
s.json()[String(xParserSchemaId)] = key;
}
}
}
/**
* Assign uid to component parameters schemas
*
* @private
* @param {AsyncAPIDocument} doc
*/
function assignUidToComponentParameterSchemas(doc) {
if (doc.hasComponents()) {
assignIdToParameters(doc.components().parameters());
}
}
/**
* Assign anonymous names to nameless messages.
*
* @private
* @param {AsyncAPIDocument} doc
*/
function assignNameToAnonymousMessages(doc) {
let anonymousMessageCounter = 0;
if (doc.hasChannels()) {
doc.channelNames().forEach(channelName => {
const channel = doc.channel(channelName);
if (channel.hasPublish()) addNameToKey(channel.publish().messages(), ++anonymousMessageCounter);
if (channel.hasSubscribe()) addNameToKey(channel.subscribe().messages(), ++anonymousMessageCounter);
});
}
}
/**
* Add anonymous name to key if no name provided.
*
* @private
* @param {Message} map of messages
*/
function addNameToKey(messages, number) {
messages.forEach(m => {
if (m.name() === undefined && m.ext(xParserMessageName) === undefined) {
m.json()[String(xParserMessageName)] = `<anonymous-message-${number}>`;
}
});
}
/**
* Gives schemas id to all anonymous schemas.
*
* @private
* @param {AsyncAPIDocument} doc
*/
function assignIdToAnonymousSchemas(doc) {
let anonymousSchemaCounter = 0;
const callback = (schema) => {
if (!schema.uid()) {
schema.json()[String(xParserSchemaId)] = `<anonymous-schema-${++anonymousSchemaCounter}>`;
}
};
traverseAsyncApiDocument(doc, callback);
}
module.exports = {
assignNameToComponentMessages,
assignUidToParameterSchemas,
assignUidToComponentSchemas,
assignUidToComponentParameterSchemas,
assignNameToAnonymousMessages,
assignIdToAnonymousSchemas
};
@@ -0,0 +1,116 @@
const Ajv = require('ajv');
const ParserError = require('./errors/parser-error');
const asyncapi = require('@asyncapi/specs');
const { improveAjvErrors } = require('./utils');
const cloneDeep = require('lodash.clonedeep');
const ajv = new Ajv({
jsonPointers: true,
allErrors: true,
schemaId: 'auto',
logger: false,
});
ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json'));
module.exports = {
parse,
getMimeTypes
};
/**
* @private
*/
async function parse({ message, originalAsyncAPIDocument, fileFormat, parsedAsyncAPIDocument, pathToPayload, defaultSchemaFormat }) {
const payload = message.payload;
if (!payload) return;
message['x-parser-original-schema-format'] = message.schemaFormat || defaultSchemaFormat;
message['x-parser-original-payload'] = cloneDeep(message.payload);
const validate = getValidator(parsedAsyncAPIDocument.asyncapi);
const valid = validate(payload);
const errors = validate.errors && [...validate.errors];
if (!valid) throw new ParserError({
type: 'schema-validation-errors',
title: 'This is not a valid AsyncAPI Schema Object.',
parsedJSON: parsedAsyncAPIDocument,
validationErrors: improveAjvErrors(addFullPathToDataPath(errors, pathToPayload), originalAsyncAPIDocument, fileFormat),
});
}
/**
* @private
*/
function getMimeTypes() {
const mimeTypes = [
'application/schema;version=draft-07',
'application/schema+json;version=draft-07',
'application/schema+yaml;version=draft-07',
];
['2.0.0', '2.1.0', '2.2.0', '2.3.0', '2.4.0', '2.5.0', '2.6.0'].forEach(version => {
mimeTypes.push(
`application/vnd.aai.asyncapi;version=${version}`,
`application/vnd.aai.asyncapi+json;version=${version}`,
`application/vnd.aai.asyncapi+yaml;version=${version}`,
);
});
return mimeTypes;
}
/**
* Creates (or reuses) a function that validates an AsyncAPI Schema Object based on the passed AsyncAPI version.
*
* @private
* @param {Object} version AsyncAPI version.
* @returns {Function} Function that validates an AsyncAPI Schema Object based on the passed AsyncAPI version.
*/
function getValidator(version) {
let validate = ajv.getSchema(version);
if (!validate) {
const payloadSchema = preparePayloadSchema(asyncapi[String(version)], version);
ajv.addSchema(payloadSchema, version);
validate = ajv.getSchema(version);
}
return validate;
}
/**
* To validate schema of the payload we just need a small portion of official AsyncAPI spec JSON Schema, the definition of the schema must be
* a main part of the JSON Schema
*
* @private
* @param {Object} asyncapiSchema AsyncAPI specification JSON Schema
* @param {Object} version AsyncAPI version.
* @returns {Object} valid JSON Schema document describing format of AsyncAPI-valid schema for message payload
*/
function preparePayloadSchema(asyncapiSchema, version) {
const payloadSchema = `http://asyncapi.com/definitions/${version}/schema.json`;
const definitions = asyncapiSchema.definitions;
// Remove the meta schemas because it is already present within Ajv, and it's not possible to add duplicate schemas.
delete definitions['http://json-schema.org/draft-07/schema'];
delete definitions['http://json-schema.org/draft-04/schema'];
return {
$ref: payloadSchema,
definitions
};
}
/**
* Errors from Ajv contain dataPath information about parameter relative to parsed payload message.
* This function enriches dataPath with additional information on where is the parameter located in AsyncAPI document
*
* @private
* @param {Array<Object>} errors Ajv errors
* @param {String} path Path to location of the payload schema in AsyncAPI Document
* @returns {Array<Object>} same object as received in input but with modified datePath property so it contain full path relative to AsyncAPI document
*/
function addFullPathToDataPath(errors, path) {
return errors.map((err) => ({
...err,
...{
dataPath: `${path}${err.dataPath}`
}
}));
}
+1
View File
@@ -0,0 +1 @@
window.AsyncAPIParser = require('./index');
+15
View File
@@ -0,0 +1,15 @@
const xParserSpecParsed = 'x-parser-spec-parsed';
const xParserSpecStringified = 'x-parser-spec-stringified';
const xParserMessageName = 'x-parser-message-name';
const xParserSchemaId = 'x-parser-schema-id';
const xParserCircle = 'x-parser-circular';
const xParserCircleProps = 'x-parser-circular-props';
module.exports = {
xParserSpecParsed,
xParserSpecStringified,
xParserMessageName,
xParserSchemaId,
xParserCircle,
xParserCircleProps
};
+682
View File
@@ -0,0 +1,682 @@
const ParserError = require('./errors/parser-error');
// eslint-disable-next-line no-unused-vars
const Operation = require('./models/operation');
const {
parseUrlVariables,
getMissingProps,
groupValidationErrors,
tilde,
parseUrlQueryParameters,
setNotProvidedParams,
getUnknownServers
} = require('./utils');
const validationError = 'validation-errors';
/**
* Validates if variables provided in the url have corresponding variable object defined and if example is correct
* @private
* @param {Object} parsedJSON parsed AsyncAPI document
* @param {String} asyncapiYAMLorJSON AsyncAPI document in string
* @param {String} initialFormat information of the document was originally JSON or YAML
* @returns {Boolean} true in case the document is valid, otherwise throws {@link ParserError}
*/
function validateServerVariables(
parsedJSON,
asyncapiYAMLorJSON,
initialFormat
) {
const srvs = parsedJSON.servers;
if (!srvs) return true;
const srvsMap = new Map(Object.entries(srvs));
const notProvidedVariables = new Map();
const notProvidedExamplesInEnum = new Map();
srvsMap.forEach((srvr, srvrName) => {
const variables = parseUrlVariables(srvr.url);
const variablesObj = srvr.variables;
const notProvidedServerVars = notProvidedVariables.get(tilde(srvrName));
if (!variables) return;
const missingServerVariables = getMissingProps(variables, variablesObj);
if (missingServerVariables.length) {
notProvidedVariables.set(
tilde(srvrName),
notProvidedServerVars
? notProvidedServerVars.concat(missingServerVariables)
: missingServerVariables
);
}
if (variablesObj) {
setNotValidExamples(variablesObj, srvrName, notProvidedExamplesInEnum);
}
});
if (notProvidedVariables.size) {
throw new ParserError({
type: validationError,
title: 'Not all server variables are described with variable object',
parsedJSON,
validationErrors: groupValidationErrors(
'servers',
'server does not have a corresponding variable object for',
notProvidedVariables,
asyncapiYAMLorJSON,
initialFormat
),
});
}
if (notProvidedExamplesInEnum.size) {
throw new ParserError({
type: validationError,
title:
'Check your server variables. The example does not match the enum list',
parsedJSON,
validationErrors: groupValidationErrors(
'servers',
'server variable provides an example that does not match the enum list',
notProvidedExamplesInEnum,
asyncapiYAMLorJSON,
initialFormat
),
});
}
return true;
}
/**
* extend map with info about examples that are not part of the enum
*
* @function setNotValidExamples
* @private
* @param {Array<Object>} variables server variables object
* @param {String} srvrName name of the server where variables object is located
* @param {Map} notProvidedExamplesInEnum result map of all wrong examples and what variable they belong to
*/
function setNotValidExamples(variables, srvrName, notProvidedExamplesInEnum) {
const variablesMap = new Map(Object.entries(variables));
variablesMap.forEach((variable, variableName) => {
if (variable.enum && variable.examples) {
const wrongExamples = variable.examples.filter(r => !variable.enum.includes(r));
if (wrongExamples.length) {
notProvidedExamplesInEnum.set(
`${tilde(srvrName)}/variables/${tilde(variableName)}`,
wrongExamples
);
}
}
});
}
/**
* Validates if operationIds are duplicated in the document
*
* @private
* @param {Object} parsedJSON parsed AsyncAPI document
* @param {String} asyncapiYAMLorJSON AsyncAPI document in string
* @param {String} initialFormat information of the document was originally JSON or YAML
* @returns {Boolean} true in case the document is valid, otherwise throws {@link ParserError}
*/
function validateOperationId(
parsedJSON,
asyncapiYAMLorJSON,
initialFormat,
operations
) {
const chnls = parsedJSON.channels;
if (!chnls) return true;
const chnlsMap = new Map(Object.entries(chnls));
//it is a map of paths, the one that is a duplicate and the one that is duplicated
const duplicatedOperations = new Map();
//is is a 2-dimensional array that holds information with operationId value and its path
const allOperations = [];
const addDuplicateToMap = (op, channelName, opName) => {
const operationId = op.operationId;
if (!operationId) return;
const operationPath = `${tilde(channelName)}/${opName}/operationId`;
const isOperationIdDuplicated = allOperations.filter(
(v) => v[0] === operationId
);
if (!isOperationIdDuplicated.length)
return allOperations.push([operationId, operationPath]);
//isOperationIdDuplicated always holds one record and it is an array of paths, the one that is a duplicate and the one that is duplicated
duplicatedOperations.set(operationPath, isOperationIdDuplicated[0][1]);
};
chnlsMap.forEach((chnlObj, chnlName) => {
operations.forEach((opName) => {
const op = chnlObj[String(opName)];
if (op) addDuplicateToMap(op, chnlName, opName);
});
});
if (duplicatedOperations.size) {
throw new ParserError({
type: validationError,
title: 'operationId must be unique across all the operations.',
parsedJSON,
validationErrors: groupValidationErrors(
'channels',
'is a duplicate of',
duplicatedOperations,
asyncapiYAMLorJSON,
initialFormat
),
});
}
return true;
}
/**
* Validates if messageIds are duplicated in the document
*
* @private
* @param {Object} parsedJSON parsed AsyncAPI document
* @param {String} asyncapiYAMLorJSON AsyncAPI document in string
* @param {String} initialFormat information of the document was originally JSON or YAML
* @returns {Boolean} true in case the document is valid, otherwise throws {@link ParserError}
*/
function validateMessageId(
parsedJSON,
asyncapiYAMLorJSON,
initialFormat,
operations
) {
const chnls = parsedJSON.channels;
if (!chnls) return true;
const chnlsMap = new Map(Object.entries(chnls));
//it is a map of paths, the one that is a duplicate and the one that is duplicated
const duplicatedMessages = new Map();
//is is a 2-dimensional array that holds information with messageId value and its path
const allMessages = [];
const addDuplicateToMap = (msg, channelName, opName, oneOf = '') => {
const messageId = msg.messageId;
if (!messageId) return;
const messagePath = `${tilde(channelName)}/${opName}/message${oneOf}/messageId`;
const isMessageIdDuplicated = allMessages.find(v => v[0] === messageId);
if (!isMessageIdDuplicated)
return allMessages.push([messageId, messagePath]);
//isMessageIdDuplicated always holds one record and it is an array of paths, the one that is a duplicate and the one that is duplicated
duplicatedMessages.set(messagePath, isMessageIdDuplicated[1]);
};
chnlsMap.forEach((chnlObj, chnlName) => {
operations.forEach((opName) => {
const op = chnlObj[String(opName)];
if (op && op.message) {
if (op.message.oneOf) op.message.oneOf.forEach((msg, index) => addDuplicateToMap(msg, chnlName, opName , `/oneOf/${index}`));
else addDuplicateToMap(op.message, chnlName, opName);
}
});
});
if (duplicatedMessages.size) {
throw new ParserError({
type: validationError,
title: 'messageId must be unique across all the messages.',
parsedJSON,
validationErrors: groupValidationErrors(
'channels',
'is a duplicate of',
duplicatedMessages,
asyncapiYAMLorJSON,
initialFormat
),
});
}
return true;
}
/**
* Validates if server security is declared properly and the name has a corresponding security schema definition in components with the same name
*
* @private
* @param {Object} parsedJSON parsed AsyncAPI document
* @param {String} asyncapiYAMLorJSON AsyncAPI document in string
* @param {String} initialFormat information of the document was originally JSON or YAML
* @param {String[]} specialSecTypes list of security types that can have data in array
* @returns {Boolean} true in case the document is valid, otherwise throws {@link ParserError}
*/
function validateServerSecurity(
parsedJSON,
asyncapiYAMLorJSON,
initialFormat,
specialSecTypes
) {
const srvs = parsedJSON.servers;
if (!srvs) return true;
const root = 'servers';
const srvsMap = new Map(Object.entries(srvs));
const missingSecSchema = new Map(),
invalidSecurityValues = new Map();
//we need to validate every server specified in the document
srvsMap.forEach((server, serverName) => {
const serverSecInfo = server.security;
if (!serverSecInfo) return true;
//server security info is an array of many possible values
serverSecInfo.forEach((secObj) => {
Object.keys(secObj).forEach((secName) => {
//security schema is located in components object, we need to find if there is security schema with the same name as the server security info object
const schema = findSecuritySchema(secName, parsedJSON.components);
const srvrSecurityPath = `${serverName}/security/${secName}`;
if (!schema.length) return missingSecSchema.set(srvrSecurityPath);
//findSecuritySchema returns type always on index 1. Type is needed further to validate if server security info can be or not an empty array
const schemaType = schema[1];
if (!isSrvrSecProperArray(schemaType, specialSecTypes, secObj, secName))
invalidSecurityValues.set(srvrSecurityPath, schemaType);
});
});
});
if (missingSecSchema.size) {
throw new ParserError({
type: validationError,
title:
'Server security name must correspond to a security scheme which is declared in the security schemes under the components object.',
parsedJSON,
validationErrors: groupValidationErrors(
root,
'doesn\'t have a corresponding security schema under the components object',
missingSecSchema,
asyncapiYAMLorJSON,
initialFormat
),
});
}
if (invalidSecurityValues.size) {
throw new ParserError({
type: validationError,
title:
'Server security value must be an empty array if corresponding security schema type is not oauth2 or openIdConnect.',
parsedJSON,
validationErrors: groupValidationErrors(
root,
'security info must have an empty array because its corresponding security schema type is',
invalidSecurityValues,
asyncapiYAMLorJSON,
initialFormat
),
});
}
return true;
}
/**
* Searches for server security corresponding object in security schema object
* @private
* @param {String} securityName name of the server security element that you want to localize in the security schema object
* @param {Object} components components object from the AsyncAPI document
* @returns {String[]} there are 2 elements in array, index 0 is the name of the security schema object and index 1 is it's type
*/
function findSecuritySchema(securityName, components) {
const secSchemes = components && components.securitySchemes;
const secSchemesMap = secSchemes
? new Map(Object.entries(secSchemes))
: new Map();
const schemaInfo = [];
//using for loop here as there is no point to iterate over all entries as it is enough to find first matching element
for (const [schemaName, schema] of secSchemesMap.entries()) {
if (schemaName === securityName) {
schemaInfo.push(schemaName, schema.type);
return schemaInfo;
}
}
return schemaInfo;
}
/**
* Validates if given server security is a proper empty array when security type requires it
* @private
* @param {String} schemaType security type, like httpApiKey or userPassword
* @param {String[]} specialSecTypes list of special types that do not have to be an empty array
* @param {Object} secObj server security object
* @param {String} secName name os server security object
* @returns {String[]} there are 2 elements in array, index 0 is the name of the security schema object and index 1 is it's type
*/
function isSrvrSecProperArray(schemaType, specialSecTypes, secObj, secName) {
if (!specialSecTypes.includes(schemaType)) {
const securityObjValue = secObj[String(secName)];
return !securityObjValue.length;
}
return true;
}
/**
* Validates if parameters specified in the channel have corresponding parameters object defined and if name does not contain url parameters.
* Also validates that all servers listed for this channel are declared in the top-level servers object.
*
* @private
* @param {Object} parsedJSON parsed AsyncAPI document
* @param {String} asyncapiYAMLorJSON AsyncAPI document in string
* @param {String} initialFormat information of the document was originally JSON or YAML
* @returns {Boolean} true in case the document is valid, otherwise throws {@link ParserError}
*/
function validateChannels(parsedJSON, asyncapiYAMLorJSON, initialFormat) {
const chnls = parsedJSON.channels;
if (!chnls) return true;
const chnlsMap = new Map(Object.entries(chnls));
const notProvidedParams = new Map(); //return object for missing parameters
const invalidChannelName = new Map(); //return object for invalid channel names with query parameters
const unknownServers = new Map(); //return object for server names not declared in top-level servers object
chnlsMap.forEach((val, key) => {
const variables = parseUrlVariables(key);
const notProvidedChannelParams = notProvidedParams.get(tilde(key));
const queryParameters = parseUrlQueryParameters(key);
const unknownServerNames = getUnknownServers(parsedJSON, val);
//channel variable validation: fill return object with missing parameters
if (variables) {
setNotProvidedParams(
variables,
val,
key,
notProvidedChannelParams,
notProvidedParams
);
}
//channel name validation: fill return object with channels containing query parameters
if (queryParameters) {
invalidChannelName.set(tilde(key), queryParameters);
}
//server validation: fill return object with unknown server names
if (unknownServerNames.length > 0) {
unknownServers.set(tilde(key), unknownServerNames);
}
});
//combine validation errors of both checks and output them as one array
const parameterValidationErrors = groupValidationErrors(
'channels',
'channel does not have a corresponding parameter object for',
notProvidedParams,
asyncapiYAMLorJSON,
initialFormat
);
const nameValidationErrors = groupValidationErrors(
'channels',
'channel contains invalid name with url query parameters',
invalidChannelName,
asyncapiYAMLorJSON,
initialFormat
);
const serverValidationErrors = groupValidationErrors(
'channels',
'channel contains servers that are not on the servers list in the root of the document',
unknownServers,
asyncapiYAMLorJSON,
initialFormat
);
const allValidationErrors = parameterValidationErrors.concat(nameValidationErrors).concat(serverValidationErrors);
//channel variable validation: throw exception if channel validation fails
if (notProvidedParams.size || invalidChannelName.size || unknownServers.size) {
throw new ParserError({
type: validationError,
title: 'Channel validation failed',
parsedJSON,
validationErrors: allValidationErrors,
});
}
return true;
}
/**
* Validates if tags specified in the following objects have no duplicates: root, operations, operation traits, channels,
* messages and message traits.
*
* @private
* @param {Object} parsedJSON parsed AsyncAPI document
* @param {String} asyncapiYAMLorJSON AsyncAPI document in string
* @param {String} initialFormat information of the document was originally JSON or YAML
* @returns {Boolean} true in case the document is valid, otherwise throws {@link ParserError}
*/
function validateTags(parsedJSON, asyncapiYAMLorJSON, initialFormat) {
const invalidRoot = validateRootTags(parsedJSON);
const invalidChannels = validateAllChannelsTags(parsedJSON);
const invalidOperationTraits = validateOperationTraitTags(parsedJSON);
const invalidMessages = validateMessageTags(parsedJSON);
const invalidMessageTraits = validateMessageTraitsTags(parsedJSON);
const errorMessage = 'contains duplicate tag names';
let invalidRootValidationErrors = [];
let invalidChannelsValidationErrors = [];
let invalidOperationTraitsValidationErrors = [];
let invalidMessagesValidationErrors = [];
let invalidMessageTraitsValidationErrors = [];
if (invalidRoot.size) {
invalidRootValidationErrors = groupValidationErrors(
null,
errorMessage,
invalidRoot,
asyncapiYAMLorJSON,
initialFormat
);
}
if (invalidChannels.size) {
invalidChannelsValidationErrors = groupValidationErrors(
'channels',
errorMessage,
invalidChannels,
asyncapiYAMLorJSON,
initialFormat
);
}
if (invalidOperationTraits.size) {
invalidOperationTraitsValidationErrors = groupValidationErrors(
'components',
errorMessage,
invalidOperationTraits,
asyncapiYAMLorJSON,
initialFormat
);
}
if (invalidMessages.size) {
invalidMessagesValidationErrors = groupValidationErrors(
'components',
errorMessage,
invalidMessages,
asyncapiYAMLorJSON,
initialFormat
);
}
if (invalidMessageTraits.size) {
invalidMessageTraitsValidationErrors = groupValidationErrors(
'components',
errorMessage,
invalidMessageTraits,
asyncapiYAMLorJSON,
initialFormat
);
}
const allValidationErrors = invalidRootValidationErrors
.concat(invalidChannelsValidationErrors)
.concat(invalidOperationTraitsValidationErrors)
.concat(invalidMessagesValidationErrors)
.concat(invalidMessageTraitsValidationErrors);
if (allValidationErrors.length) {
throw new ParserError({
type: validationError,
title: 'Tags validation failed',
parsedJSON,
validationErrors: allValidationErrors,
});
}
return true;
}
function validateRootTags(parsedJSON) {
const invalidRoot = new Map();
const duplicateNames = parsedJSON.tags && getDuplicateTagNames(parsedJSON.tags);
if (duplicateNames && duplicateNames.length) {
invalidRoot.set('tags', duplicateNames.toString());
}
return invalidRoot;
}
function validateOperationTraitTags(parsedJSON) {
const invalidOperationTraits = new Map();
if (parsedJSON && parsedJSON.components && parsedJSON.components.operationTraits) {
Object.keys(parsedJSON.components.operationTraits).forEach((operationTrait) => {
// eslint-disable-next-line security/detect-object-injection
const duplicateNames = getDuplicateTagNames(parsedJSON.components.operationTraits[operationTrait].tags);
if (duplicateNames && duplicateNames.length) {
const operationTraitsPath = `operationTraits/${operationTrait}/tags`;
invalidOperationTraits.set(
operationTraitsPath,
duplicateNames.toString()
);
}
});
}
return invalidOperationTraits;
}
function validateAllChannelsTags(parsedJSON) {
const chnls = parsedJSON.channels;
if (!chnls) return true;
const chnlsMap = new Map(Object.entries(chnls));
const invalidChannels = new Map();
chnlsMap.forEach((channel, channelName) => validateChannelTags(invalidChannels, channel, channelName));
return invalidChannels;
}
function validateChannelTags(invalidChannels, channel, channelName) {
if (channel.publish) {
validateOperationTags(invalidChannels, channel.publish, `${tilde(channelName)}/publish`);
}
if (channel.subscribe) {
validateOperationTags(invalidChannels, channel.subscribe, `${tilde(channelName)}/subscribe`);
}
}
/**
* Check tags in operation and in message.
*
* @private
* @param {Map} invalidChannels map with invalid channel entries
* @param {Operation} operation operation object
* @param {String} operationPath operation path
*/
function validateOperationTags(invalidChannels, operation, operationPath) {
if (!operation) return;
tryAddInvalidEntries(invalidChannels, `${operationPath}/tags`, operation.tags);
if (operation.message) {
if (operation.message.oneOf) {
operation.message.oneOf.forEach((message, idx) => {
tryAddInvalidEntries(invalidChannels, `${operationPath}/message/oneOf/${idx}/tags`, message.tags);
});
} else {
tryAddInvalidEntries(invalidChannels, `${operationPath}/message/tags`, operation.message.tags);
}
}
}
function tryAddInvalidEntries(invalidChannels, key, tags) {
const duplicateNames = tags && getDuplicateTagNames(tags);
if (duplicateNames && duplicateNames.length) {
invalidChannels.set(key, duplicateNames.toString());
}
}
function validateMessageTraitsTags(parsedJSON) {
const invalidMessageTraits = new Map();
if (parsedJSON && parsedJSON.components && parsedJSON.components.messageTraits) {
Object.keys(parsedJSON.components.messageTraits).forEach((messageTrait) => {
// eslint-disable-next-line security/detect-object-injection
const duplicateNames = getDuplicateTagNames(parsedJSON.components.messageTraits[messageTrait].tags);
if (duplicateNames && duplicateNames.length) {
const messageTraitsPath = `messageTraits/${messageTrait}/tags`;
invalidMessageTraits.set(messageTraitsPath, duplicateNames.toString());
}
});
}
return invalidMessageTraits;
}
function validateMessageTags(parsedJSON) {
const invalidMessages = new Map();
if (parsedJSON && parsedJSON.components && parsedJSON.components.messages) {
Object.keys(parsedJSON.components.messages).forEach((message) => {
// eslint-disable-next-line security/detect-object-injection
const duplicateNames = getDuplicateTagNames(parsedJSON.components.messages[message].tags);
if (duplicateNames && duplicateNames.length) {
const messagePath = `messages/${message}/tags`;
invalidMessages.set(messagePath, duplicateNames.toString());
}
});
}
return invalidMessages;
}
function getDuplicateTagNames(tags) {
if (!tags) return null;
const tagNames = tags.map((item) => item.name);
return tagNames.reduce((acc, item, idx, arr) => {
if (arr.indexOf(item) !== idx && acc.indexOf(item) < 0) {
acc.push(item);
}
return acc;
}, []);
}
module.exports = {
validateServerVariables,
validateOperationId,
validateMessageId,
validateServerSecurity,
validateChannels,
validateTags,
};
@@ -0,0 +1,64 @@
const ERROR_URL_PREFIX = 'https://github.com/asyncapi/parser-js/';
const buildError = (from, to) => {
to.type = from.type.startsWith(ERROR_URL_PREFIX) ? from.type : `${ERROR_URL_PREFIX}${from.type}`;
to.title = from.title;
if (from.detail) to.detail = from.detail;
if (from.validationErrors) to.validationErrors = from.validationErrors;
if (from.parsedJSON) to.parsedJSON = from.parsedJSON;
if (from.location) to.location = from.location;
if (from.refs) to.refs = from.refs;
return to;
};
/**
* Represents an error while trying to parse an AsyncAPI document.
* @alias module:@asyncapi/parser#ParserError
* @extends Error
*/
class ParserError extends Error {
/**
* Instantiates an error
* @param {Object} definition
* @param {String} definition.type The type of the error.
* @param {String} definition.title The message of the error.
* @param {String} [definition.detail] A string containing more detailed information about the error.
* @param {Object} [definition.parsedJSON] The resulting JSON after YAML transformation. Or the JSON object if the this was the initial format.
* @param {Object[]} [definition.validationErrors] The errors resulting from the validation. For more information, see https://www.npmjs.com/package/better-ajv-errors.
* @param {String} definition.validationErrors.title A validation error message.
* @param {String} definition.validationErrors.jsonPointer The path to the field that contains the error. Uses JSON Pointer format.
* @param {Number} definition.validationErrors.startLine The line where the error starts in the AsyncAPI document.
* @param {Number} definition.validationErrors.startColumn The column where the error starts in the AsyncAPI document.
* @param {Number} definition.validationErrors.startOffset The offset (starting from the beginning of the document) where the error starts in the AsyncAPI document.
* @param {Number} definition.validationErrors.endLine The line where the error ends in the AsyncAPI document.
* @param {Number} definition.validationErrors.endColumn The column where the error ends in the AsyncAPI document.
* @param {Number} definition.validationErrors.endOffset The offset (starting from the beginning of the document) where the error ends in the AsyncAPI document.
* @param {Object} [definition.location] Error location details after trying to parse an invalid JSON or YAML document.
* @param {Number} definition.location.startLine The line of the YAML/JSON document where the error starts.
* @param {Number} definition.location.startColumn The column of the YAML/JSON document where the error starts.
* @param {Number} definition.location.startOffset The offset (starting from the beginning of the document) where the error starts in the YAML/JSON AsyncAPI document.
* @param {Object[]} [definition.refs] Error details after trying to resolve $ref's.
* @param {String} definition.refs.title A validation error message.
* @param {String} definition.refs.jsonPointer The path to the field that contains the error. Uses JSON Pointer format.
* @param {Number} definition.refs.startLine The line where the error starts in the AsyncAPI document.
* @param {Number} definition.refs.startColumn The column where the error starts in the AsyncAPI document.
* @param {Number} definition.refs.startOffset The offset (starting from the beginning of the document) where the error starts in the AsyncAPI document.
* @param {Number} definition.refs.endLine The line where the error ends in the AsyncAPI document.
* @param {Number} definition.refs.endColumn The column where the error ends in the AsyncAPI document.
* @param {Number} definition.refs.endOffset The offset (starting from the beginning of the document) where the error ends in the AsyncAPI document.
*/
constructor(def) {
super();
buildError(def, this);
this.message = def.title;
}
/**
* Returns a JS object representation of the error.
*/
toJS() {
return buildError(this, {});
}
}
module.exports = ParserError;
+6
View File
@@ -0,0 +1,6 @@
const parser = require('./parser');
const defaultAsyncAPISchemaParser = require('./asyncapiSchemaFormatParser');
parser.registerSchemaParser(defaultAsyncAPISchemaParser);
module.exports = parser;
+325
View File
@@ -0,0 +1,325 @@
/**
* @readonly
* @enum {SchemaIteratorCallbackType}
*/
/**
* The different kind of stages when crawling a schema.
*
* @typedef SchemaIteratorCallbackType
* @property {string} NEW_SCHEMA The crawler just started crawling a schema.
* @property {string} END_SCHEMA The crawler just finished crawling a schema.
*/
const SchemaIteratorCallbackType = Object.freeze({
NEW_SCHEMA: 'NEW_SCHEMA',
END_SCHEMA: 'END_SCHEMA'
});
/**
*
* @readonly
* @enum {SchemaTypesToIterate}
*/
/**
* The different types of schemas you can iterate
*
* @typedef SchemaTypesToIterate
* @property {string} parameters Crawl all schemas in parameters
* @property {string} payloads Crawl all schemas in payloads
* @property {string} headers Crawl all schemas in headers
* @property {string} components Crawl all schemas in components
* @property {string} objects Crawl all schemas of type object
* @property {string} arrays Crawl all schemas of type array
* @property {string} oneOfs Crawl all schemas in oneOf's
* @property {string} allOfs Crawl all schemas in allOf's
* @property {string} anyOfs Crawl all schemas in anyOf's
* @property {string} nots Crawl all schemas in not field
* @property {string} propertyNames Crawl all schemas in propertyNames field
* @property {string} patternProperties Crawl all schemas in patternProperties field
* @property {string} contains Crawl all schemas in contains field
* @property {string} ifs Crawl all schemas in if field
* @property {string} thenes Crawl all schemas in then field
* @property {string} elses Crawl all schemas in else field
* @property {string} dependencies Crawl all schemas in dependencies field
* @property {string} definitions Crawl all schemas in definitions field
*/
const SchemaTypesToIterate = Object.freeze({
parameters: 'parameters',
payloads: 'payloads',
headers: 'headers',
components: 'components',
objects: 'objects',
arrays: 'arrays',
oneOfs: 'oneOfs',
allOfs: 'allOfs',
anyOfs: 'anyOfs',
nots: 'nots',
propertyNames: 'propertyNames',
patternProperties: 'patternProperties',
contains: 'contains',
ifs: 'ifs',
thenes: 'thenes',
elses: 'elses',
dependencies: 'dependencies',
definitions: 'definitions',
});
/* eslint-disable sonarjs/cognitive-complexity */
/**
* Traverse current schema and all nested schemas.
*
* @private
* @param {Schema} schema which is being crawled.
* @param {(String | Number)} propOrIndex if the schema is from a property/index get the name/number of such.
* @param {Object} options
* @param {SchemaIteratorCallbackType} [options.callback] callback used when crawling a schema.
* @param {SchemaTypesToIterate[]} [options.schemaTypesToIterate] list of schema types to crawl.
* @param {Set<Object>} [options.seenSchemas] Set which holds all defined schemas in the tree - it is mainly used to check circular references
*/
function traverseSchema(schema, propOrIndex, options) { // NOSONAR
if (!schema) return;
const { callback, schemaTypesToIterate, seenSchemas } = options;
// handle circular references
const jsonSchema = schema.json();
if (seenSchemas.has(jsonSchema)) return;
seenSchemas.add(jsonSchema);
// `type` isn't required so save type as array in the fallback
let types = schema.type() || [];
// change primitive type to array of types for easier handling
if (!Array.isArray(types)) {
types = [types];
}
if (!schemaTypesToIterate.includes(SchemaTypesToIterate.objects) && types.includes('object')) return;
if (!schemaTypesToIterate.includes(SchemaTypesToIterate.arrays) && types.includes('array')) return;
// check callback `NEW_SCHEMA` case
if (callback(schema, propOrIndex, SchemaIteratorCallbackType.NEW_SCHEMA) === false) return;
if (schemaTypesToIterate.includes(SchemaTypesToIterate.objects) && types.includes('object')) {
recursiveSchemaObject(schema, options);
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.arrays) && types.includes('array')) {
recursiveSchemaArray(schema, options);
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.oneOfs)) {
(schema.oneOf() || []).forEach((combineSchema, idx) => {
traverseSchema(combineSchema, idx, options);
});
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.anyOfs)) {
(schema.anyOf() || []).forEach((combineSchema, idx) => {
traverseSchema(combineSchema, idx, options);
});
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.allOfs)) {
(schema.allOf() || []).forEach((combineSchema, idx) => {
traverseSchema(combineSchema, idx, options);
});
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.nots) && schema.not()) {
traverseSchema(schema.not(), null, options);
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.ifs) && schema.if()) {
traverseSchema(schema.if(), null, options);
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.thenes) && schema.then()) {
traverseSchema(schema.then(), null, options);
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.elses) && schema.else()) {
traverseSchema(schema.else(), null, options);
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.dependencies)) {
Object.entries(schema.dependencies() || {}).forEach(([depName, dep]) => {
// do not iterate dependent required
if (dep && !Array.isArray(dep)) {
traverseSchema(dep, depName, options);
}
});
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.definitions)) {
Object.entries(schema.definitions() || {}).forEach(([defName, def]) => {
traverseSchema(def, defName, options);
});
}
callback(schema, propOrIndex, SchemaIteratorCallbackType.END_SCHEMA);
seenSchemas.delete(jsonSchema);
}
/* eslint-enable sonarjs/cognitive-complexity */
/**
* Recursively go through schema of object type and execute callback.
*
* @private
* @param {Object} options
* @param {SchemaIteratorCallbackType} [options.callback] callback used when crawling a schema.
* @param {SchemaTypesToIterate[]} [options.schemaTypesToIterate] list of schema types to crawl.
* @param {Set<Object>} [options.seenSchemas] Set which holds all defined schemas in the tree - it is mainly used to check circular references
*/
function recursiveSchemaObject(schema, options) {
Object.entries(schema.properties() || {}).forEach(([propertyName, property]) => {
traverseSchema(property, propertyName, options);
});
const additionalProperties = schema.additionalProperties();
if (typeof additionalProperties === 'object') {
traverseSchema(additionalProperties, null, options);
}
const schemaTypesToIterate = options.schemaTypesToIterate;
if (schemaTypesToIterate.includes(SchemaTypesToIterate.propertyNames) && schema.propertyNames()) {
traverseSchema(schema.propertyNames(), null, options);
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.patternProperties)) {
Object.entries(schema.patternProperties() || {}).forEach(([propertyName, property]) => {
traverseSchema(property, propertyName, options);
});
}
}
/**
* Recursively go through schema of array type and execute callback.
*
* @private
* @param {Object} options
* @param {SchemaIteratorCallbackType} [options.callback] callback used when crawling a schema.
* @param {SchemaTypesToIterate[]} [options.schemaTypesToIterate] list of schema types to crawl.
* @param {Set<Object>} [options.seenSchemas] Set which holds all defined schemas in the tree - it is mainly used to check circular references
*/
function recursiveSchemaArray(schema, options) {
const items = schema.items();
if (items) {
if (Array.isArray(items)) {
items.forEach((item, idx) => {
traverseSchema(item, idx, options);
});
} else {
traverseSchema(items, null, options);
}
}
const additionalItems = schema.additionalItems();
if (typeof additionalItems === 'object') {
traverseSchema(additionalItems, null, options);
}
if (options.schemaTypesToIterate.includes(SchemaTypesToIterate.contains) && schema.contains()) {
traverseSchema(schema.contains(), null, options);
}
}
/**
* Go through each channel and for each parameter, and message payload and headers recursively call the callback for each schema.
*
* @private
* @param {AsyncAPIDocument} doc parsed AsyncAPI Document
* @param {FoundSchemaCallback} callback callback used when crawling a schema.
* @param {SchemaTypesToIterate[]} schemaTypesToIterate list of schema types to crawl.
*/
function traverseAsyncApiDocument(doc, callback, schemaTypesToIterate) {
if (!schemaTypesToIterate) {
schemaTypesToIterate = Object.values(SchemaTypesToIterate);
}
const options = { callback, schemaTypesToIterate, seenSchemas: new Set() };
if (doc.hasChannels()) {
Object.values(doc.channels()).forEach(channel => {
traverseChannel(channel, options);
});
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.components) && doc.hasComponents()) {
const components = doc.components();
Object.values(components.messages() || {}).forEach(message => {
traverseMessage(message, options);
});
Object.values(components.schemas() || {}).forEach(schema => {
traverseSchema(schema, null, options);
});
if (schemaTypesToIterate.includes(SchemaTypesToIterate.parameters)) {
Object.values(components.parameters() || {}).forEach(parameter => {
traverseSchema(parameter.schema(), null, options);
});
}
Object.values(components.messageTraits() || {}).forEach(messageTrait => {
traverseMessageTrait(messageTrait, options);
});
}
}
/**
* Go through each schema in channel
*
* @private
* @param {Channel} channel
* @param {Object} options
* @param {SchemaIteratorCallbackType} [options.callback] callback used when crawling a schema.
* @param {SchemaTypesToIterate[]} [options.schemaTypesToIterate] list of schema types to crawl.
* @param {Set<Object>} [options.seenSchemas] Set which holds all defined schemas in the tree - it is mainly used to check circular references
*/
function traverseChannel(channel, options) {
if (!channel) return;
const { schemaTypesToIterate } = options;
if (schemaTypesToIterate.includes(SchemaTypesToIterate.parameters)) {
Object.values(channel.parameters() || {}).forEach(parameter => {
traverseSchema(parameter.schema(), null, options);
});
}
if (channel.hasPublish()) {
channel.publish().messages().forEach(message => {
traverseMessage(message, options);
});
}
if (channel.hasSubscribe()) {
channel.subscribe().messages().forEach(message => {
traverseMessage(message, options);
});
}
}
/**
* Go through each schema in a message
*
* @private
* @param {Message} message
* @param {Object} options
* @param {SchemaIteratorCallbackType} [options.callback] callback used when crawling a schema.
* @param {SchemaTypesToIterate[]} [options.schemaTypesToIterate] list of schema types to crawl.
* @param {Set<Object>} [options.seenSchemas] Set which holds all defined schemas in the tree - it is mainly used to check circular references
*/
function traverseMessage(message, options) {
if (!message) return;
const { schemaTypesToIterate } = options;
if (schemaTypesToIterate.includes(SchemaTypesToIterate.headers)) {
traverseSchema(message.headers(), null, options);
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.payloads)) {
traverseSchema(message.payload(), null, options);
}
}
/**
* Go through each schema in a messageTrait
*
* @private
* @param {MessageTrait} messageTrait
* @param {Object} options
* @param {SchemaIteratorCallbackType} [options.callback] callback used when crawling a schema.
* @param {SchemaTypesToIterate[]} [options.schemaTypesToIterate] list of schema types to crawl.
* @param {Set<Object>} [options.seenSchemas] Set which holds all defined schemas in the tree - it is mainly used to check circular references
*/
function traverseMessageTrait(messageTrait, options) {
if (!messageTrait) return;
const { schemaTypesToIterate } = options;
if (schemaTypesToIterate.includes(SchemaTypesToIterate.headers)) {
traverseSchema(messageTrait.headers(), null, options);
}
}
module.exports = {
SchemaIteratorCallbackType,
SchemaTypesToIterate,
traverseAsyncApiDocument,
};
+43
View File
@@ -0,0 +1,43 @@
module.exports = (txt, reviver, context = 20) => {
try {
return JSON.parse(txt, reviver);
} catch (e) {
handleJsonNotString(txt);
const syntaxErr = e.message.match(/^Unexpected token.*position\s+(\d+)/i);
const errIdxBrokenJson = e.message.match(/^Unexpected end of JSON.*/i) ? txt.length - 1 : null;
const errIdx = syntaxErr ? +syntaxErr[1] : errIdxBrokenJson;
handleErrIdxNotNull(e, txt, errIdx, context);
e.offset = errIdx;
const lines = txt.substr(0, errIdx).split('\n');
e.startLine = lines.length;
e.startColumn = lines[lines.length - 1].length;
throw e;
}
};
function handleJsonNotString(txt) {
if (typeof txt !== 'string') {
const isEmptyArray = Array.isArray(txt) && txt.length === 0;
const errorMessage = `Cannot parse ${
isEmptyArray ? 'an empty array' : String(txt)}`;
throw new TypeError(errorMessage);
}
}
function handleErrIdxNotNull(e, txt, errIdx, context) {
if (errIdx !== null) {
const start = errIdx <= context
? 0
: errIdx - context;
const end = errIdx + context >= txt.length
? txt.length
: errIdx + context;
e.message += ` while parsing near '${
start === 0 ? '' : '...'
}${txt.slice(start, end)}${
end === txt.length ? '' : '...'
}'`;
} else {
e.message += ` while parsing '${txt.slice(0, context * 2)}'`;
}
}
+46
View File
@@ -0,0 +1,46 @@
const { getMapValueByKey } = require('../models/utils');
/**
* Implements functions to deal with the common Bindings object.
* @mixin
*/
const MixinBindings = {
/**
* @returns {boolean}
*/
hasBindings() {
return !!(this._json.bindings && Object.keys(this._json.bindings).length);
},
/**
* @returns {Object}
*/
bindings() {
return this.hasBindings() ? this._json.bindings : {};
},
/**
* @returns {string[]}
*/
bindingProtocols() {
return Object.keys(this.bindings());
},
/**
* @param {string} name - Name of the binding.
* @returns {boolean}
*/
hasBinding(name) {
return this.hasBindings() && !!this._json.bindings[String(name)];
},
/**
* @param {string} name - Name of the binding.
* @returns {(Object | null)}
*/
binding(name) {
return getMapValueByKey(this._json.bindings, name);
},
};
module.exports = MixinBindings;
@@ -0,0 +1,23 @@
const { getMapValueByKey } = require('../models/utils');
/**
* Implements functions to deal with the description field.
* @mixin
*/
const MixinDescription = {
/**
* @returns {boolean}
*/
hasDescription() {
return !!this._json.description;
},
/**
* @returns {(string | null)}
*/
description() {
return getMapValueByKey(this._json, 'description');
},
};
module.exports = MixinDescription;
@@ -0,0 +1,25 @@
const { getMapValueOfType } = require('../models/utils');
const ExternalDocs = require('../models/external-docs');
/**
* Implements functions to deal with the ExternalDocs object.
* @mixin
*/
const MixinExternalDocs = {
/**
* @returns {boolean}
*/
hasExternalDocs() {
return !!(this._json.externalDocs && Object.keys(this._json.externalDocs).length);
},
/**
* @returns {(ExternalDocs | null)}
*/
externalDocs() {
return getMapValueOfType(this._json, 'externalDocs', ExternalDocs);
},
};
module.exports = MixinExternalDocs;
@@ -0,0 +1,79 @@
/**
* Implements functions to deal with the SpecificationExtensions object.
* @mixin
*/
const MixinSpecificationExtensions = {
/**
* @returns {boolean}
*/
hasExtensions() {
return !!this.extensionKeys().length;
},
/**
* @returns {Object<string, any>}
*/
extensions() {
const result = {};
Object.entries(this._json).forEach(([key, value]) => {
if ((/^x-[\w\d\.\-\_]+$/).test(key)) {
result[String(key)] = value;
}
});
return result;
},
/**
* @returns {string[]}
*/
extensionKeys() {
return Object.keys(this.extensions());
},
/**
* @returns {string[]}
*/
extKeys() {
return this.extensionKeys();
},
/**
* @param {string} key - Extension key.
* @returns {boolean}
*/
hasExtension(key) {
if (!key.startsWith('x-')) {
return false;
}
return !!this._json[String(key)];
},
/**
* @param {string} key - Extension key.
* @returns {any}
*/
extension(key) {
if (!key.startsWith('x-')) {
return null;
}
return this._json[String(key)];
},
/**
* @param {string} key - Extension key.
* @returns {boolean}
*/
hasExt(key) {
return this.hasExtension(key);
},
/**
* @param {string} key - Extension key.
* @returns {any}
*/
ext(key) {
return this.extension(key);
},
};
module.exports = MixinSpecificationExtensions;
+47
View File
@@ -0,0 +1,47 @@
const Tag = require('../models/tag');
/**
* Implements functions to deal with the Tags object.
* @mixin
*/
const MixinTags = {
/**
* @returns {boolean}
*/
hasTags() {
return !!(Array.isArray(this._json.tags) && this._json.tags.length);
},
/**
* @returns {Tag[]}
*/
tags() {
return this.hasTags() ? this._json.tags.map(t => new Tag(t)) : [];
},
/**
* @returns {string[]}
*/
tagNames() {
return this.hasTags() ? this._json.tags.map(t => t.name) : [];
},
/**
* @param {string} name - Name of the tag.
* @returns {boolean}
*/
hasTag(name) {
return this.hasTags() && this._json.tags.some(t => t.name === name);
},
/**
* @param {string} name - Name of the tag.
* @returns {(Tag | null)}
*/
tag(name) {
const tg = this.hasTags() && this._json.tags.find(t => t.name === name);
return tg ? new Tag(tg) : null;
},
};
module.exports = MixinTags;
+365
View File
@@ -0,0 +1,365 @@
const { createMapOfType, getMapValueOfType, mix } = require('./utils');
const Base = require('./base');
const Info = require('./info');
const Server = require('./server');
const Channel = require('./channel');
const Components = require('./components');
const MixinExternalDocs = require('../mixins/external-docs');
const MixinTags = require('../mixins/tags');
const MixinSpecificationExtensions = require('../mixins/specification-extensions');
const {xParserSpecParsed, xParserSpecStringified, xParserCircle} = require('../constants');
const {assignNameToAnonymousMessages, assignNameToComponentMessages, assignUidToComponentSchemas, assignUidToParameterSchemas, assignIdToAnonymousSchemas, assignUidToComponentParameterSchemas} = require('../anonymousNaming');
const {traverseAsyncApiDocument} = require('../iterators');
/**
* Implements functions to deal with the AsyncAPI document.
* @class
* @alias module:@asyncapi/parser#AsyncAPIDocument
* @extends Base
* @mixes MixinTags
* @mixes MixinExternalDocs
* @mixes MixinSpecificationExtensions
* @returns {AsyncAPIDocument}
*/
class AsyncAPIDocument extends Base {
/**
* @constructor
*/
constructor(...args) {
super(...args);
if (this.ext(xParserSpecParsed) === true) {
return;
}
assignNameToComponentMessages(this);
assignNameToAnonymousMessages(this);
assignUidToComponentSchemas(this);
assignUidToComponentParameterSchemas(this);
assignUidToParameterSchemas(this);
assignIdToAnonymousSchemas(this);
// We add `x-parser-spec-parsed=true` extension to determine that the specification is parsed and validated
// and when the specification is re-passed to the AsyncAPIDocument constructor,
// there is no need to perform the same operations.
this.json()[String(xParserSpecParsed)] = true;
}
/**
* @returns {string}
*/
version() {
return this._json.asyncapi;
}
/**
* @returns {Info}
*/
info() {
return new Info(this._json.info);
}
/**
* @returns {string}
*/
id() {
return this._json.id;
}
/**
* @returns {boolean}
*/
hasServers() {
return !!this._json.servers;
}
/**
* @returns {Object<string, Server>}
*/
servers() {
return createMapOfType(this._json.servers, Server);
}
/**
* @returns {string[]}
*/
serverNames() {
if (!this._json.servers) return [];
return Object.keys(this._json.servers);
}
/**
* @param {string} name - Name of the server.
* @returns {Server}
*/
server(name) {
return getMapValueOfType(this._json.servers, name, Server);
}
/**
* @returns {boolean}
*/
hasDefaultContentType() {
return !!this._json.defaultContentType;
}
/**
* @returns {string|null}
*/
defaultContentType() {
return this._json.defaultContentType || null;
}
/**
* @returns {boolean}
*/
hasChannels() {
return !!this._json.channels;
}
/**
* @returns {Object<string, Channel>}
*/
channels() {
return createMapOfType(this._json.channels, Channel, this);
}
/**
* @returns {string[]}
*/
channelNames() {
if (!this._json.channels) return [];
return Object.keys(this._json.channels);
}
/**
* @param {string} name - Name of the channel.
* @returns {Channel}
*/
channel(name) {
return getMapValueOfType(this._json.channels, name, Channel, this);
}
/**
* @returns {boolean}
*/
hasComponents() {
return !!this._json.components;
}
/**
* @returns {Components}
*/
components() {
if (!this._json.components) return null;
return new Components(this._json.components);
}
/**
* @returns {boolean}
*/
hasMessages() {
return !!this.allMessages().size;
}
/**
* @returns {Map<string, Message>}
*/
allMessages() {
const messages = new Map();
if (this.hasChannels()) {
this.channelNames().forEach(channelName => {
const channel = this.channel(channelName);
if (channel.hasPublish()) {
channel.publish().messages().forEach(m => {
messages.set(m.uid(), m);
});
}
if (channel.hasSubscribe()) {
channel.subscribe().messages().forEach(m => {
messages.set(m.uid(), m);
});
}
});
}
if (this.hasComponents()) {
Object.values(this.components().messages()).forEach(m => {
messages.set(m.uid(), m);
});
}
return messages;
}
/**
* @returns {Map<string, Schema>}
*/
allSchemas() {
const schemas = new Map();
const allSchemasCallback = (schema) => {
if (schema.uid()) {
schemas.set(schema.uid(), schema);
}
};
traverseAsyncApiDocument(this, allSchemasCallback);
return schemas;
}
/**
* @returns {boolean}
*/
hasCircular() {
return !!this._json[String(xParserCircle)];
}
/**
* Callback used when crawling a schema.
* @callback module:@asyncapi/parser.TraverseSchemas
* @param {Schema} schema which is being crawled
* @param {String} propName if the schema is from a property get the name of such
* @param {SchemaIteratorCallbackType} callbackType is the schema a new one or is the crawler finishing one.
* @returns {boolean} should the crawler continue crawling the schema?
*/
/**
* Traverse schemas in the document and select which types of schemas to include.
* By default all schemas are iterated
* @param {TraverseSchemas} callback
* @param {SchemaTypesToIterate[]} schemaTypesToIterate
*/
traverseSchemas(callback, schemaTypesToIterate) {
traverseAsyncApiDocument(this, callback, schemaTypesToIterate);
}
/**
* Converts a valid AsyncAPI document to a JavaScript Object Notation (JSON) string.
* A stringified AsyncAPI document using this function should be parsed via the AsyncAPIDocument.parse() function - the JSON.parse() function is not compatible.
*
* @param {AsyncAPIDocument} doc A valid AsyncAPIDocument instance.
* @param {(number | string)=} space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.
* @returns {string}
*/
static stringify(doc, space) {
const rawDoc = doc.json();
const copiedDoc = { ...rawDoc };
copiedDoc[String(xParserSpecStringified)] = true;
return JSON.stringify(copiedDoc, refReplacer(), space);
}
/**
* Converts a valid stringified AsyncAPIDocument instance into an AsyncAPIDocument instance.
*
* @param {string} doc A valid stringified AsyncAPIDocument instance.
* @returns {AsyncAPIDocument}
*/
static parse(doc) {
let parsedJSON = doc;
if (typeof doc === 'string') {
parsedJSON = JSON.parse(doc);
} else if (typeof doc === 'object') {
// shall copy
parsedJSON = { ...parsedJSON };
}
// the `doc` must be an AsyncAPI parsed document
if (typeof parsedJSON !== 'object' || !parsedJSON[String(xParserSpecParsed)]) {
throw new Error('Cannot parse invalid AsyncAPI document');
}
// if the `doc` is not stringified via the `stringify` static method then immediately return a model.
if (!parsedJSON[String(xParserSpecStringified)]) {
return new AsyncAPIDocument(parsedJSON);
}
// remove `x-parser-spec-stringified` extension
delete parsedJSON[String(xParserSpecStringified)];
const objToPath = new Map();
const pathToObj = new Map();
traverseStringifiedDoc(parsedJSON, undefined, parsedJSON, objToPath, pathToObj);
return new AsyncAPIDocument(parsedJSON);
}
}
/**
* Replacer function (that transforms the result) for AsyncAPI.stringify() function.
* Handles circular references by replacing it by JSONPath notation.
*
* @private
*/
function refReplacer() {
const modelPaths = new Map();
const paths = new Map();
let init = null;
return function(field, value) {
// `this` points to parent object of given value - some object or array
const pathPart = modelPaths.get(this) + (Array.isArray(this) ? `[${field}]` : `.${ field}`);
// check if `objOrPath` has "reference"
const isComplex = value === Object(value);
if (isComplex) {
modelPaths.set(value, pathPart);
}
const savedPath = paths.get(value) || '';
if (!savedPath && isComplex) {
const valuePath = pathPart.replace(/undefined\.\.?/,'');
paths.set(value, valuePath);
}
const prefixPath = savedPath[0] === '[' ? '$' : '$.';
let val = savedPath ? `$ref:${prefixPath}${savedPath}` : value;
if (init === null) {
init = value;
} else if (val === init) {
val = '$ref:$';
}
return val;
};
}
/**
* Traverses stringified AsyncAPIDocument and replaces all JSON Pointer instance with real object reference.
*
* @private
* @param {Object} parent object
* @param {string} field of parent object
* @param {Object} root reference to the original object
* @param {Map} objToPath
* @param {Map} pathToObj
*/
function traverseStringifiedDoc(parent, field, root, objToPath, pathToObj) {
let objOrPath = parent;
let path = '$ref:$';
if (field !== undefined) {
// here can be string with `$ref` prefix or normal value
objOrPath = parent[String(field)];
const concatenatedPath = field ? `.${field}` : '';
path = objToPath.get(parent) + (Array.isArray(parent) ? `[${field}]` : concatenatedPath);
}
objToPath.set(objOrPath, path);
pathToObj.set(path, objOrPath);
const ref = pathToObj.get(objOrPath);
if (ref) {
parent[String(field)] = ref;
}
if (objOrPath === '$ref:$' || ref === '$ref:$') { // NOSONAR
parent[String(field)] = root;
}
// traverse all keys, only if object is array/object
if (objOrPath === Object(objOrPath)) {
for (const f in objOrPath) {
traverseStringifiedDoc(objOrPath, f, root, objToPath, pathToObj);
}
}
}
module.exports = mix(AsyncAPIDocument, MixinTags, MixinExternalDocs, MixinSpecificationExtensions);
+26
View File
@@ -0,0 +1,26 @@
const ParserError = require('../errors/parser-error');
/**
* Implements common functionality for all the models.
* @class
* @alias module:@asyncapi/parser#Base
* @returns {Base}
*/
class Base {
constructor(json) {
if (json === undefined || json === null) throw new ParserError(`Invalid JSON to instantiate the ${this.constructor.name} object.`);
this._json = json;
}
/**
* @param {string} [key] A key to retrieve from the JSON object.
* @returns {any}
*/
json(key) {
if (key === undefined) return this._json;
if (!this._json) return;
return this._json[String(key)];
}
}
module.exports = Base;
@@ -0,0 +1,35 @@
const { mix } = require('./utils');
const Base = require('./base');
const Schema = require('./schema');
const MixinDescription = require('../mixins/description');
const MixinSpecificationExtensions = require('../mixins/specification-extensions');
/**
* Implements functions to deal with a ChannelParameter object.
* @class
* @alias module:@asyncapi/parser#ChannelParameter
* @extends Base
* @mixes MixinDescription
* @mixes MixinSpecificationExtensions
* @returns {ChannelParameter}
*/
class ChannelParameter extends Base {
/**
* @returns {string}
*/
location() {
return this._json.location;
}
/**
* @returns {Schema}
*/
schema() {
if (!this._json.schema) return null;
return new Schema(this._json.schema);
}
}
module.exports = mix(ChannelParameter, MixinDescription, MixinSpecificationExtensions);
+102
View File
@@ -0,0 +1,102 @@
const { createMapOfType, getMapValueOfType, mix } = require('./utils');
const Base = require('./base');
const ChannelParameter = require('./channel-parameter');
const PublishOperation = require('./publish-operation');
const SubscribeOperation = require('./subscribe-operation');
const MixinDescription = require('../mixins/description');
const MixinBindings = require('../mixins/bindings');
const MixinSpecificationExtensions = require('../mixins/specification-extensions');
/**
* Implements functions to deal with a Channel object.
* @class
* @alias module:@asyncapi/parser#Channel
* @extends Base
* @mixes MixinDescription
* @mixes MixinBindings
* @mixes MixinSpecificationExtensions
* @returns {Channel}
*/
class Channel extends Base {
/**
* @returns {Object<string, ChannelParameter>}
*/
parameters() {
return createMapOfType(this._json.parameters, ChannelParameter);
}
/**
* @param {string} name - Name of the parameter.
* @returns {ChannelParameter}
*/
parameter(name) {
return getMapValueOfType(this._json.parameters, name, ChannelParameter);
}
/**
* @returns {boolean}
*/
hasParameters() {
return !!this._json.parameters;
}
/**
* @returns {boolean}
*/
hasServers() {
return !!this._json.servers;
}
/**
* @returns {String[]}
*/
servers() {
if (!this._json.servers) return [];
return this._json.servers;
}
/**
* @param {number} index - Index of the server.
* @returns {String}
*/
server(index) {
if (!this._json.servers) return null;
if (typeof index !== 'number') return null;
if (index > this._json.servers.length - 1) return null;
return this._json.servers[+index];
}
/**
* @returns {PublishOperation}
*/
publish() {
if (!this._json.publish) return null;
return new PublishOperation(this._json.publish);
}
/**
* @returns {SubscribeOperation}
*/
subscribe() {
if (!this._json.subscribe) return null;
return new SubscribeOperation(this._json.subscribe);
}
/**
* @returns {boolean}
*/
hasPublish() {
return !!this._json.publish;
}
/**
* @returns {boolean}
*/
hasSubscribe() {
return !!this._json.subscribe;
}
}
module.exports = mix(Channel, MixinDescription, MixinBindings, MixinSpecificationExtensions);
+250
View File
@@ -0,0 +1,250 @@
const { createMapOfType, getMapValueOfType, mix } = require('./utils');
const Base = require('./base');
const Channel = require('./channel');
const Message = require('./message');
const Schema = require('./schema');
const SecurityScheme = require('./security-scheme');
const Server = require('./server');
const ChannelParameter = require('./channel-parameter');
const CorrelationId = require('./correlation-id');
const OperationTrait = require('./operation-trait');
const MessageTrait = require('./message-trait');
const ServerVariable = require('./server-variable');
const MixinSpecificationExtensions = require('../mixins/specification-extensions');
/**
* Implements functions to deal with a Components object.
* @class
* @alias module:@asyncapi/parser#Components
* @extends Base
* @mixes MixinSpecificationExtensions
* @returns {Components}
*/
class Components extends Base {
/**
* @returns {Object<string, Channel>}
*/
channels() {
return createMapOfType(this._json.channels, Channel);
}
/**
* @returns {boolean}
*/
hasChannels() {
return !!this._json.channels;
}
/**
* @param {string} name - Name of the channel.
* @returns {Channel}
*/
channel(name) {
return getMapValueOfType(this._json.channels, name, Channel);
}
/**
* @returns {Object<string, Message>}
*/
messages() {
return createMapOfType(this._json.messages, Message);
}
/**
* @returns {boolean}
*/
hasMessages() {
return !!this._json.messages;
}
/**
* @param {string} name - Name of the message.
* @returns {Message}
*/
message(name) {
return getMapValueOfType(this._json.messages, name, Message);
}
/**
* @returns {Object<string, Schema>}
*/
schemas() {
return createMapOfType(this._json.schemas, Schema);
}
/**
* @returns {boolean}
*/
hasSchemas() {
return !!this._json.schemas;
}
/**
* @param {string} name - Name of the schema.
* @returns {Schema}
*/
schema(name) {
return getMapValueOfType(this._json.schemas, name, Schema);
}
/**
* @returns {Object<string, SecurityScheme>}
*/
securitySchemes() {
return createMapOfType(this._json.securitySchemes, SecurityScheme);
}
/**
* @returns {boolean}
*/
hasSecuritySchemes() {
return !!this._json.securitySchemes;
}
/**
* @param {string} name - Name of the security schema.
* @returns {SecurityScheme}
*/
securityScheme(name) {
return getMapValueOfType(this._json.securitySchemes, name, SecurityScheme);
}
/**
* @returns {Object<string, Server>}
*/
servers() {
return createMapOfType(this._json.servers, Server);
}
/**
* @returns {boolean}
*/
hasServers() {
return !!this._json.servers;
}
/**
* @param {string} name - Name of the server.
* @returns {Server}
*/
server(name) {
return getMapValueOfType(this._json.servers, name, Server);
}
/**
* @returns {Object<string, ChannelParameter>}
*/
parameters() {
return createMapOfType(this._json.parameters, ChannelParameter);
}
/**
* @returns {boolean}
*/
hasParameters() {
return !!this._json.parameters;
}
/**
* @param {string} name - Name of the channel parameter.
* @returns {ChannelParameter}
*/
parameter(name) {
return getMapValueOfType(this._json.parameters, name, ChannelParameter);
}
/**
* @returns {Object<string, CorrelationId>}
*/
correlationIds() {
return createMapOfType(this._json.correlationIds, CorrelationId);
}
/**
* @returns {boolean}
*/
hasCorrelationIds() {
return !!this._json.correlationIds;
}
/**
* @param {string} name - Name of the correlationId.
* @returns {CorrelationId}
*/
correlationId(name) {
return getMapValueOfType(this._json.correlationIds, name, CorrelationId);
}
/**
* @returns {Object<string, OperationTrait>}
*/
operationTraits() {
return createMapOfType(this._json.operationTraits, OperationTrait);
}
/**
* @returns {boolean}
*/
hasOperationTraits() {
return !!this._json.operationTraits;
}
/**
* @param {string} name - Name of the operation trait.
* @returns {OperationTrait}
*/
operationTrait(name) {
return getMapValueOfType(this._json.operationTraits, name, OperationTrait);
}
/**
* @returns {Object<string, MessageTrait>}
*/
messageTraits() {
return createMapOfType(this._json.messageTraits, MessageTrait);
}
/**
* @returns {boolean}
*/
hasMessageTraits() {
return !!this._json.messageTraits;
}
/**
* @param {string} name - Name of the message trait.
* @returns {MessageTrait}
*/
messageTrait(name) {
return getMapValueOfType(this._json.messageTraits, name, MessageTrait);
}
/**
*
* @returns {Object<string, ServerVariable>}
*/
serverVariables() {
return createMapOfType(this._json.serverVariables, ServerVariable);
}
/**
*
* @returns {boolean}
*/
hasServerVariables() {
return !!this._json.serverVariables;
}
/**
*
* @param {string} name - Name of the server variable.
* @returns {ServerVariable}
*/
serverVariable(name) {
return getMapValueOfType(this._json.serverVariables, name, ServerVariable);
}
}
module.exports = mix(Components, MixinSpecificationExtensions);
+39
View File
@@ -0,0 +1,39 @@
const { mix } = require('./utils');
const Base = require('./base');
const MixinSpecificationExtensions = require('../mixins/specification-extensions');
/**
* Implements functions to deal with the Contact object.
* @class
* @alias module:@asyncapi/parser#Contact
* @extends Base
* @mixes MixinSpecificationExtensions
* @returns {Contact}
*/
class Contact extends Base {
/**
* @returns {string}
*/
name() {
return this._json.name;
}
/**
* @returns {string}
*/
url() {
return this._json.url;
}
/**
* @returns {string}
*/
email() {
return this._json.email;
}
}
module.exports = mix(Contact, MixinSpecificationExtensions);
@@ -0,0 +1,26 @@
const { mix } = require('./utils');
const Base = require('./base');
const MixinDescription = require('../mixins/description');
const MixinSpecificationExtensions = require('../mixins/specification-extensions');
/**
* Implements functions to deal with a CorrelationId object.
* @class
* @alias module:@asyncapi/parser#CorrelationId
* @extends Base
* @mixes MixinDescription
* @mixes MixinSpecificationExtensions
* @returns {CorrelationId}
*/
class CorrelationId extends Base {
/**
* @returns {string}
*/
location() {
return this._json.location;
}
}
module.exports = mix(CorrelationId, MixinSpecificationExtensions, MixinDescription);
@@ -0,0 +1,26 @@
const { mix } = require('./utils');
const Base = require('./base');
const MixinDescription = require('../mixins/description');
const MixinSpecificationExtensions = require('../mixins/specification-extensions');
/**
* Implements functions to deal with an ExternalDocs object.
* @class
* @alias module:@asyncapi/parser#ExternalDocs
* @extends Base
* @mixes MixinDescription
* @mixes MixinSpecificationExtensions
* @returns {ExternalDocs}
*/
class ExternalDocs extends Base {
/**
* @returns {string}
*/
url() {
return this._json.url;
}
}
module.exports = mix(ExternalDocs, MixinDescription, MixinSpecificationExtensions);
+58
View File
@@ -0,0 +1,58 @@
const { mix } = require('./utils');
const Base = require('./base');
const License = require('./license');
const Contact = require('./contact');
const MixinDescription = require('../mixins/description');
const MixinSpecificationExtensions = require('../mixins/specification-extensions');
/**
* Implements functions to deal with the Info object.
* @class
* @alias module:@asyncapi/parser#Info
* @extends Base
* @mixes MixinDescription
* @mixes MixinSpecificationExtensions
* @returns {Info}
*/
class Info extends Base {
/**
* @returns {string}
*/
title() {
return this._json.title;
}
/**
* @returns {string}
*/
version() {
return this._json.version;
}
/**
* @returns {(string | undefined)}
*/
termsOfService() {
return this._json.termsOfService;
}
/**
* @returns {License}
*/
license() {
if (!this._json.license) return null;
return new License(this._json.license);
}
/**
* @returns {Contact}
*/
contact() {
if (!this._json.contact) return null;
return new Contact(this._json.contact);
}
}
module.exports = mix(Info, MixinDescription, MixinSpecificationExtensions);
+31
View File
@@ -0,0 +1,31 @@
const { mix } = require('./utils');
const Base = require('./base');
const MixinSpecificationExtensions = require('../mixins/specification-extensions');
/**
* Implements functions to deal with the License object.
* @class
* @alias module:@asyncapi/parser#License
* @extends Base
* @mixes MixinSpecificationExtensions
* @returns {License}
*/
class License extends Base {
/**
* @returns {string}
*/
name() {
return this._json.name;
}
/**
* @returns {string}
*/
url() {
return this._json.url;
}
}
module.exports = mix(License, MixinSpecificationExtensions);
@@ -0,0 +1,13 @@
const MessageTraitable = require('./message-traitable');
/**
* Implements functions to deal with a MessageTrait object.
* @class
* @alias module:@asyncapi/parser#MessageTrait
* @extends MessageTraitable
* @returns {MessageTrait}
*/
class MessageTrait extends MessageTraitable {
}
module.exports = MessageTrait;
@@ -0,0 +1,101 @@
const { getMapValueOfType, mix } = require('./utils');
const Base = require('./base');
const Schema = require('./schema');
const CorrelationId = require('./correlation-id');
const MixinDescription = require('../mixins/description');
const MixinExternalDocs = require('../mixins/external-docs');
const MixinTags = require('../mixins/tags');
const MixinBindings = require('../mixins/bindings');
const MixinSpecificationExtensions = require('../mixins/specification-extensions');
/**
* Implements functions to deal with a the common properties that Message and MessageTrait objects have.
* @class
* @alias module:@asyncapi/parser#MessageTraitable
* @extends Base
* @mixes MixinDescription
* @mixes MixinTags
* @mixes MixinExternalDocs
* @mixes MixinBindings
* @mixes MixinSpecificationExtensions
* @returns {MessageTraitable}
*/
class MessageTraitable extends Base {
/**
* @returns {Schema}
*/
headers() {
if (!this._json.headers) return null;
return new Schema(this._json.headers);
}
/**
* @param {string} name - Name of the header.
* @returns {Schema}
*/
header(name) {
if (!this._json.headers) return null;
return getMapValueOfType(this._json.headers.properties, name, Schema);
}
/**
* @returns {string}
*/
id() {
return this._json.messageId;
}
/**
* @returns {CorrelationId}
*/
correlationId() {
if (!this._json.correlationId) return null;
return new CorrelationId(this._json.correlationId);
}
/**
* @returns {string}
*/
schemaFormat() {
return this._json.schemaFormat;
}
/**
* @returns {string}
*/
contentType() {
return this._json.contentType;
}
/**
* @returns {string}
*/
name() {
return this._json.name;
}
/**
* @returns {string}
*/
title() {
return this._json.title;
}
/**
* @returns {string}
*/
summary() {
return this._json.summary;
}
/**
* @returns {any[]}
*/
examples() {
return this._json.examples;
}
}
module.exports = mix(MessageTraitable, MixinDescription, MixinTags, MixinExternalDocs, MixinBindings, MixinSpecificationExtensions);
+59
View File
@@ -0,0 +1,59 @@
const MessageTrait = require('./message-trait');
const MessageTraitable = require('./message-traitable');
const Schema = require('./schema');
/**
* Implements functions to deal with a Message object.
* @class
* @alias module:@asyncapi/parser#Message
* @extends MessageTraitable
* @returns {Message}
*/
class Message extends MessageTraitable {
/**
* @returns {string}
*/
uid() {
return this.id() || this.name() || this.ext('x-parser-message-name') || Buffer.from(JSON.stringify(this._json)).toString('base64');
}
/**
* @returns {Schema}
*/
payload() {
if (!this._json.payload) return null;
return new Schema(this._json.payload);
}
/**
* @returns {MessageTrait[]}
*/
traits() {
const traits = this._json['x-parser-original-traits'] || this._json.traits;
if (!traits) return [];
return traits.map(t => new MessageTrait(t));
}
/**
* @returns {boolean}
*/
hasTraits() {
return !!this._json['x-parser-original-traits'] || !!this._json.traits;
}
/**
* @returns {any}
*/
originalPayload() {
return this._json['x-parser-original-payload'] || this.payload();
}
/**
* @returns {string}
*/
originalSchemaFormat() {
return this._json['x-parser-original-schema-format'] || this.schemaFormat();
}
}
module.exports = Message;
@@ -0,0 +1,45 @@
const { mix } = require('./utils');
const Base = require('./base');
const MixinSpecificationExtensions = require('../mixins/specification-extensions');
/**
* Implements functions to deal with a OAuthFlow object.
* @class
* @alias module:@asyncapi/parser#OAuthFlow
* @extends Base
* @mixes MixinSpecificationExtensions
* @returns {OAuthFlow}
*/
class OAuthFlow extends Base {
/**
* @returns {string}
*/
authorizationUrl() {
return this._json.authorizationUrl;
}
/**
* @returns {string}
*/
tokenUrl() {
return this._json.tokenUrl;
}
/**
* @returns {string}
*/
refreshUrl() {
return this._json.refreshUrl;
}
/**
* @returns {Object<string, string>}
*/
scopes() {
return this._json.scopes;
}
}
module.exports = mix(OAuthFlow, MixinSpecificationExtensions);
@@ -0,0 +1,13 @@
const Base = require('./base');
/**
* Implements functions to deal with a OperationSecurityRequirement object.
* @class
* @alias module:@asyncapi/parser#OperationSecurityRequirement
* @extends Base
* @returns {OperationSecurityRequirement}
*/
class OperationSecurityRequirement extends Base {
}
module.exports = OperationSecurityRequirement;
@@ -0,0 +1,13 @@
const OperationTraitable = require('./operation-traitable');
/**
* Implements functions to deal with a OperationTrait object.
* @class
* @alias module:@asyncapi/parser#OperationTrait
* @extends OperationTraitable
* @returns {OperationTrait}
*/
class OperationTrait extends OperationTraitable {
}
module.exports = OperationTrait;
@@ -0,0 +1,39 @@
const { mix } = require('./utils');
const Base = require('./base');
const MixinDescription = require('../mixins/description');
const MixinTags = require('../mixins/tags');
const MixinExternalDocs = require('../mixins/external-docs');
const MixinBindings = require('../mixins/bindings');
const MixinSpecificationExtensions = require('../mixins/specification-extensions');
/**
* Implements functions to deal with the common properties Operation and OperationTrait object have.
* @class
* @alias module:@asyncapi/parser#OperationTraitable
* @extends Base
* @mixes MixinDescription
* @mixes MixinTags
* @mixes MixinExternalDocs
* @mixes MixinBindings
* @mixes MixinSpecificationExtensions
* @returns {OperationTraitable}
*/
class OperationTraitable extends Base {
/**
* @returns {string}
*/
id() {
return this._json.operationId;
}
/**
* @returns {string}
*/
summary() {
return this._json.summary;
}
}
module.exports = mix(OperationTraitable, MixinDescription, MixinTags, MixinExternalDocs, MixinBindings, MixinSpecificationExtensions);
+70
View File
@@ -0,0 +1,70 @@
const OperationTraitable = require('./operation-traitable');
const Message = require('./message');
const OperationTrait = require('./operation-trait');
const OperationSecurityRequirement = require('./operation-security-requirement');
/**
* Implements functions to deal with an Operation object.
* @class
* @alias module:@asyncapi/parser#Operation
* @extends OperationTraitable
* @returns {Operation}
*/
class Operation extends OperationTraitable {
/**
* @returns {boolean}
*/
hasMultipleMessages() {
if (this._json.message && this._json.message.oneOf && this._json.message.oneOf.length > 1) return true;
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
if (!this._json.message) return false;
return false;
}
/**
* @returns {OperationTrait[]}
*/
traits() {
const traits = this._json['x-parser-original-traits'] || this._json.traits;
if (!traits) return [];
return traits.map(t => new OperationTrait(t));
}
/**
* @returns {boolean}
*/
hasTraits() {
return !!this._json['x-parser-original-traits'] || !!this._json.traits;
}
/**
* @returns {Message[]}
*/
messages() {
if (!this._json.message) return [];
if (this._json.message.oneOf) return this._json.message.oneOf.map(m => new Message(m));
return [new Message(this._json.message)];
}
/**
* @returns {Message}
*/
message(index) {
if (!this._json.message) return null;
if (this._json.message.oneOf && this._json.message.oneOf.length === 1) return new Message(this._json.message.oneOf[0]);
if (!this._json.message.oneOf) return new Message(this._json.message);
if (typeof index !== 'number') return null;
if (index > this._json.message.oneOf.length - 1) return null;
return new Message(this._json.message.oneOf[+index]);
}
/**
* @returns {OperationSecurityRequirement[]}
*/
security() {
if (!this._json.security) return null;
return this._json.security.map(sec => new OperationSecurityRequirement(sec));
}
}
module.exports = Operation;
@@ -0,0 +1,33 @@
const Operation = require('./operation');
/**
* Implements functions to deal with a PublishOperation object.
* @class
* @alias module:@asyncapi/parser#PublishOperation
* @extends Operation
* @returns {PublishOperation}
*/
class PublishOperation extends Operation {
/**
* @returns {boolean}
*/
isPublish() {
return true;
}
/**
* @returns {boolean}
*/
isSubscribe() {
return false;
}
/**
* @returns {string}
*/
kind() {
return 'publish';
}
}
module.exports = PublishOperation;
+445
View File
@@ -0,0 +1,445 @@
const { createMapOfType, getMapValueOfType, mix } = require('./utils');
const Base = require('./base');
const {xParserCircle, xParserCircleProps} = require('../constants');
const MixinDescription = require('../mixins/description');
const MixinExternalDocs = require('../mixins/external-docs');
const MixinSpecificationExtensions = require('../mixins/specification-extensions');
/**
* Implements functions to deal with a Schema object.
* @class
* @alias module:@asyncapi/parser#Schema
* @extends Base
* @mixes MixinDescription
* @mixes MixinExternalDocs
* @mixes MixinSpecificationExtensions
* @returns {Schema}
*/
class Schema extends Base {
/**
* Instantiates a schema object
*
* @constructor
* @param {any} json Schema definition
* @param {Object=} options
* @param {Schema=} options.parent Parent schema definition
*/
constructor(json, options) {
super(json);
this.options = options || {};
}
/**
* @returns {string}
*/
uid() {
return this.$id() || this.ext('x-parser-schema-id');
}
/**
* @returns {string}
*/
$id() {
return this._json.$id;
}
/**
* @returns {number}
*/
multipleOf() {
return this._json.multipleOf;
}
/**
* @returns {number}
*/
maximum() {
return this._json.maximum;
}
/**
* @returns {number}
*/
exclusiveMaximum() {
return this._json.exclusiveMaximum;
}
/**
* @returns {number}
*/
minimum() {
return this._json.minimum;
}
/**
* @returns {number}
*/
exclusiveMinimum() {
return this._json.exclusiveMinimum;
}
/**
* @returns {number}
*/
maxLength() {
return this._json.maxLength;
}
/**
* @returns {number}
*/
minLength() {
return this._json.minLength;
}
/**
* @returns {string}
*/
pattern() {
return this._json.pattern;
}
/**
* @returns {number}
*/
maxItems() {
return this._json.maxItems;
}
/**
* @returns {number}
*/
minItems() {
return this._json.minItems;
}
/**
* @returns {boolean}
*/
uniqueItems() {
return !!this._json.uniqueItems;
}
/**
* @returns {number}
*/
maxProperties() {
return this._json.maxProperties;
}
/**
* @returns {number}
*/
minProperties() {
return this._json.minProperties;
}
/**
* @returns {string[]}
*/
required() {
return this._json.required;
}
/**
* @returns {any[]}
*/
enum() {
return this._json.enum;
}
/**
* @returns {string|string[]}
*/
type() {
return this._json.type;
}
/**
* @returns {Schema[]}
*/
allOf() {
if (!this._json.allOf) return null;
return this._json.allOf.map(s => new Schema(s, { parent: this }));
}
/**
* @returns {Schema[]}
*/
oneOf() {
if (!this._json.oneOf) return null;
return this._json.oneOf.map(s => new Schema(s, { parent: this }));
}
/**
* @returns {Schema[]}
*/
anyOf() {
if (!this._json.anyOf) return null;
return this._json.anyOf.map(s => new Schema(s, { parent: this }));
}
/**
* @returns {Schema}
*/
not() {
if (!this._json.not) return null;
return new Schema(this._json.not, { parent: this });
}
/**
* @returns {Schema|Schema[]}
*/
items() {
if (!this._json.items) return null;
if (Array.isArray(this._json.items)) {
return this._json.items.map(s => new Schema(s, { parent: this }));
}
return new Schema(this._json.items, { parent: this });
}
/**
* @returns {Object<string, Schema>}
*/
properties() {
return createMapOfType(this._json.properties, Schema, { parent: this });
}
/**
* @param {string} name - Name of the property.
* @returns {Schema}
*/
property(name) {
return getMapValueOfType(this._json.properties, name, Schema, { parent: this });
}
/**
* @returns {boolean|Schema}
*/
additionalProperties() {
const ap = this._json.additionalProperties;
if (ap === undefined || ap === null) return;
if (typeof ap === 'boolean') return ap;
return new Schema(ap, { parent: this });
}
/**
* @returns {Schema}
*/
additionalItems() {
const ai = this._json.additionalItems;
if (ai === undefined || ai === null) return;
return new Schema(ai, { parent: this });
}
/**
* @returns {Object<string, Schema>}
*/
patternProperties() {
return createMapOfType(this._json.patternProperties, Schema, { parent: this });
}
/**
* @returns {any}
*/
const() {
return this._json.const;
}
/**
* @returns {Schema}
*/
contains() {
if (!this._json.contains) return null;
return new Schema(this._json.contains, { parent: this });
}
/**
* @returns {Object<string, Schema|string[]>}
*/
dependencies() {
if (!this._json.dependencies) return null;
const result = {};
Object.entries(this._json.dependencies).forEach(([key, value]) => {
result[String(key)] = !Array.isArray(value) ? new Schema(value, { parent: this }) : value;
});
return result;
}
/**
* @returns {Schema}
*/
propertyNames() {
if (!this._json.propertyNames) return null;
return new Schema(this._json.propertyNames, { parent: this });
}
/**
* @returns {Schema}
*/
if() {
if (!this._json.if) return null;
return new Schema(this._json.if, { parent: this });
}
/**
* @returns {Schema}
*/
then() {
if (!this._json.then) return null;
return new Schema(this._json.then, { parent: this });
}
/**
* @returns {Schema}
*/
else() {
if (!this._json.else) return null;
return new Schema(this._json.else, { parent: this });
}
/**
* @returns {string}
*/
format() {
return this._json.format;
}
/**
* @returns {string}
*/
contentEncoding() {
return this._json.contentEncoding;
}
/**
* @returns {string}
*/
contentMediaType() {
return this._json.contentMediaType;
}
/**
* @returns {Object<string, Schema>}
*/
definitions() {
return createMapOfType(this._json.definitions, Schema, { parent: this });
}
/**
* @returns {string}
*/
title() {
return this._json.title;
}
/**
* @returns {any}
*/
default() {
return this._json.default;
}
/**
* @returns {boolean}
*/
deprecated() {
return this._json.deprecated;
}
/**
* @returns {string}
*/
discriminator() {
return this._json.discriminator;
}
/**
* @returns {boolean}
*/
readOnly() {
return !!this._json.readOnly;
}
/**
* @returns {boolean}
*/
writeOnly() {
return !!this._json.writeOnly;
}
/**
* @returns {any[]}
*/
examples() {
return this._json.examples;
}
/**
* @returns {boolean}
*/
isBooleanSchema() {
return typeof this._json === 'boolean';
}
/**
* @returns {boolean}
*/
isCircular() {
if (!!this.ext(xParserCircle)) {
return true;
}
let parent = this.options.parent;
while (parent) {
if (parent._json === this._json) return true;
parent = parent.options && parent.options.parent;
}
return false;
}
/**
* @returns {Schema}
*/
circularSchema() {
let parent = this.options.parent;
while (parent) {
if (parent._json === this._json) return parent;
parent = parent.options && parent.options.parent;
}
}
/**
* @deprecated
* @returns {boolean}
*/
hasCircularProps() {
if (Array.isArray(this.ext(xParserCircleProps))) {
return this.ext(xParserCircleProps).length > 0;
}
return Object.entries(this.properties() || {})
.map(([propertyName, property]) => {
if (property.isCircular()) return propertyName;
})
.filter(Boolean)
.length > 0;
}
/**
* @deprecated
* @returns {string[]}
*/
circularProps() {
if (Array.isArray(this.ext(xParserCircleProps))) {
return this.ext(xParserCircleProps);
}
return Object.entries(this.properties() || {})
.map(([propertyName, property]) => {
if (property.isCircular()) return propertyName;
})
.filter(Boolean);
}
}
module.exports = mix(Schema, MixinDescription, MixinExternalDocs, MixinSpecificationExtensions);
@@ -0,0 +1,69 @@
const { createMapOfType, mix } = require('./utils');
const Base = require('./base');
const OAuthFlow = require('./oauth-flow');
const MixinDescription = require('../mixins/description');
const MixinSpecificationExtensions = require('../mixins/specification-extensions');
/**
* Implements functions to deal with a SecurityScheme object.
* @class
* @alias module:@asyncapi/parser#SecurityScheme
* @extends Base
* @mixes MixinDescription
* @mixes MixinSpecificationExtensions
* @returns {SecurityScheme}
*/
class SecurityScheme extends Base {
/**
* @returns {string}
*/
type() {
return this._json.type;
}
/**
* @returns {string}
*/
name() {
return this._json.name;
}
/**
* @returns {string}
*/
in() {
return this._json.in;
}
/**
* @returns {string}
*/
scheme() {
return this._json.scheme;
}
/**
* @returns {string}
*/
bearerFormat() {
return this._json.bearerFormat;
}
/**
* @returns {string}
*/
openIdConnectUrl() {
return this._json.openIdConnectUrl;
}
/**
* @returns {Object<string, OAuthFlow>}
*/
flows() {
return createMapOfType(this._json.flows, OAuthFlow);
}
}
module.exports = mix(SecurityScheme, MixinDescription, MixinSpecificationExtensions);
@@ -0,0 +1,13 @@
const Base = require('./base');
/**
* Implements functions to deal with a ServerSecurityRequirement object.
* @class
* @alias module:@asyncapi/parser#ServerSecurityRequirement
* @extends Base
* @returns {ServerSecurityRequirement}
*/
class ServerSecurityRequirement extends Base {
}
module.exports = ServerSecurityRequirement;
@@ -0,0 +1,63 @@
const { mix } = require('./utils');
const Base = require('./base');
const MixinDescription = require('../mixins/description');
const MixinSpecificationExtensions = require('../mixins/specification-extensions');
/**
* Implements functions to deal with a ServerVariable object.
* @class
* @alias module:@asyncapi/parser#ServerVariable
* @extends Base
* @mixes MixinDescription
* @mixes MixinSpecificationExtensions
* @returns {ServerVariable}
*/
class ServerVariable extends Base {
/**
* @returns {any[]}
*/
allowedValues() {
return this._json.enum;
}
/**
* @param {string} name - Name of the variable.
* @returns {boolean}
*/
allows(name) {
if (this._json.enum === undefined) return true;
return this._json.enum.includes(name);
}
/**
* @returns {boolean}
*/
hasAllowedValues() {
return this._json.enum !== undefined;
}
/**
* @returns {string}
*/
defaultValue() {
return this._json.default;
}
/**
* @returns {boolean}
*/
hasDefaultValue() {
return this._json.default !== undefined;
}
/**
* @returns {string[]}
*/
examples() {
return this._json.examples;
}
}
module.exports = mix(ServerVariable, MixinDescription, MixinSpecificationExtensions);
+76
View File
@@ -0,0 +1,76 @@
const { createMapOfType, getMapValueOfType, mix } = require('./utils');
const Base = require('./base');
const ServerVariable = require('./server-variable');
const ServerSecurityRequirement = require('./server-security-requirement');
const MixinDescription = require('../mixins/description');
const MixinBindings = require('../mixins/bindings');
const MixinSpecificationExtensions = require('../mixins/specification-extensions');
const MixinTags = require('../mixins/tags');
/**
* Implements functions to deal with a Server object.
* @class
* @alias module:@asyncapi/parser#Server
* @extends Base
* @mixes MixinDescription
* @mixes MixinBindings
* @mixes MixinSpecificationExtensions
* @mixes MixinTags
* @returns {Server}
*/
class Server extends Base {
/**
* @returns {string}
*/
url() {
return this._json.url;
}
/**
* @returns {string}
*/
protocol() {
return this._json.protocol;
}
/**
* @returns {string}
*/
protocolVersion() {
return this._json.protocolVersion;
}
/**
* @returns {Object<string, ServerVariable>}
*/
variables() {
return createMapOfType(this._json.variables, ServerVariable);
}
/**
* @param {string} name - Name of the server variable.
* @returns {ServerVariable}
*/
variable(name) {
return getMapValueOfType(this._json.variables, name, ServerVariable);
}
/**
* @returns {boolean}
*/
hasVariables() {
return !!this._json.variables;
}
/**
* @returns {ServerSecurityRequirement[]}
*/
security() {
if (!this._json.security) return null;
return this._json.security.map(sec => new ServerSecurityRequirement(sec));
}
}
module.exports = mix(Server, MixinDescription, MixinBindings, MixinSpecificationExtensions, MixinTags);
@@ -0,0 +1,33 @@
const Operation = require('./operation');
/**
* Implements functions to deal with a SubscribeOperation object.
* @class
* @alias module:@asyncapi/parser#SubscribeOperation
* @extends Operation
* @returns {SubscribeOperation}
*/
class SubscribeOperation extends Operation {
/**
* @returns {boolean}
*/
isPublish() {
return false;
}
/**
* @returns {boolean}
*/
isSubscribe() {
return true;
}
/**
* @returns {string}
*/
kind() {
return 'subscribe';
}
}
module.exports = SubscribeOperation;
+28
View File
@@ -0,0 +1,28 @@
const { mix } = require('./utils');
const Base = require('./base');
const MixinDescription = require('../mixins/description');
const MixinExternalDocs = require('../mixins/external-docs');
const MixinSpecificationExtensions = require('../mixins/specification-extensions');
/**
* Implements functions to deal with a Tag object.
* @class
* @alias module:@asyncapi/parser#Tag
* @extends Base
* @mixes MixinDescription
* @mixes MixinExternalDocs
* @mixes MixinSpecificationExtensions
* @returns {Tag}
*/
class Tag extends Base {
/**
* @returns {string}
*/
name() {
return this._json.name;
}
}
module.exports = mix(Tag, MixinDescription, MixinExternalDocs, MixinSpecificationExtensions);
+76
View File
@@ -0,0 +1,76 @@
const utils = module.exports;
const getMapValue = (obj, key, Type, options) => {
if (typeof key !== 'string' || !obj) return null;
const v = obj[String(key)];
if (v === undefined) return null;
return Type ? new Type(v, options) : v;
};
/**
* Creates map of given type from object.
* @private
* @param {Object} obj
* @param {Any} Type
* @param {Object} options
*/
utils.createMapOfType = (obj, Type, options) => {
const result = {};
if (!obj) return result;
Object.entries(obj).forEach(([key, value]) => {
result[String(key)] = new Type(value, options);
});
return result;
};
/**
* Creates given type from value retrieved from object by key.
* @private
* @param {Object} obj
* @param {string} key
* @param {Any} Type
* @param {Object} options
*/
utils.getMapValueOfType = (obj, key, Type, options) => {
return getMapValue(obj, key, Type, options);
};
/**
* Retrieves value from object by key.
* @private
* @param {Object} obj
* @param {string} key
*/
utils.getMapValueByKey = (obj, key) => {
return getMapValue(obj, key);
};
/**
* Extends a given model with additional methods related to frequently recurring models.
* @function mix
* @private
* @param {Object} model model to extend
* @param {Array<Object>} mixins array with mixins to extend the model with
*/
utils.mix = (model, ...mixins) => {
let duplicatedMethods = false;
function checkDuplication(mixin) {
// check duplication of model in mixins array
if (model === mixin) return true;
// check duplication of model's methods
duplicatedMethods = Object.keys(mixin).some(mixinMethod => model.prototype.hasOwnProperty(mixinMethod));
return duplicatedMethods;
}
if (mixins.some(checkDuplication)) {
if (duplicatedMethods) {
throw new Error(`invalid mix function: model ${model.name} has at least one method that it is trying to replace by mixin`);
} else {
throw new Error(`invalid mix function: cannot use the model ${model.name} as a mixin`);
}
}
mixins.forEach(mixin => Object.assign(model.prototype, mixin));
return model;
};
+337
View File
@@ -0,0 +1,337 @@
const path = require('path');
const fetch = require('node-fetch');
const Ajv = require('ajv');
const asyncapi = require('@asyncapi/specs');
const $RefParser = require('@apidevtools/json-schema-ref-parser');
const mergePatch = require('tiny-merge-patch').apply;
const ParserError = require('./errors/parser-error');
const { validateChannels, validateTags, validateServerVariables, validateOperationId, validateServerSecurity, validateMessageId } = require('./customValidators.js');
const {
toJS,
findRefs,
getLocationOf,
improveAjvErrors,
getDefaultSchemaFormat,
getBaseUrl,
} = require('./utils');
const AsyncAPIDocument = require('./models/asyncapi');
const OPERATIONS = ['publish', 'subscribe'];
//the only security types that can have a non empty array in the server security item
const SPECIAL_SECURITY_TYPES = ['oauth2', 'openIdConnect'];
const PARSERS = {};
const xParserCircle = 'x-parser-circular';
const xParserMessageParsed = 'x-parser-message-parsed';
const ajv = new Ajv({
jsonPointers: true,
allErrors: true,
schemaId: 'auto',
logger: false,
validateSchema: true,
});
ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json'));
/**
* @module @asyncapi/parser
*/
module.exports = {
parse,
parseFromUrl,
registerSchemaParser,
ParserError,
AsyncAPIDocument,
};
/**
* The complete list of parse configuration options used to parse the given data.
* @typedef {Object} ParserOptions
* @property {String=} path - Path to the AsyncAPI document. It will be used to resolve relative references. Defaults to current working dir.
* @property {Object=} parse - Options object to pass to {@link https://apitools.dev/json-schema-ref-parser/docs/options.html|json-schema-ref-parser}.
* @property {Object=} resolve - Options object to pass to {@link https://apitools.dev/json-schema-ref-parser/docs/options.html|json-schema-ref-parser}.
* @property {Boolean=} applyTraits - Whether to resolve and apply traits or not. Defaults to true.
*/
/**
* Parses and validate an AsyncAPI document from YAML or JSON.
*
* @param {(String | Object)} asyncapiYAMLorJSON An AsyncAPI document in JSON or YAML format.
* @param {ParserOptions=} options Configuration options object {@link #asyncapiparserparseroptions--object|ParserOptions}
* @returns {Promise<AsyncAPIDocument>} The parsed AsyncAPI document.
*/
async function parse(asyncapiYAMLorJSON, options = {}) {
let parsedJSON;
let initialFormat;
if (typeof window !== 'undefined' && !options.hasOwnProperty('path')) {
options.path = getBaseUrl(window.location.href);
} else {
options.path = options.path || `${process.cwd()}${path.sep}`;
}
try {
({ initialFormat, parsedJSON } = toJS(asyncapiYAMLorJSON));
if (typeof parsedJSON !== 'object') {
throw new ParserError({
type: 'impossible-to-convert-to-json',
title: 'Could not convert AsyncAPI to JSON.',
detail: 'Most probably the AsyncAPI document contains invalid YAML or YAML features not supported in JSON.'
});
}
if (!parsedJSON.asyncapi) {
throw new ParserError({
type: 'missing-asyncapi-field',
title: 'The `asyncapi` field is missing.',
parsedJSON,
});
}
if (parsedJSON.asyncapi.startsWith('1.') || !asyncapi[parsedJSON.asyncapi]) {
throw new ParserError({
type: 'unsupported-version',
title: `Version ${parsedJSON.asyncapi} is not supported.`,
detail: 'Please use latest version of the specification.',
parsedJSON,
validationErrors: [getLocationOf('/asyncapi', asyncapiYAMLorJSON, initialFormat)],
});
}
if (options.applyTraits === undefined) options.applyTraits = true;
const refParser = new $RefParser;
//because of Ajv lacks support for circular refs, parser should not resolve them before Ajv validation and first needs to ignore them and leave circular $refs to successfully validate the document
//this is done pair to advice from Ajv creator https://github.com/ajv-validator/ajv/issues/1122#issuecomment-559378449
//later we perform full dereference of circular refs if they occure
await dereference(refParser, parsedJSON, initialFormat, asyncapiYAMLorJSON, { ...options, dereference: { circular: 'ignore' } });
const validate = getValidator(parsedJSON.asyncapi);
const valid = validate(parsedJSON);
const errors = validate.errors && [...validate.errors];
if (!valid) throw new ParserError({
type: 'validation-errors',
title: 'There were errors validating the AsyncAPI document.',
parsedJSON,
validationErrors: improveAjvErrors(errors, asyncapiYAMLorJSON, initialFormat),
});
await customDocumentOperations(parsedJSON, asyncapiYAMLorJSON, initialFormat, options);
if (refParser.$refs.circular) await handleCircularRefs(refParser, parsedJSON, initialFormat, asyncapiYAMLorJSON, options);
} catch (e) {
if (e instanceof ParserError) throw e;
throw new ParserError({
type: 'unexpected-error',
title: e.message,
parsedJSON,
});
}
return new AsyncAPIDocument(parsedJSON);
}
/**
* Fetches an AsyncAPI document from the given URL and passes its content to the `parse` method.
*
* @param {String} url URL where the AsyncAPI document is located.
* @param {Object=} [fetchOptions] Configuration to pass to the {@link https://developer.mozilla.org/en-US/docs/Web/API/Request|fetch} call.
* @param {ParserOptions=} [options] Configuration to pass to the {@link #asyncapiparserparseroptions--object|ParserOptions} method.
* @returns {Promise<AsyncAPIDocument>} The parsed AsyncAPI document.
*/
function parseFromUrl(url, fetchOptions, options = {}) {
//Why not just addinga default to the arguments list?
//All function parameters with default values should be declared after the function parameters without default values. Otherwise, it makes it impossible for callers to take advantage of defaults; they must re-specify the defaulted values or pass undefined in order to "get to" the non-default parameters.
//To not break the API by changing argument position and to silet the linter it is just better to move adding
if (!fetchOptions) fetchOptions = {};
if (!options.hasOwnProperty('path')) {
options = { ...options, path: getBaseUrl(url) };
}
return new Promise((resolve, reject) => {
fetch(url, fetchOptions)
.then(res => res.text())
.then(doc => parse(doc, options))
.then(result => resolve(result))
.catch(e => {
if (e instanceof ParserError) return reject(e);
return reject(new ParserError({
type: 'fetch-url-error',
title: e.message,
}));
});
});
}
async function dereference(refParser, parsedJSON, initialFormat, asyncapiYAMLorJSON, options) {
try {
return await refParser.dereference(options.path, parsedJSON, {
continueOnError: true,
parse: options.parse,
resolve: options.resolve,
dereference: options.dereference,
});
} catch (err) {
throw new ParserError({
type: 'dereference-error',
title: err.errors[0].message,
parsedJSON,
refs: findRefs(err.errors, initialFormat, asyncapiYAMLorJSON),
});
}
}
/*
* In case of circular refs, this function dereferences the spec again to dereference circular dependencies
* Special property is added to the document that indicates it contains circular refs
*/
async function handleCircularRefs(refParser, parsedJSON, initialFormat, asyncapiYAMLorJSON, options) {
await dereference(refParser, parsedJSON, initialFormat, asyncapiYAMLorJSON, { ...options, dereference: { circular: true } });
//mark entire document as containing circular references
parsedJSON[String(xParserCircle)] = true;
}
/**
* Creates (or reuses) a function that validates an AsyncAPI document based on the passed AsyncAPI version.
*
* @private
* @param {Object} version AsyncAPI version.
* @returns {Function} Function that validates an AsyncAPI document based on the passed AsyncAPI version.
*/
function getValidator(version) {
let validate = ajv.getSchema(version);
if (!validate) {
const asyncapiSchema = asyncapi[String(version)];
// Remove the meta schemas because it is already present within Ajv, and it's not possible to add duplicate schemas.
delete asyncapiSchema.definitions['http://json-schema.org/draft-07/schema'];
delete asyncapiSchema.definitions['http://json-schema.org/draft-04/schema'];
ajv.addSchema(asyncapiSchema, version);
validate = ajv.getSchema(version);
}
return validate;
}
async function customDocumentOperations(parsedJSON, asyncapiYAMLorJSON, initialFormat, options) {
validateServerVariables(parsedJSON, asyncapiYAMLorJSON, initialFormat);
validateServerSecurity(parsedJSON, asyncapiYAMLorJSON, initialFormat, SPECIAL_SECURITY_TYPES);
if (!parsedJSON.channels) return;
validateTags(parsedJSON, asyncapiYAMLorJSON, initialFormat);
validateChannels(parsedJSON, asyncapiYAMLorJSON, initialFormat);
validateOperationId(parsedJSON, asyncapiYAMLorJSON, initialFormat, OPERATIONS);
validateMessageId(parsedJSON, asyncapiYAMLorJSON, initialFormat, OPERATIONS);
await customComponentsMsgOperations(parsedJSON, asyncapiYAMLorJSON, initialFormat, options);
await customChannelsOperations(parsedJSON, asyncapiYAMLorJSON, initialFormat, options);
}
async function validateAndConvertMessage(msg, originalAsyncAPIDocument, fileFormat, parsedAsyncAPIDocument, pathToPayload) {
//check if the message has been parsed before
if (xParserMessageParsed in msg && msg[String(xParserMessageParsed)] === true) return;
const defaultSchemaFormat = getDefaultSchemaFormat(parsedAsyncAPIDocument.asyncapi);
const schemaFormat = msg.schemaFormat || defaultSchemaFormat;
await PARSERS[String(schemaFormat)]({
schemaFormat,
message: msg,
defaultSchemaFormat,
originalAsyncAPIDocument,
parsedAsyncAPIDocument,
fileFormat,
pathToPayload
});
msg.schemaFormat = defaultSchemaFormat;
msg[String(xParserMessageParsed)] = true;
}
/**
* Registers a new schema parser. Schema parsers are in charge of parsing and transforming payloads to AsyncAPI Schema format.
*
* @param {Object} parserModule The schema parser module containing parse() and getMimeTypes() functions.
*/
function registerSchemaParser(parserModule) {
if (typeof parserModule !== 'object'
|| typeof parserModule.parse !== 'function'
|| typeof parserModule.getMimeTypes !== 'function')
throw new ParserError({
type: 'impossible-to-register-parser',
title: 'parserModule must have parse() and getMimeTypes() functions.'
});
parserModule.getMimeTypes().forEach((schemaFormat) => {
PARSERS[String(schemaFormat)] = parserModule.parse;
});
}
function applyTraits(js) {
if (Array.isArray(js.traits)) {
for (const trait of js.traits) {
for (const key in trait) {
js[String(key)] = mergePatch(js[String(key)], trait[String(key)]);
}
}
js['x-parser-original-traits'] = js.traits;
delete js.traits;
}
}
/**
* Triggers additional operations on the AsyncAPI channels like traits application or message validation and conversion
*
* @private
*
* @param {Object} parsedJSON parsed AsyncAPI document
* @param {String} asyncapiYAMLorJSON AsyncAPI document in string
* @param {String} initialFormat information of the document was originally JSON or YAML
* @param {ParserOptions} options Configuration options. {@link ParserOptions}
*/
async function customChannelsOperations(parsedJSON, asyncapiYAMLorJSON, initialFormat, options) {
const promisesArray = [];
Object.entries(parsedJSON.channels).forEach(([channelName, channel]) => {
promisesArray.push(...OPERATIONS.map(async (opName) => {
const op = channel[String(opName)];
if (!op) return;
const messages = op.message ? (op.message.oneOf || [op.message]) : [];
if (options.applyTraits) {
applyTraits(op);
messages.forEach(m => applyTraits(m));
}
const pathToPayload = `/channels/${channelName}/${opName}/message/payload`;
for (const m of messages) {
await validateAndConvertMessage(m, asyncapiYAMLorJSON, initialFormat, parsedJSON, pathToPayload);
}
}));
});
await Promise.all(promisesArray);
}
/**
* Triggers additional operations on the AsyncAPI messages located in the components section of the document. It triggers operations like traits application, validation and conversion
*
* @private
*
* @param {Object} parsedJSON parsed AsyncAPI document
* @param {String} asyncapiYAMLorJSON AsyncAPI document in string
* @param {String} initialFormat information of the document was originally JSON or YAML
* @param {ParserOptions} options Configuration options. {@link ParserOptions}
*/
async function customComponentsMsgOperations(parsedJSON, asyncapiYAMLorJSON, initialFormat, options) {
if (!parsedJSON.components || !parsedJSON.components.messages) return;
const promisesArray = [];
Object.entries(parsedJSON.components.messages).forEach(([messageName, message]) => {
if (options.applyTraits) {
applyTraits(message);
}
const pathToPayload = `/components/messages/${messageName}/payload`;
promisesArray.push(validateAndConvertMessage(message, asyncapiYAMLorJSON, initialFormat, parsedJSON, pathToPayload));
});
await Promise.all(promisesArray);
}
+339
View File
@@ -0,0 +1,339 @@
const YAML = require('js-yaml');
const { yamlAST, loc } = require('@fmvilas/pseudo-yaml-ast');
const jsonAST = require('json-to-ast');
const jsonParseBetterErrors = require('../lib/json-parse');
const ParserError = require('./errors/parser-error');
const jsonPointerToArray = jsonPointer => (jsonPointer || '/').split('/').splice(1);
const utils = module.exports;
const getAST = (asyncapiYAMLorJSON, initialFormat) => {
if (initialFormat === 'yaml') {
return yamlAST(asyncapiYAMLorJSON);
} else if (initialFormat === 'json') {
return jsonAST(asyncapiYAMLorJSON);
}
};
const findNode = (obj, location) => {
for (const key of location) {
obj = obj ? obj[utils.untilde(key)] : null;
}
return obj;
};
const findNodeInAST = (ast, location) => {
let obj = ast;
for (const key of location) {
if (!Array.isArray(obj.children)) return;
let childArray;
const child = obj.children.find(c => {
if (!c) return;
if (c.type === 'Object') return childArray = c.children.find(a => a.key.value === utils.untilde(key));
return c.type === 'Property' && c.key && c.key.value === utils.untilde(key);
});
if (!child) return;
obj = childArray ? childArray.value : child.value;
}
return obj;
};
const findLocationOf = (keys, ast, initialFormat) => {
if (initialFormat === 'js') return { jsonPointer: `/${keys.join('/')}` };
let node;
if (initialFormat === 'yaml') {
node = findNode(ast, keys);
} else if (initialFormat === 'json') {
node = findNodeInAST(ast, keys);
}
if (!node) return { jsonPointer: `/${keys.join('/')}` };
let info;
if (initialFormat === 'yaml') {
// disable eslint because loc is a Symbol
// eslint-disable-next-line security/detect-object-injection
info = node[loc];
} else if (initialFormat === 'json') {
info = node.loc;
}
if (!info) return { jsonPointer: `/${keys.join('/')}` };
return {
jsonPointer: `/${keys.join('/')}`,
startLine: info.start.line,
startColumn: info.start.column + 1,
startOffset: info.start.offset,
endLine: info.end ? info.end.line : undefined,
endColumn: info.end ? info.end.column + 1 : undefined,
endOffset: info.end ? info.end.offset : undefined,
};
};
utils.tilde = (str) => {
return str.replace(/[~\/]{1}/g, (m) => {
switch (m) {
case '/': return '~1';
case '~': return '~0';
}
return m;
});
};
utils.untilde = (str) => {
if (!str.includes('~')) return str;
return str.replace(/~[01]/g, (m) => {
switch (m) {
case '~1': return '/';
case '~0': return '~';
}
return m;
});
};
utils.toJS = (asyncapiYAMLorJSON) => {
if (!asyncapiYAMLorJSON) {
throw new ParserError({
type: 'null-or-falsey-document',
title: 'Document can\'t be null or falsey.',
});
}
if (asyncapiYAMLorJSON.constructor && asyncapiYAMLorJSON.constructor.name === 'Object') {
return {
initialFormat: 'js',
parsedJSON: asyncapiYAMLorJSON,
};
}
if (typeof asyncapiYAMLorJSON !== 'string') {
throw new ParserError({
type: 'invalid-document-type',
title: 'The AsyncAPI document has to be either a string or a JS object.',
});
}
if (asyncapiYAMLorJSON.trimLeft().startsWith('{')) {
try {
return {
initialFormat: 'json',
parsedJSON: jsonParseBetterErrors(asyncapiYAMLorJSON),
};
} catch (e) {
throw new ParserError({
type: 'invalid-json',
title: 'The provided JSON is not valid.',
detail: e.message,
location: {
startOffset: e.offset,
startLine: e.startLine,
startColumn: e.startColumn,
},
});
}
} else {
try {
return {
initialFormat: 'yaml',
parsedJSON: YAML.safeLoad(asyncapiYAMLorJSON),
};
} catch (err) {
throw new ParserError({
type: 'invalid-yaml',
title: 'The provided YAML is not valid.',
detail: err.message,
location: {
startOffset: err.mark.position,
startLine: err.mark.line + 1,
startColumn: err.mark.column + 1,
},
});
}
}
};
utils.findRefs = (errors, initialFormat, asyncapiYAMLorJSON) => {
let refs = [];
errors.map(({ path }) => refs.push({ location: [...path.map(utils.tilde), '$ref'] }));
if (initialFormat === 'js') {
return refs.map(ref => ({ jsonPointer: `/${ref.location.join('/')}` }));
}
if (initialFormat === 'yaml') {
const pseudoAST = yamlAST(asyncapiYAMLorJSON);
refs = refs.map(ref => findLocationOf(ref.location, pseudoAST, initialFormat));
} else if (initialFormat === 'json') {
const ast = jsonAST(asyncapiYAMLorJSON);
refs = refs.map(ref => findLocationOf(ref.location, ast, initialFormat));
}
return refs;
};
utils.getLocationOf = (jsonPointer, asyncapiYAMLorJSON, initialFormat) => {
const ast = getAST(asyncapiYAMLorJSON, initialFormat);
if (!ast) return { jsonPointer };
return findLocationOf(jsonPointerToArray(jsonPointer), ast, initialFormat);
};
utils.improveAjvErrors = (errors, asyncapiYAMLorJSON, initialFormat) => {
const ast = getAST(asyncapiYAMLorJSON, initialFormat);
return errors.map(error => {
const defaultLocation = { jsonPointer: error.dataPath || '/' };
const additionalProperty = error.params.additionalProperty;
const jsonPointer = additionalProperty ? `${error.dataPath}/${additionalProperty}`: error.dataPath;
return {
title: `${error.dataPath || '/'} ${error.message}`,
location: ast ? findLocationOf(jsonPointerToArray(jsonPointer), ast, initialFormat) : defaultLocation,
};
});
};
/**
* It parses the string and returns an array with all values that are between curly braces, including braces
* @function parseUrlVariables
* @private
*/
utils.parseUrlVariables = str => {
if (typeof str !== 'string') return;
return str.match(/{(.+?)}/g);
};
/**
* It parses the string and returns url parameters as string
* @function parseUrlQueryParameters
* @private
*/
utils.parseUrlQueryParameters = str => {
if (typeof str !== 'string') return;
return str.match(/\?((.*=.*)(&?))/g);
};
/**
* Returns base URL parsed from location of AsyncAPI document
*
* @function getBaseUrl
* @private
* @param {String} url URL of AsyncAPI document
*/
utils.getBaseUrl = url => {
url = typeof url !== 'string' ? String(url) : url;
//URL validation is not performed because 'node-fetch' performs its own
//validation at fetch time, so no repetition of this task is made.
//Only ensuring that 'url' has type of 'string' and letting 'node-fetch' deal
//with the rest.
return url.substring(0, url.lastIndexOf('/') + 1);
};
/**
* Returns an array of not existing properties in provided object with names specified in provided array
* @function getMissingProps
* @private
*/
utils.getMissingProps = (arr, obj) => {
arr = arr.map(val => val.replace(/[{}]/g, ''));
if (!obj) return arr;
return arr.filter(val => {
return !obj.hasOwnProperty(val);
});
};
/**
* Returns array of errors messages compatible with validationErrors parameter from ParserError
*
* @function groupValidationErrors
* @private
* @param {String} root name of the root element in the AsyncAPI document, for example channels
* @param {String} errorMessage the text of the custom error message that will follow the path that points the error
* @param {Map} errorElements map of error elements cause the validation error might happen in many places in the document.
* The key should have a path information where the error was found, the value holds information about error element but it is not mandatory
* @param {String} asyncapiYAMLorJSON AsyncAPI document in string
* @param {String} initialFormat information of the document was oryginally JSON or YAML
* @returns {Array<Object>} Object has always 2 keys, title and location. Title is a combination of errorElement key + errorMessage + errorElement value.
* Location is the object with information about location of the issue in the file and json Pointer
*/
utils.groupValidationErrors = (root, errorMessage, errorElements, asyncapiYAMLorJSON, initialFormat) => {
const errors = [];
errorElements.forEach((val, key) => {
if (typeof val === 'string') val = utils.untilde(val);
const jsonPointer = root ? `/${root}/${key}` : `/${key}`;
errors.push({
title: val ? `${ utils.untilde(key) } ${errorMessage}: ${val}` : `${ utils.untilde(key) } ${errorMessage}`,
location: utils.getLocationOf(jsonPointer, asyncapiYAMLorJSON, initialFormat)
});
});
return errors;
};
/**
* extend map with channel params missing corresponding param object
*
* @function setNotProvidedParams
* @private
* @param {Array<String>} variables array of all identified URL variables in a channel name
* @param {Object} val the channel object for which to identify the missing parameters
* @param {String} key the channel name.
* @param {Array<Object>} notProvidedChannelParams concatinated list of missing parameters for all channels
* @param {Map} notProvidedParams result map of all missing parameters extended by this function
*/
utils.setNotProvidedParams = (variables, val, key, notProvidedChannelParams, notProvidedParams) => {
const missingChannelParams = utils.getMissingProps(variables, val.parameters);
if (missingChannelParams.length) {
notProvidedParams.set(utils.tilde(key),
notProvidedChannelParams
? notProvidedChannelParams.concat(missingChannelParams)
: missingChannelParams);
}
};
/**
* Returns an array of server names listed in a channel's servers list that are not declared in the top-level servers object.
*
* @param {Map} parsedJSON the parsed AsyncAPI document, with potentially a top-level map of servers (keys are server names)
* @param {Object} channel the channel object for which to validate the servers list (array elements are server names)
* @private
*/
utils.getUnknownServers = (parsedJSON, channel) => {
// servers list on channel
if (!channel) return []; // no channel: no unknown servers
const channelServers = channel.servers;
if (!channelServers || channelServers.length === 0) return []; // no servers listed on channel: no unknown servers
// top-level servers map
const servers = parsedJSON.servers;
if (!servers) return channelServers; // servers list on channel but no top-level servers: all servers are unknown
const serversMap = new Map(Object.entries(servers));
// retain only servers listed on channel that are not defined in the top-level servers map
return channelServers.filter(serverName => {
return !serversMap.has(serverName);
});
};
/**
* returns default schema format for a given asyncapi version
*
* @function getDefaultSchemaFormat
* @private
* @param {String} asyncapiVersion AsyncAPI spec version.
*/
utils.getDefaultSchemaFormat = (asyncapiVersion) => {
return `application/vnd.aai.asyncapi;version=${asyncapiVersion}`;
};