Befor generating
This commit is contained in:
+128
@@ -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
|
||||
};
|
||||
+116
@@ -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
@@ -0,0 +1 @@
|
||||
window.AsyncAPIParser = require('./index');
|
||||
+15
@@ -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
@@ -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,
|
||||
};
|
||||
+64
@@ -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
@@ -0,0 +1,6 @@
|
||||
const parser = require('./parser');
|
||||
const defaultAsyncAPISchemaParser = require('./asyncapiSchemaFormatParser');
|
||||
|
||||
parser.registerSchemaParser(defaultAsyncAPISchemaParser);
|
||||
|
||||
module.exports = parser;
|
||||
+325
@@ -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
@@ -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
@@ -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;
|
||||
+23
@@ -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;
|
||||
+25
@@ -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;
|
||||
Generated
Vendored
+79
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
+35
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
+26
@@ -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);
|
||||
+26
@@ -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
@@ -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
@@ -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);
|
||||
+13
@@ -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;
|
||||
+101
@@ -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
@@ -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;
|
||||
+45
@@ -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);
|
||||
Generated
Vendored
+13
@@ -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;
|
||||
+13
@@ -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;
|
||||
+39
@@ -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
@@ -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;
|
||||
+33
@@ -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
@@ -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);
|
||||
+69
@@ -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);
|
||||
Generated
Vendored
+13
@@ -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;
|
||||
+63
@@ -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
@@ -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);
|
||||
+33
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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}`;
|
||||
};
|
||||
Reference in New Issue
Block a user