Skip to content

Commit

Permalink
feat: add validation of query params in channel name (#191)
Browse files Browse the repository at this point in the history
Co-authored-by: Lukasz Gornicki <[email protected]>
  • Loading branch information
juergenbr and derberg authored Nov 2, 2020
1 parent 1a3be93 commit d871aff
Show file tree
Hide file tree
Showing 4 changed files with 412 additions and 126 deletions.
129 changes: 69 additions & 60 deletions lib/customValidators.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const ParserError = require('./errors/parser-error');
const { parseUrlVariables, getMissingProps, groupValidationErrors, tilde } = require('./utils');
const { parseUrlVariables, getMissingProps, groupValidationErrors, tilde, parseUrlQueryParameters, setNotProvidedParams } = require('./utils');
const validationError = 'validation-errors';

/**
Expand All @@ -25,9 +25,9 @@ function validateServerVariables(parsedJSON, asyncapiYAMLorJSON, initialFormat)
const missingServerVariables = getMissingProps(variables, val.variables);
if (!missingServerVariables.length) return;

notProvidedVariables.set(tilde(key),
notProvidedServerVars
? notProvidedServerVars.concat(missingServerVariables)
notProvidedVariables.set(tilde(key),
notProvidedServerVars
? notProvidedServerVars.concat(missingServerVariables)
: missingServerVariables);
});

Expand All @@ -43,49 +43,6 @@ function validateServerVariables(parsedJSON, asyncapiYAMLorJSON, initialFormat)
return true;
}

/**
* Validates if parameters specified in the channel have corresponding parameters object defined
*
* @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 validateChannelParams(parsedJSON, asyncapiYAMLorJSON, initialFormat) {
const chnls = parsedJSON.channels;
if (!chnls) return true;

const chnlsMap = new Map(Object.entries(chnls));
const notProvidedParams = new Map();

chnlsMap.forEach((val, key) => {
const variables = parseUrlVariables(key);
const notProvidedChannelParams = notProvidedParams.get(tilde(key));
if (!variables) return;

const missingChannelParams = getMissingProps(variables, val.parameters);

if (!missingChannelParams.length) return;

notProvidedParams.set(tilde(key),
notProvidedChannelParams
? notProvidedChannelParams.concat(missingChannelParams)
: missingChannelParams);
});

if (notProvidedParams.size) {
throw new ParserError({
type: validationError,
title: 'Not all channel parameters are described with parameter object',
parsedJSON,
validationErrors: groupValidationErrors('channels', 'channel does not have a corresponding parameter object for', notProvidedParams, asyncapiYAMLorJSON, initialFormat)
});
}

return true;
}

/**
* Validates if operationIds are duplicated in the document
*
Expand All @@ -103,20 +60,20 @@ function validateOperationId(parsedJSON, asyncapiYAMLorJSON, initialFormat, oper
const duplicatedOperations = new Map();
//is is a 2-dimentional 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);

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]);
duplicatedOperations.set(operationPath, isOperationIdDuplicated[0][1]);
};

chnlsMap.forEach((chnlObj,chnlName) => {
chnlsMap.forEach((chnlObj, chnlName) => {
operations.forEach(opName => {
const op = chnlObj[String(opName)];
if (op) addDuplicateToMap(op, chnlName, opName);
Expand Down Expand Up @@ -148,7 +105,7 @@ function validateOperationId(parsedJSON, asyncapiYAMLorJSON, initialFormat, oper
function validateServerSecurity(parsedJSON, asyncapiYAMLorJSON, initialFormat, specialSecTypes) {
const srvs = parsedJSON.servers;
if (!srvs) return true;

const root = 'servers';
const srvsMap = new Map(Object.entries(srvs));

Expand Down Expand Up @@ -206,10 +163,10 @@ function validateServerSecurity(parsedJSON, asyncapiYAMLorJSON, initialFormat, s
* @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 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) {
Expand Down Expand Up @@ -239,9 +196,61 @@ function isSrvrSecProperArray(schemaType, specialSecTypes, secObj, secName) {
return true;
}

/**
* 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
* @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 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

chnlsMap.forEach((val, key) => {
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);
}

//channel name validation: fill return object with channels containing query parameters
if (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);

//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
});
}

return true;
}

module.exports = {
validateChannelParams,
validateServerVariables,
validateOperationId,
validateServerSecurity
validateServerSecurity,
validateChannels
};
4 changes: 2 additions & 2 deletions lib/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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 { validateChannelParams, validateServerVariables, validateOperationId, validateServerSecurity } = require('./customValidators.js');
const { validateChannels, validateServerVariables, validateOperationId, validateServerSecurity } = require('./customValidators.js');
const { toJS, findRefs, getLocationOf, improveAjvErrors } = require('./utils');
const AsyncAPIDocument = require('./models/asyncapi');

Expand Down Expand Up @@ -170,7 +170,7 @@ async function customDocumentOperations(parsedJSON, asyncapiYAMLorJSON, initialF

if (!parsedJSON.channels) return;

validateChannelParams(parsedJSON, asyncapiYAMLorJSON, initialFormat);
validateChannels(parsedJSON, asyncapiYAMLorJSON, initialFormat);
validateOperationId(parsedJSON, asyncapiYAMLorJSON, initialFormat, OPERATIONS);

await customChannelsOperations(parsedJSON, asyncapiYAMLorJSON, initialFormat, options);
Expand Down
35 changes: 35 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,19 @@ utils.parseUrlVariables = str => {
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;
const channelName = str;
const url = new URL(`http://${channelName}`);

return url.search;
};

/**
* Returns an array of not existing properties in provided object with names specified in provided array
* @function getMissingProps
Expand Down Expand Up @@ -304,3 +317,25 @@ utils.groupValidationErrors = (root, errorMessage, errorElements, asyncapiYAMLor

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);
}
};
Loading

0 comments on commit d871aff

Please sign in to comment.