Skip to content

Commit

Permalink
feat: add validation of server variable example (#221)
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielChuDC authored Feb 16, 2021
1 parent f9f4a9e commit fcbc939
Show file tree
Hide file tree
Showing 5 changed files with 684 additions and 325 deletions.
3 changes: 2 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ insert_final_newline = true

[*.js]
indent_size = 2
indent_style = space
indent_style = space
quote_type = single
2 changes: 1 addition & 1 deletion dist/bundle.js

Large diffs are not rendered by default.

200 changes: 160 additions & 40 deletions lib/customValidators.js
Original file line number Diff line number Diff line change
@@ -1,58 +1,128 @@
const ParserError = require('./errors/parser-error');
const { parseUrlVariables, getMissingProps, groupValidationErrors, tilde, parseUrlQueryParameters, setNotProvidedParams } = require('./utils');
const {
parseUrlVariables,
getMissingProps,
groupValidationErrors,
tilde,
parseUrlQueryParameters,
setNotProvidedParams
} = require('./utils');
const validationError = 'validation-errors';

/**
* Validates if variables provided in the url have corresponding variable object defined
* 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 oryginally JSON or YAML
* @returns {Boolean} true in case the document is valid, otherwise throws ParserError
*/
function validateServerVariables(parsedJSON, asyncapiYAMLorJSON, initialFormat) {
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((val, key) => {
const variables = parseUrlVariables(val.url);
const notProvidedServerVars = notProvidedVariables.get(tilde(key));
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, val.variables);
if (!missingServerVariables.length) return;
const missingServerVariables = getMissingProps(variables, variablesObj);
if (missingServerVariables.length) {
notProvidedVariables.set(
tilde(srvrName),
notProvidedServerVars
? notProvidedServerVars.concat(missingServerVariables)
: missingServerVariables
);
}

notProvidedVariables.set(tilde(key),
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)
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 oryginally JSON or YAML
* @returns {Boolean} true in case the document is valid, otherwise throws ParserError
*/
function validateOperationId(parsedJSON, asyncapiYAMLorJSON, initialFormat, operations) {
function validateOperationId(
parsedJSON,
asyncapiYAMLorJSON,
initialFormat,
operations
) {
const chnls = parsedJSON.channels;
if (!chnls) return true;
const chnlsMap = new Map(Object.entries(chnls));
Expand All @@ -66,15 +136,18 @@ function validateOperationId(parsedJSON, asyncapiYAMLorJSON, initialFormat, oper
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]);
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
//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 => {
operations.forEach((opName) => {
const op = chnlObj[String(opName)];
if (op) addDuplicateToMap(op, chnlName, opName);
});
Expand All @@ -85,7 +158,13 @@ function validateOperationId(parsedJSON, asyncapiYAMLorJSON, initialFormat, oper
type: validationError,
title: 'operationId must be unique across all the operations.',
parsedJSON,
validationErrors: groupValidationErrors('channels', 'is a duplicate of', duplicatedOperations, asyncapiYAMLorJSON, initialFormat)
validationErrors: groupValidationErrors(
'channels',
'is a duplicate of',
duplicatedOperations,
asyncapiYAMLorJSON,
initialFormat
),
});
}

Expand All @@ -94,15 +173,20 @@ function validateOperationId(parsedJSON, asyncapiYAMLorJSON, initialFormat, oper

/**
* 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 oryginally 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 ParserError
*/
function validateServerSecurity(parsedJSON, asyncapiYAMLorJSON, initialFormat, specialSecTypes) {
function validateServerSecurity(
parsedJSON,
asyncapiYAMLorJSON,
initialFormat,
specialSecTypes
) {
const srvs = parsedJSON.servers;
if (!srvs) return true;

Expand All @@ -119,8 +203,8 @@ function validateServerSecurity(parsedJSON, asyncapiYAMLorJSON, initialFormat, s
if (!serverSecInfo) return true;

//server security info is an array of many possible values
serverSecInfo.forEach(secObj => {
Object.keys(secObj).forEach(secName => {
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}`;
Expand All @@ -129,26 +213,41 @@ function validateServerSecurity(parsedJSON, asyncapiYAMLorJSON, initialFormat, s

//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 (!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.',
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)
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.',
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)
validationErrors: groupValidationErrors(
root,
'security info must have an empty array because its corresponding security schema type is',
invalidSecurityValues,
asyncapiYAMLorJSON,
initialFormat
),
});
}

Expand All @@ -164,7 +263,9 @@ function validateServerSecurity(parsedJSON, asyncapiYAMLorJSON, initialFormat, s
*/
function findSecuritySchema(securityName, components) {
const secSchemes = components && components.securitySchemes;
const secSchemesMap = secSchemes ? new Map(Object.entries(secSchemes)) : new Map();
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
Expand Down Expand Up @@ -198,7 +299,7 @@ function isSrvrSecProperArray(schemaType, specialSecTypes, secObj, secName) {

/**
* Validates if parameters specified in the channel have corresponding parameters object defined and if name does not contain url parameters
*
*
* @private
* @param {Object} parsedJSON parsed AsyncAPI document
* @param {String} asyncapiYAMLorJSON AsyncAPI document in string
Expand All @@ -217,31 +318,50 @@ function validateChannels(parsedJSON, asyncapiYAMLorJSON, initialFormat) {
const variables = parseUrlVariables(key);
const notProvidedChannelParams = notProvidedParams.get(tilde(key));
const queryParameters = parseUrlQueryParameters(key);

//channel variable validation: fill return obeject with missing parameters
if (variables) {
setNotProvidedParams(variables, val, key, notProvidedChannelParams, notProvidedParams);
setNotProvidedParams(
variables,
val,
key,
notProvidedChannelParams,
notProvidedParams
);
}

//channel name validation: fill return object with channels containing query parameters
if (queryParameters) {
invalidChannelName.set(tilde(key),
queryParameters);
invalidChannelName.set(tilde(key), queryParameters);
}
});

//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 allValidationErrors = parameterValidationErrors.concat(nameValidationErrors);
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 allValidationErrors = parameterValidationErrors.concat(
nameValidationErrors
);

//channel variable validation: throw exception if channel validation failes
if (notProvidedParams.size || invalidChannelName.size) {
throw new ParserError({
type: validationError,
title: 'Channel validation failed',
parsedJSON,
validationErrors: allValidationErrors
validationErrors: allValidationErrors,
});
}

Expand All @@ -252,5 +372,5 @@ module.exports = {
validateServerVariables,
validateOperationId,
validateServerSecurity,
validateChannels
validateChannels,
};
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit fcbc939

Please sign in to comment.