Skip to content

Commit

Permalink
feat: add server variables and channel parameters validation (#91)
Browse files Browse the repository at this point in the history
  • Loading branch information
derberg authored Jul 2, 2020
1 parent 6da7acb commit a65d776
Show file tree
Hide file tree
Showing 7 changed files with 972 additions and 70 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
node_modules
.vscode
.nyc_output
coverage
.DS_Store
74 changes: 74 additions & 0 deletions lib/customValidators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const ParserError = require('./errors/parser-error');
const { parseUrlVariables, getMissingProps, groupValidationErrors } = require('./utils');

function validateServerVariables(parsedJSON, asyncapiYAMLorJSON, initialFormat) {
const srvs = parsedJSON.servers;
if (!srvs) return true;

const srvsMap = new Map(Object.entries(srvs));
const notProvidedVariables = new Map();

srvsMap.forEach((val, key) => {
const variables = parseUrlVariables(val.url);
const notProvidedServerVars = notProvidedVariables.get(key);
if (!variables) return;

const missingServerVariables = getMissingProps(variables, val.variables);
if (!missingServerVariables.length) return;

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

if (notProvidedVariables.size > 0) {
throw new ParserError({
type: 'validation-errors',
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)
});
}

return true;
}

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(key);
if (!variables) return;

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

if (!missingChannelParams.length) return;

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

if (notProvidedParams.size > 0) {
throw new ParserError({
type: 'validation-errors',
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;
}

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

Expand Down Expand Up @@ -115,7 +116,7 @@ async function parse(asyncapiYAMLorJSON, options = {}) {
validationErrors: improveAjvErrors(validate.errors, asyncapiYAMLorJSON, initialFormat),
});

await iterateDocument(parsedJSON, options);
await customDocumentOperations(parsedJSON, asyncapiYAMLorJSON, initialFormat, options);
} catch (e) {
if (e instanceof ParserError) throw e;
throw new ParserError({
Expand Down Expand Up @@ -147,7 +148,12 @@ function parseFromUrl(url, fetchOptions = {}, options) {
});
}

async function iterateDocument (js, options) {
async function customDocumentOperations(js, asyncapiYAMLorJSON, initialFormat, options) {
if (js.servers) validateServerVariables(js, asyncapiYAMLorJSON, initialFormat);
if (!js.channels) return;

validateChannelParams(js, asyncapiYAMLorJSON, initialFormat);

for (const channelName in js.channels) {
const channel = js.channels[channelName];
const convert = OPERATIONS.map(async (opName) => {
Expand All @@ -166,7 +172,7 @@ async function iterateDocument (js, options) {
}
}

async function validateAndConvertMessage (msg) {
async function validateAndConvertMessage(msg) {
const schemaFormat = msg.schemaFormat || DEFAULT_SCHEMA_FORMAT;

await PARSERS[schemaFormat]({
Expand Down
48 changes: 45 additions & 3 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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 RE2 = require('re2');

const jsonPointerToArray = jsonPointer => (jsonPointer || '/').split('/').splice(1);

Expand Down Expand Up @@ -56,9 +57,8 @@ const findNodeInAST = (ast, location) => {
const findLocationOf = (keys, ast, initialFormat) => {
let node;
let info;

if (initialFormat === 'js') return { jsonPointer: `/${keys.join('/')}` };

if (initialFormat === 'yaml') {
node = findNode(ast, keys);
if (!node) return { jsonPointer: `/${keys.join('/')}` };
Expand Down Expand Up @@ -219,16 +219,58 @@ utils.findRefs = (json, absolutePath, relativePath, initialFormat, asyncapiYAMLo
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 || '/' };

return {
title: `${error.dataPath || '/'} ${error.message}`,
location: ast ? findLocationOf(jsonPointerToArray(error.dataPath), ast, initialFormat) : defaultLocation,
};
});
};
};

/**
* It parses the string and returns an array with all values that are between curly braces, including braces
*/
utils.parseUrlVariables = str => {
if (typeof str !== 'string') return;
const regex = new RE2(/{(.+?)}/g);

return regex.match(str);
};

/**
* Returns an array of not existing properties in provided object with names specified in provided array
*/
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
*/
utils.groupValidationErrors = (root, errorMessage, errorElements, asyncapiYAMLorJSON, initialFormat) => {
const errors = [];
const regex = new RE2(/\//g);

errorElements.forEach((val,key) => {
errors.push({
title: `${key} ${errorMessage}: ${val}`,
location: utils.getLocationOf(root + key.replace(regex, '~1'), asyncapiYAMLorJSON, initialFormat)
});
});

return errors;
};
Loading

0 comments on commit a65d776

Please sign in to comment.