Files
nats-python/openapi_templete/node_modules/@asyncapi/parser/lib/customValidators.js
T
2026-06-01 13:17:37 +02:00

683 lines
22 KiB
JavaScript

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,
};