From 628d6c00f4968a276819f2d947de31d790b81897 Mon Sep 17 00:00:00 2001 From: barrett-schonefeld <59850348+barrett-schonefeld@users.noreply.github.com> Date: Wed, 19 Feb 2020 11:17:39 -0600 Subject: [PATCH] refactor: introduces MessageCarrier to simplify adding warnings and errors (#141) Adds a message carrier class to handle collecting errors and warnings --- src/plugins/utils/messageCarrier.js | 42 ++++ .../2and3/semantic-validators/info.js | 35 +-- .../items-required-for-array-objects.js | 34 +-- .../semantic-validators/operation-ids.js | 26 +- .../semantic-validators/operations-shared.js | 103 ++++---- .../semantic-validators/parameters-ibm.js | 71 +++--- .../2and3/semantic-validators/paths-ibm.js | 53 +++-- .../2and3/semantic-validators/paths.js | 74 +++--- .../2and3/semantic-validators/refs.js | 17 +- .../2and3/semantic-validators/responses.js | 65 ++--- .../2and3/semantic-validators/schema-ibm.js | 224 +++++++----------- .../security-definitions-ibm.js | 47 ++-- .../2and3/semantic-validators/security-ibm.js | 41 ++-- .../2and3/semantic-validators/walker-ibm.js | 41 ++-- .../2and3/semantic-validators/walker.js | 71 +++--- .../oas3/semantic-validators/discriminator.js | 45 ++-- .../oas3/semantic-validators/dummy-ibm.js | 20 +- .../oas3/semantic-validators/openapi.js | 34 +-- .../oas3/semantic-validators/operations.js | 44 ++-- .../semantic-validators/pagination-ibm.js | 86 ++++--- .../oas3/semantic-validators/parameters.js | 73 +++--- .../oas3/semantic-validators/responses.js | 72 +++--- .../security-definitions-ibm.js | 164 ++++++------- .../semantic-validators/discriminator.js | 53 +++-- .../swagger2/semantic-validators/dummy-ibm.js | 8 +- .../swagger2/semantic-validators/dummy.js | 8 +- .../swagger2/semantic-validators/form-data.js | 71 +++--- .../semantic-validators/operations-ibm.js | 47 ++-- .../semantic-validators/operations.js | 25 +- .../semantic-validators/parameters.js | 13 +- .../swagger2/semantic-validators/schema.js | 22 +- .../security-definitions.js | 98 ++++---- .../swagger2/semantic-validators/swagger.js | 34 +-- .../{ => utils}/caseConventionCheck.js | 2 +- test/plugins/{ => utils}/has-ref-property.js | 2 +- test/plugins/utils/messageCarrier.js | 154 ++++++++++++ 36 files changed, 1064 insertions(+), 955 deletions(-) create mode 100644 src/plugins/utils/messageCarrier.js rename test/plugins/{ => utils}/caseConventionCheck.js (98%) rename test/plugins/{ => utils}/has-ref-property.js (97%) create mode 100644 test/plugins/utils/messageCarrier.js diff --git a/src/plugins/utils/messageCarrier.js b/src/plugins/utils/messageCarrier.js new file mode 100644 index 000000000..ca3d51c1c --- /dev/null +++ b/src/plugins/utils/messageCarrier.js @@ -0,0 +1,42 @@ +'use strict'; + +module.exports = class MessageCarrier { + constructor() { + this._messages = { + error: [], + warning: [] + }; + } + + get messages() { + return this._messages; + } + + get errors() { + return this._messages.error; + } + + get warnings() { + return this._messages.warning; + } + + // status should be 'off', 'error', or 'warning' + addMessage(path, message, status) { + if (this._messages[status]) { + this._messages[status].push({ + path, + message + }); + } + } + + addMessageWithAuthId(path, message, authId, status) { + if (this._messages[status]) { + this._messages[status].push({ + path, + message, + authId + }); + } + } +}; diff --git a/src/plugins/validation/2and3/semantic-validators/info.js b/src/plugins/validation/2and3/semantic-validators/info.js index 0d3a46840..f7a027253 100644 --- a/src/plugins/validation/2and3/semantic-validators/info.js +++ b/src/plugins/validation/2and3/semantic-validators/info.js @@ -3,17 +3,20 @@ // Assertation 2: // making sure that the required version and title are defined properly + +const MessageCarrier = require('../../../utils/messageCarrier'); + module.exports.validate = function({ jsSpec }) { - const errors = []; - const warnings = []; + const messages = new MessageCarrier(); const info = jsSpec.info; const hasInfo = info && typeof info === 'object'; if (!hasInfo) { - errors.push({ - path: ['info'], - message: 'API definition must have an `info` object' - }); + messages.addMessage( + ['info'], + 'API definition must have an `info` object', + 'error' + ); } else { const title = jsSpec.info.title; const hasTitle = @@ -23,16 +26,18 @@ module.exports.validate = function({ jsSpec }) { typeof version === 'string' && version.toString().trim().length > 0; if (!hasTitle) { - errors.push({ - path: ['info', 'title'], - message: '`info` object must have a string-type `title` field' - }); + messages.addMessage( + ['info', 'title'], + '`info` object must have a string-type `title` field', + 'error' + ); } else if (!hasVersion) { - errors.push({ - path: ['info', 'version'], - message: '`info` object must have a string-type `version` field' - }); + messages.addMessage( + ['info', 'version'], + '`info` object must have a string-type `version` field', + 'error' + ); } } - return { errors, warnings }; + return messages; }; diff --git a/src/plugins/validation/2and3/semantic-validators/items-required-for-array-objects.js b/src/plugins/validation/2and3/semantic-validators/items-required-for-array-objects.js index fba87ddbc..bb7594dc6 100644 --- a/src/plugins/validation/2and3/semantic-validators/items-required-for-array-objects.js +++ b/src/plugins/validation/2and3/semantic-validators/items-required-for-array-objects.js @@ -9,10 +9,10 @@ // Headers with 'array' type require an 'items' property const { walk } = require('../../../utils'); +const MessageCarrier = require('../../../utils/messageCarrier'); module.exports.validate = function({ jsSpec }) { - const errors = []; - const warnings = []; + const messages = new MessageCarrier(); walk(jsSpec, [], function(obj, path) { // `definitions` for Swagger 2, `schemas` for OAS 3 @@ -28,23 +28,22 @@ module.exports.validate = function({ jsSpec }) { // Assertation 1 if (obj.type === 'array' && typeof obj.items !== 'object') { - errors.push({ - path: path.join('.'), - message: - "Schema objects with 'array' type require an 'items' property" - }); + messages.addMessage( + path.join('.'), + "Schema objects with 'array' type require an 'items' property", + 'error' + ); } // Assertation 2 if (Array.isArray(obj.required)) { obj.required.forEach((requiredProp, i) => { if (!obj.properties || !obj.properties[requiredProp]) { - const pathStr = path.concat([`required[${i}]`]).join('.'); - errors.push({ - path: pathStr, - message: - "Schema properties specified as 'required' must be defined" - }); + messages.addMessage( + path.concat([`required[${i}]`]).join('.'), + "Schema properties specified as 'required' must be defined", + 'error' + ); } }); } @@ -53,13 +52,14 @@ module.exports.validate = function({ jsSpec }) { // this only applies to Swagger 2 if (path[path.length - 2] === 'headers') { if (obj.type === 'array' && typeof obj.items !== 'object') { - errors.push({ + messages.addMessage( path, - message: "Headers with 'array' type require an 'items' property" - }); + "Headers with 'array' type require an 'items' property", + 'error' + ); } } }); - return { errors, warnings }; + return messages; }; diff --git a/src/plugins/validation/2and3/semantic-validators/operation-ids.js b/src/plugins/validation/2and3/semantic-validators/operation-ids.js index 976c42e34..bb3c585f5 100644 --- a/src/plugins/validation/2and3/semantic-validators/operation-ids.js +++ b/src/plugins/validation/2and3/semantic-validators/operation-ids.js @@ -6,10 +6,10 @@ const pickBy = require('lodash/pickBy'); const reduce = require('lodash/reduce'); const merge = require('lodash/merge'); const each = require('lodash/each'); +const MessageCarrier = require('../../../utils/messageCarrier'); module.exports.validate = function({ resolvedSpec }) { - const errors = []; - const warnings = []; + const messages = new MessageCarrier(); const validOperationKeys = [ 'get', @@ -137,10 +137,11 @@ module.exports.validate = function({ resolvedSpec }) { const hasBeenSeen = tallyOperationId(op.operationId); if (hasBeenSeen) { // Assertation 1: Operations must have a unique operationId. - errors.push({ - path: op.path + '.operationId', - message: 'operationIds must be unique' - }); + messages.addMessage( + op.path + '.operationId', + 'operationIds must be unique', + 'error' + ); } else { // Assertation 2: OperationId must conform to naming conventions const regex = RegExp(/{[a-zA-Z0-9_-]+\}$/m); @@ -153,17 +154,18 @@ module.exports.validate = function({ resolvedSpec }) { ); if (checkPassed === false) { - warnings.push({ - path: op.path + '.operationId', - message: `operationIds should follow consistent naming convention. operationId verb should be ${verbs}`.replace( + messages.addMessage( + op.path + '.operationId', + `operationIds should follow consistent naming convention. operationId verb should be ${verbs}`.replace( ',', ' or ' - ) - }); + ), + 'warning' + ); } } } }); - return { errors, warnings }; + return messages; }; diff --git a/src/plugins/validation/2and3/semantic-validators/operations-shared.js b/src/plugins/validation/2and3/semantic-validators/operations-shared.js index 48c9a6614..e06660310 100644 --- a/src/plugins/validation/2and3/semantic-validators/operations-shared.js +++ b/src/plugins/validation/2and3/semantic-validators/operations-shared.js @@ -19,11 +19,10 @@ const map = require('lodash/map'); const each = require('lodash/each'); const findIndex = require('lodash/findIndex'); const { checkCase, hasRefProperty } = require('../../../utils'); +const MessageCarrier = require('../../../utils/messageCarrier'); module.exports.validate = function({ jsSpec, resolvedSpec, isOAS3 }, config) { - const result = {}; - result.error = []; - result.warning = []; + const messages = new MessageCarrier(); config = config.operations; @@ -50,10 +49,11 @@ module.exports.validate = function({ jsSpec, resolvedSpec, isOAS3 }, config) { // check for operations that have a $ref property // these are illegal in the spec if (hasRefProperty(jsSpec, ['paths', pathKey, opKey])) { - result.error.push({ - path: `paths.${pathKey}.${opKey}.$ref`, - message: '$ref found in illegal location' - }); + messages.addMessage( + `paths.${pathKey}.${opKey}.$ref`, + '$ref found in illegal location', + 'error' + ); } // check for unique name/in properties in params @@ -68,11 +68,11 @@ module.exports.validate = function({ jsSpec, resolvedSpec, isOAS3 }, config) { // it also will favor complaining about parameters later in the spec, which // makes more sense to the user. if (paramIndex !== nameAndInComboIndex) { - result.error.push({ - path: `paths.${pathKey}.${opKey}.parameters[${paramIndex}]`, - message: - "Operation parameters must have unique 'name' + 'in' properties" - }); + messages.addMessage( + `paths.${pathKey}.${opKey}.parameters[${paramIndex}]`, + "Operation parameters must have unique 'name' + 'in' properties", + 'error' + ); } }); @@ -87,11 +87,11 @@ module.exports.validate = function({ jsSpec, resolvedSpec, isOAS3 }, config) { (content.schema.type === 'array' || content.schema.items); if (isArray) { - result[checkStatusArrRes].push({ - path: `paths.${pathKey}.${opKey}.responses.${name}.content.${contentType}.schema`, - message: - 'Arrays MUST NOT be returned as the top-level structure in a response body.' - }); + messages.addMessage( + `paths.${pathKey}.${opKey}.responses.${name}.content.${contentType}.schema`, + 'Arrays MUST NOT be returned as the top-level structure in a response body.', + checkStatusArrRes + ); } }); } else { @@ -100,11 +100,11 @@ module.exports.validate = function({ jsSpec, resolvedSpec, isOAS3 }, config) { (response.schema.type === 'array' || response.schema.items); if (isArray) { - result[checkStatusArrRes].push({ - path: `paths.${pathKey}.${opKey}.responses.${name}.schema`, - message: - 'Arrays MUST NOT be returned as the top-level structure in a response body.' - }); + messages.addMessage( + `paths.${pathKey}.${opKey}.responses.${name}.schema`, + 'Arrays MUST NOT be returned as the top-level structure in a response body.', + checkStatusArrRes + ); } } }); @@ -115,23 +115,22 @@ module.exports.validate = function({ jsSpec, resolvedSpec, isOAS3 }, config) { op.operationId.length > 0 && !!op.operationId.toString().trim(); if (!hasOperationId) { - const checkStatus = config.no_operation_id; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path: `paths.${pathKey}.${opKey}.operationId`, - message: 'Operations must have a non-empty `operationId`.' - }); - } + messages.addMessage( + `paths.${pathKey}.${opKey}.operationId`, + 'Operations must have a non-empty `operationId`.', + config.no_operation_id + ); } else { // check operationId for case convention const checkStatus = config.operation_id_case_convention[0]; const caseConvention = config.operation_id_case_convention[1]; const isCorrectCase = checkCase(op.operationId, caseConvention); - if (!isCorrectCase && checkStatus != 'off') { - result[checkStatus].push({ - path: `paths.${pathKey}.${opKey}.operationId`, - message: `operationIds must follow case convention: ${caseConvention}` - }); + if (!isCorrectCase) { + messages.addMessage( + `paths.${pathKey}.${opKey}.operationId`, + `operationIds must follow case convention: ${caseConvention}`, + checkStatus + ); } } const hasOperationTags = op.tags && op.tags.length > 0; @@ -143,13 +142,11 @@ module.exports.validate = function({ jsSpec, resolvedSpec, isOAS3 }, config) { } for (let i = 0, len = op.tags.length; i < len; i++) { if (!resolvedTags.includes(op.tags[i])) { - const checkStatus = config.unused_tag; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path: `paths.${pathKey}.${opKey}.tags`, - message: 'tag is not defined at the global level: ' + op.tags[i] - }); - } + messages.addMessage( + `paths.${pathKey}.${opKey}.tags`, + 'tag is not defined at the global level: ' + op.tags[i], + config.unused_tag + ); } } } @@ -157,13 +154,11 @@ module.exports.validate = function({ jsSpec, resolvedSpec, isOAS3 }, config) { const hasSummary = op.summary && op.summary.length > 0 && !!op.summary.toString().trim(); if (!hasSummary) { - const checkStatus = config.no_summary; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path: `paths.${pathKey}.${opKey}.summary`, - message: 'Operations must have a non-empty `summary` field.' - }); - } + messages.addMessage( + `paths.${pathKey}.${opKey}.summary`, + 'Operations must have a non-empty `summary` field.', + config.no_summary + ); } // this should be good with resolved spec, but double check @@ -180,11 +175,11 @@ module.exports.validate = function({ jsSpec, resolvedSpec, isOAS3 }, config) { } } else { if (param.required) { - result[checkStatusParamOrder].push({ - path: `paths.${pathKey}.${opKey}.parameters[${indx}]`, - message: - 'Required parameters should appear before optional parameters.' - }); + messages.addMessage( + `paths.${pathKey}.${opKey}.parameters[${indx}]`, + 'Required parameters should appear before optional parameters.', + checkStatusParamOrder + ); } } } @@ -193,5 +188,5 @@ module.exports.validate = function({ jsSpec, resolvedSpec, isOAS3 }, config) { }); }); - return { errors: result.error, warnings: result.warning }; + return messages; }; diff --git a/src/plugins/validation/2and3/semantic-validators/parameters-ibm.js b/src/plugins/validation/2and3/semantic-validators/parameters-ibm.js index 4c54766bf..3b3cbbb60 100644 --- a/src/plugins/validation/2and3/semantic-validators/parameters-ibm.js +++ b/src/plugins/validation/2and3/semantic-validators/parameters-ibm.js @@ -11,11 +11,10 @@ const pick = require('lodash/pick'); const includes = require('lodash/includes'); const { checkCase, isParameterObject, walk } = require('../../../utils'); +const MessageCarrier = require('../../../utils/messageCarrier'); module.exports.validate = function({ jsSpec, isOAS3 }, config) { - const result = {}; - result.error = []; - result.warning = []; + const messages = new MessageCarrier(); config = config.parameters; @@ -33,14 +32,11 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) { const hasDescription = !!obj.description; if (!hasDescription && !isRef) { - const message = 'Parameter objects must have a `description` field.'; - const checkStatus = config.no_parameter_description; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path, - message - }); - } + messages.addMessage( + path, + 'Parameter objects must have a `description` field.', + config.no_parameter_description + ); } const isParameter = obj.in; // the `in` property is required by OpenAPI for parameters - this should be true (unless obj is a ref) @@ -60,11 +56,11 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) { .every(v => v); if (!isCorrectCase) { - const message = `Parameter names must follow case convention: ${caseConvention}`; - result[checkStatus].push({ + messages.addMessage( path, - message - }); + `Parameter names must follow case convention: ${caseConvention}`, + checkStatus + ); } } } @@ -77,11 +73,8 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) { messageCT = isOAS3 ? `${messageCT} Rely on the \`content\` field of a request body or response object to specify content-type.` : `${messageCT} Rely on the \`consumes\` field to specify content-type.`; - if (definesContentType && checkStatusCT !== 'off') { - result[checkStatusCT].push({ - path, - message: messageCT - }); + if (definesContentType) { + messages.addMessage(path, messageCT, checkStatusCT); } // check for accept-type defined in a header parameter (AT = accept-type) @@ -91,11 +84,8 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) { messageAT = isOAS3 ? `${messageAT} Rely on the \`content\` field of a response object to specify accept-type.` : `${messageAT} Rely on the \`produces\` field to specify accept-type.`; - if (definesAcceptType && checkStatusAT !== 'off') { - result[checkStatusAT].push({ - path, - message: messageAT - }); + if (definesAcceptType) { + messages.addMessage(path, messageAT, checkStatusAT); } // check for accept-type defined in a header parameter (AT = accept-type) @@ -112,11 +102,8 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) { messageAuth + ' This check will be converted to an `error` in an upcoming release.'; } - if (definesAuth && checkStatusAuth !== 'off') { - result[checkStatusAuth].push({ - path, - message: messageAuth - }); + if (definesAuth) { + messages.addMessage(path, messageAuth, checkStatusAuth); } } @@ -124,11 +111,11 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) { if (checkStatus !== 'off') { const valid = formatValid(obj, isOAS3); if (!valid) { - const message = 'Parameter type+format is not well-defined.'; - result[checkStatus].push({ + messages.addMessage( path, - message - }); + 'Parameter type+format is not well-defined.', + checkStatus + ); } } @@ -141,20 +128,16 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) { } if (isParameterRequired && isDefaultDefined) { - const message = - 'Required parameters should not specify default values.'; - const checkStatus = config.required_param_has_default; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path, - message - }); - } + messages.addMessage( + path, + 'Required parameters should not specify default values.', + config.required_param_has_default + ); } } }); - return { errors: result.error, warnings: result.warning }; + return messages; }; function formatValid(obj, isOAS3) { diff --git a/src/plugins/validation/2and3/semantic-validators/paths-ibm.js b/src/plugins/validation/2and3/semantic-validators/paths-ibm.js index 37286e1fe..92ad9631e 100644 --- a/src/plugins/validation/2and3/semantic-validators/paths-ibm.js +++ b/src/plugins/validation/2and3/semantic-validators/paths-ibm.js @@ -11,6 +11,7 @@ const flatten = require('lodash/flatten'); const isEqual = require('lodash/isEqual'); const uniqWith = require('lodash/uniqWith'); +const MessageCarrier = require('../../../utils/messageCarrier'); const { checkCase } = require('../../../utils'); @@ -26,9 +27,7 @@ const allowedOperations = [ ]; module.exports.validate = function({ resolvedSpec }, config) { - const result = {}; - result.error = []; - result.warning = []; + const messages = new MessageCarrier(); config = config.paths; @@ -93,10 +92,11 @@ module.exports.validate = function({ resolvedSpec }, config) { const checkStatus = config.missing_path_parameter; if (checkStatus != 'off') { missingParameters.forEach(name => { - result[checkStatus].push({ - path: `paths.${pathName}.${opName}.parameters`, - message: `Operation must include a path parameter with name: ${name}.` - }); + messages.addMessage( + `paths.${pathName}.${opName}.parameters`, + `Operation must include a path parameter with name: ${name}.`, + checkStatus + ); }); } } @@ -115,10 +115,11 @@ module.exports.validate = function({ resolvedSpec }, config) { const checkStatus = config.missing_path_parameter; if (checkStatus != 'off') { missingParameters.forEach(name => { - result[checkStatus].push({ - path: `paths.${pathName}`, - message: `Path parameter must be defined at the path or the operation level: ${name}.` - }); + messages.addMessage( + `paths.${pathName}`, + `Path parameter must be defined at the path or the operation level: ${name}.`, + checkStatus + ); }); } } @@ -148,11 +149,11 @@ module.exports.validate = function({ resolvedSpec }, config) { const index = pathObj[op].parameters.findIndex( p => p.name === parameter ); - result[checkStatus].push({ - path: `paths.${pathName}.${op}.parameters.${index}`, - message: - 'Common path parameters should be defined on path object' - }); + messages.addMessage( + `paths.${pathName}.${op}.parameters.${index}`, + 'Common path parameters should be defined on path object', + checkStatus + ); }); } } @@ -171,10 +172,11 @@ module.exports.validate = function({ resolvedSpec }, config) { return; } if (!checkCase(segment, 'lower_snake_case')) { - result[checkStatus].push({ - path: `paths.${pathName}`, - message: `Path segments must be lower snake case.` - }); + messages.addMessage( + `paths.${pathName}`, + `Path segments must be lower snake case.`, + checkStatus + ); } }); } else { @@ -194,10 +196,11 @@ module.exports.validate = function({ resolvedSpec }, config) { } const isCorrectCase = checkCase(segment, caseConvention); if (!isCorrectCase) { - result[checkStatusPath].push({ - path: `paths.${pathName}`, - message: `Path segments must follow case convention: ${caseConvention}` - }); + messages.addMessage( + `paths.${pathName}`, + `Path segments must follow case convention: ${caseConvention}`, + checkStatusPath + ); } }); } @@ -205,5 +208,5 @@ module.exports.validate = function({ resolvedSpec }, config) { } }); - return { errors: result.error, warnings: result.warning }; + return messages; }; diff --git a/src/plugins/validation/2and3/semantic-validators/paths.js b/src/plugins/validation/2and3/semantic-validators/paths.js index 65566a4c2..e0db436e0 100644 --- a/src/plugins/validation/2and3/semantic-validators/paths.js +++ b/src/plugins/validation/2and3/semantic-validators/paths.js @@ -19,12 +19,12 @@ const each = require('lodash/each'); const findIndex = require('lodash/findIndex'); const isObject = require('lodash/isObject'); +const MessageCarrier = require('../../../utils/messageCarrier'); const templateRegex = /\{(.*?)\}/g; module.exports.validate = function({ resolvedSpec }) { - const errors = []; - const warnings = []; + const messages = new MessageCarrier(); const seenRealPaths = {}; @@ -48,19 +48,21 @@ module.exports.validate = function({ resolvedSpec }) { templateRegex.test(substr) && substr.replace(templateRegex, '').length > 0 ) { - errors.push({ - path: `paths.${pathName}`, - message: 'Partial path templating is not allowed.' - }); + messages.addMessage( + `paths.${pathName}`, + 'Partial path templating is not allowed.', + 'error' + ); } }); // Assertation 6 if (pathName.indexOf('?') > -1) { - errors.push({ - path: `paths.${pathName}`, - message: 'Query strings in paths are not allowed.' - }); + messages.addMessage( + `paths.${pathName}`, + 'Query strings in paths are not allowed.', + 'error' + ); } const parametersFromPath = path.parameters ? path.parameters.slice() : []; @@ -94,10 +96,11 @@ module.exports.validate = function({ resolvedSpec }) { // Assertation 3 const hasBeenSeen = tallyRealPath(pathName); if (hasBeenSeen) { - errors.push({ - path: `paths.${pathName}`, - message: 'Equivalent paths are not allowed.' - }); + messages.addMessage( + `paths.${pathName}`, + 'Equivalent paths are not allowed.', + 'error' + ); } // Assertation 4 @@ -112,10 +115,11 @@ module.exports.validate = function({ resolvedSpec }) { // it also will favor complaining about parameters later in the spec, which // makes more sense to the user. if (i !== nameAndInComboIndex && parameterDefinition.in) { - errors.push({ - path: `paths.${pathName}.parameters[${i}]`, - message: "Path parameters must have unique 'name' + 'in' properties" - }); + messages.addMessage( + `paths.${pathName}.parameters[${i}]`, + "Path parameters must have unique 'name' + 'in' properties", + 'error' + ); } }); @@ -131,13 +135,13 @@ module.exports.validate = function({ resolvedSpec }) { parameterDefinition.in === 'path' && pathTemplates.indexOf(parameterDefinition.name) === -1 ) { - errors.push({ - path: - parameterDefinition.$$path || `paths.${pathName}.parameters[${i}]`, - message: `Path parameter was defined but never used: ${ + messages.addMessage( + parameterDefinition.$$path || `paths.${pathName}.parameters[${i}]`, + `Path parameter was defined but never used: ${ parameterDefinition.name - }` - }); + }`, + 'error' + ); } }); @@ -147,26 +151,28 @@ module.exports.validate = function({ resolvedSpec }) { if (parameter === '') { // it was originally "{}" - errors.push({ - path: `paths.${pathName}`, - message: 'Empty path parameter declarations are not valid' - }); + messages.addMessage( + `paths.${pathName}`, + 'Empty path parameter declarations are not valid', + 'error' + ); } }); } else { each(availableParameters, (parameterDefinition, i) => { // Assertation 1, for cases when no templating is present on the path if (parameterDefinition.in === 'path') { - errors.push({ - path: `paths.${pathName}.parameters[${i}]`, - message: `Path parameter was defined but never used: ${ + messages.addMessage( + `paths.${pathName}.parameters[${i}]`, + `Path parameter was defined but never used: ${ parameterDefinition.name - }` - }); + }`, + 'error' + ); } }); } }); - return { errors, warnings }; + return messages; }; diff --git a/src/plugins/validation/2and3/semantic-validators/refs.js b/src/plugins/validation/2and3/semantic-validators/refs.js index c4f4b5323..2a1d1225f 100644 --- a/src/plugins/validation/2and3/semantic-validators/refs.js +++ b/src/plugins/validation/2and3/semantic-validators/refs.js @@ -5,14 +5,14 @@ const uniq = require('lodash/uniq'); const filter = require('lodash/filter'); const startsWith = require('lodash/startsWith'); const each = require('lodash/each'); +const MessageCarrier = require('../../../utils/messageCarrier'); module.exports.validate = function({ jsSpec, specStr, isOAS3 }) { - const errors = []; - const warnings = []; + const messages = new MessageCarrier(); if (isOAS3 && !jsSpec.components) { // prevent trying to access components.schemas if components is undefined - return { errors, warnings }; + return messages; } const basePath = isOAS3 ? ['components', 'schemas'] : ['definitions']; @@ -37,12 +37,13 @@ module.exports.validate = function({ jsSpec, specStr, isOAS3 }) { const definitions = isOAS3 ? jsSpec.components.schemas : jsSpec.definitions; each(definitions, (def, defName) => { if (definitionsRefs.indexOf(`#/${basePath.join('/')}/${defName}`) === -1) { - warnings.push({ - path: `${basePath.join('.')}.${defName}`, - message: 'Definition was declared but never used in document' - }); + messages.addMessage( + `${basePath.join('.')}.${defName}`, + 'Definition was declared but never used in document', + 'warning' + ); } }); - return { errors, warnings }; + return messages; }; diff --git a/src/plugins/validation/2and3/semantic-validators/responses.js b/src/plugins/validation/2and3/semantic-validators/responses.js index 51bdec319..f9a95a308 100644 --- a/src/plugins/validation/2and3/semantic-validators/responses.js +++ b/src/plugins/validation/2and3/semantic-validators/responses.js @@ -1,13 +1,12 @@ const each = require('lodash/each'); const { walk } = require('../../../utils'); +const MessageCarrier = require('../../../utils/messageCarrier'); const INLINE_SCHEMA_MESSAGE = 'Response schemas should be defined with a named ref.'; module.exports.validate = function({ jsSpec, isOAS3 }, config) { - const result = {}; - result.error = []; - result.warning = []; + const messages = new MessageCarrier(); config = config.responses; @@ -41,39 +40,29 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) { const hasInlineSchema = !mediaType.schema[schemaType][i] .$ref; if (hasInlineSchema) { - const checkStatus = config.inline_response_schema; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path: [ - ...path, - responseKey, - 'content', - mediaTypeKey, - 'schema', - schemaType, - i - ], - message: INLINE_SCHEMA_MESSAGE - }); - } + messages.addMessage( + [ + ...path, + responseKey, + 'content', + mediaTypeKey, + 'schema', + schemaType, + i + ], + INLINE_SCHEMA_MESSAGE, + config.inline_response_schema + ); } } } }); } else if (!mediaType.schema.$ref) { - const checkStatus = config.inline_response_schema; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path: [ - ...path, - responseKey, - 'content', - mediaTypeKey, - 'schema' - ], - message: INLINE_SCHEMA_MESSAGE - }); - } + messages.addMessage( + [...path, responseKey, 'content', mediaTypeKey, 'schema'], + INLINE_SCHEMA_MESSAGE, + config.inline_response_schema + ); } } }); @@ -83,18 +72,16 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) { const hasInlineSchema = response.schema && !response.schema.$ref; if (hasInlineSchema) { - const checkStatus = config.inline_response_schema; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path: [...path, responseKey, 'schema'], - message: INLINE_SCHEMA_MESSAGE - }); - } + messages.addMessage( + [...path, responseKey, 'schema'], + INLINE_SCHEMA_MESSAGE, + config.inline_response_schema + ); } } }); } }); - return { errors: result.error, warnings: result.warning }; + return messages; }; diff --git a/src/plugins/validation/2and3/semantic-validators/schema-ibm.js b/src/plugins/validation/2and3/semantic-validators/schema-ibm.js index 11f558a93..4c2569df8 100644 --- a/src/plugins/validation/2and3/semantic-validators/schema-ibm.js +++ b/src/plugins/validation/2and3/semantic-validators/schema-ibm.js @@ -20,10 +20,10 @@ const forIn = require('lodash/forIn'); const includes = require('lodash/includes'); const { checkCase, walk } = require('../../../utils'); +const MessageCarrier = require('../../../utils/messageCarrier'); module.exports.validate = function({ jsSpec, isOAS3 }, config) { - const errors = []; - const warnings = []; + const messages = new MessageCarrier(); config = config.schemas; @@ -78,23 +78,15 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) { }); schemas.forEach(({ schema, path }) => { - let res = generateFormatErrors(schema, path, config, isOAS3); - errors.push(...res.error); - warnings.push(...res.warning); + generateFormatErrors(schema, path, config, isOAS3, messages); - res = generateDescriptionWarnings(schema, path, config, isOAS3); - errors.push(...res.error); - warnings.push(...res.warning); + generateDescriptionWarnings(schema, path, config, isOAS3, messages); const checkStatus = config.snake_case_only; if (checkStatus !== 'off') { - res = checkPropNames(schema, path, config); - errors.push(...res.error); - warnings.push(...res.warning); + checkPropNames(schema, path, config, messages); - res = checkEnumValues(schema, path, config); - errors.push(...res.error); - warnings.push(...res.warning); + checkEnumValues(schema, path, config, messages); } else { // optional support for property_case_convention and enum_case_convention // in config. In the else block because support should be mutually exclusive @@ -102,63 +94,58 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) { if (config.property_case_convention) { const checkCaseStatus = config.property_case_convention[0]; if (checkCaseStatus !== 'off') { - res = checkPropNamesCaseConvention( + checkPropNamesCaseConvention( schema, path, - config.property_case_convention + config.property_case_convention, + messages ); - errors.push(...res.error); - warnings.push(...res.warning); } } if (config.enum_case_convention) { const checkCaseStatus = config.enum_case_convention[0]; if (checkCaseStatus !== 'off') { - res = checkEnumCaseConvention( + checkEnumCaseConvention( schema, path, - config.enum_case_convention + config.enum_case_convention, + messages ); - errors.push(...res.error); - warnings.push(...res.warning); } } } }); - return { errors, warnings }; + return messages; }; // Flag as an error any property that does not have a recognized "type" and "format" according to the // [Swagger 2.0 spec](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types) -function generateFormatErrors(schema, contextPath, config, isOAS3) { - const result = {}; - result.error = []; - result.warning = []; - +function generateFormatErrors(schema, contextPath, config, isOAS3, messages) { if (schema.$ref) { - return result; + return; } // Special case: check for arrays of arrays let checkStatus = config.array_of_arrays; if (checkStatus !== 'off' && schema.type === 'array' && schema.items) { if (schema.items.type === 'array') { - const path = contextPath.concat(['items', 'type']); - const message = - 'Array properties should avoid having items of type array.'; - result[checkStatus].push({ path, message }); + messages.addMessage( + contextPath.concat(['items', 'type']), + 'Array properties should avoid having items of type array.', + checkStatus + ); } } checkStatus = config.invalid_type_format_pair; if (checkStatus !== 'off' && !formatValid(schema, contextPath, isOAS3)) { - const path = contextPath.concat(['type']); - const message = 'Property type+format is not well-defined.'; - result[checkStatus].push({ path, message }); + messages.addMessage( + contextPath.concat(['type']), + 'Property type+format is not well-defined.', + checkStatus + ); } - - return result; } function formatValid(property, path, isOAS3) { @@ -209,11 +196,13 @@ function formatValid(property, path, isOAS3) { } // http://watson-developer-cloud.github.io/api-guidelines/swagger-coding-style#models -function generateDescriptionWarnings(schema, contextPath, config, isOAS3) { - const result = {}; - result.error = []; - result.warning = []; - +function generateDescriptionWarnings( + schema, + contextPath, + config, + isOAS3, + messages +) { // determine if this is a top-level schema const isTopLevelSchema = isOAS3 ? contextPath.length === 3 && @@ -222,23 +211,18 @@ function generateDescriptionWarnings(schema, contextPath, config, isOAS3) { : contextPath.length === 2 && contextPath[0] === 'definitions'; // Check description in schema only for "top level" schema - if (isTopLevelSchema) { - const checkStatus = config.no_schema_description; - if (result[checkStatus]) { - const hasDescription = - schema.description && schema.description.toString().trim().length; - if (!hasDescription) { - const message = 'Schema must have a non-empty description.'; - result[checkStatus].push({ - path: contextPath, - message: message - }); - } - } + const hasDescription = + schema.description && schema.description.toString().trim().length; + if (isTopLevelSchema && !hasDescription) { + messages.addMessage( + contextPath, + 'Schema must have a non-empty description.', + config.no_schema_description + ); } if (!schema.properties) { - return result; + return; } // verify that every property of the model has a description @@ -254,43 +238,29 @@ function generateDescriptionWarnings(schema, contextPath, config, isOAS3) { const hasDescription = property.description && property.description.toString().trim().length; if (!hasDescription) { - const message = - 'Schema properties must have a description with content in it.'; - const checkStatus = config.no_property_description; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path, - message - }); - } + messages.addMessage( + path, + 'Schema properties must have a description with content in it.', + config.no_property_description + ); } else { // if the property does have a description, "Avoid describing a model as a 'JSON object' since this will be incorrect for some SDKs." const mentionsJSON = includes(property.description.toLowerCase(), 'json'); if (mentionsJSON) { - const message = - 'Not all languages use JSON, so descriptions should not state that the model is a JSON object.'; - const checkStatus = config.description_mentions_json; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path, - message - }); - } + messages.addMessage( + path, + 'Not all languages use JSON, so descriptions should not state that the model is a JSON object.', + config.description_mentions_json + ); } } }); - - return result; } // https://pages.github.ibm.com/CloudEngineering/api_handbook/design/terminology.html#formatting -function checkPropNames(schema, contextPath, config) { - const result = {}; - result.error = []; - result.warning = []; - +function checkPropNames(schema, contextPath, config, messages) { if (!schema.properties) { - return result; + return; } // flag any property whose name is not "lower snake case" @@ -303,15 +273,14 @@ function checkPropNames(schema, contextPath, config) { const checkStatus = config.snake_case_only || 'off'; if (checkStatus.match('error|warning')) { if (!checkCase(propName, 'lower_snake_case')) { - result[checkStatus].push({ - path: contextPath.concat(['properties', propName]), - message: 'Property names must be lower snake case.' - }); + messages.addMessage( + contextPath.concat(['properties', propName]), + 'Property names must be lower snake case.', + checkStatus + ); } } }); - - return result; } /** @@ -320,16 +289,14 @@ function checkPropNames(schema, contextPath, config) { * @param contextPath * @param caseConvention an array, [0]='off' | 'warning' | 'error'. [1]='lower_snake_case' etc. */ -function checkPropNamesCaseConvention(schema, contextPath, caseConvention) { - const result = {}; - result.error = []; - result.warning = []; - - if (!schema.properties) { - return result; - } - if (!caseConvention) { - return result; +function checkPropNamesCaseConvention( + schema, + contextPath, + caseConvention, + messages +) { + if (!schema.properties || !caseConvention) { + return; } // flag any property whose name does not follow the case convention @@ -345,24 +312,19 @@ function checkPropNamesCaseConvention(schema, contextPath, caseConvention) { const isCorrectCase = checkCase(propName, caseConventionValue); if (!isCorrectCase) { - result[checkStatus].push({ - path: contextPath.concat(['properties', propName]), - message: `Property names must follow case convention: ${caseConventionValue}` - }); + messages.addMessage( + contextPath.concat(['properties', propName]), + `Property names must follow case convention: ${caseConventionValue}`, + checkStatus + ); } } }); - - return result; } -function checkEnumValues(schema, contextPath, config) { - const result = {}; - result.error = []; - result.warning = []; - +function checkEnumValues(schema, contextPath, config, messages) { if (!schema.enum) { - return result; + return; } for (let i = 0; i < schema.enum.length; i++) { @@ -371,16 +333,15 @@ function checkEnumValues(schema, contextPath, config) { const checkStatus = config.snake_case_only || 'off'; if (checkStatus.match('error|warning')) { if (!checkCase(enumValue, 'lower_snake_case')) { - result[checkStatus].push({ - path: contextPath.concat(['enum', i.toString()]), - message: 'Enum values must be lower snake case.' - }); + messages.addMessage( + contextPath.concat(['enum', i.toString()]), + 'Enum values must be lower snake case.', + checkStatus + ); } } } } - - return result; } /** @@ -389,16 +350,14 @@ function checkEnumValues(schema, contextPath, config) { * @param contextPath * @param caseConvention an array, [0]='off' | 'warning' | 'error'. [1]='lower_snake_case' etc. */ -function checkEnumCaseConvention(schema, contextPath, caseConvention) { - const result = {}; - result.error = []; - result.warning = []; - - if (!schema.enum) { - return result; - } - if (!caseConvention) { - return result; +function checkEnumCaseConvention( + schema, + contextPath, + caseConvention, + messages +) { + if (!schema.enum || !caseConvention) { + return; } for (let i = 0; i < schema.enum.length; i++) { @@ -409,16 +368,15 @@ function checkEnumCaseConvention(schema, contextPath, caseConvention) { const caseConventionValue = caseConvention[1]; const isCorrectCase = checkCase(enumValue, caseConventionValue); if (!isCorrectCase) { - result[checkStatus].push({ - path: contextPath.concat(['enum', i.toString()]), - message: `Enum values must follow case convention: ${caseConventionValue}` - }); + messages.addMessage( + contextPath.concat(['enum', i.toString()]), + `Enum values must follow case convention: ${caseConventionValue}`, + checkStatus + ); } } } } - - return result; } // NOTE: this function is Swagger 2 specific and would need to be adapted to be used with OAS diff --git a/src/plugins/validation/2and3/semantic-validators/security-definitions-ibm.js b/src/plugins/validation/2and3/semantic-validators/security-definitions-ibm.js index b382fd3c0..9507e7056 100644 --- a/src/plugins/validation/2and3/semantic-validators/security-definitions-ibm.js +++ b/src/plugins/validation/2and3/semantic-validators/security-definitions-ibm.js @@ -3,11 +3,10 @@ // Assertation 2: Each scope defined in an OAuth2 scheme should be used in the spec const each = require('lodash/each'); +const MessageCarrier = require('../../../utils/messageCarrier'); module.exports.validate = function({ resolvedSpec, isOAS3 }, config) { - const result = {}; - result.error = []; - result.warning = []; + const messages = new MessageCarrier(); config = config.security_definitions; @@ -99,35 +98,31 @@ module.exports.validate = function({ resolvedSpec, isOAS3 }, config) { // check what has been used and what has not been each(definedSchemes, (info, name) => { if (info.used === false) { - const checkStatus = config.unused_security_schemes; - if (checkStatus !== 'off') { - const location = isOAS3 - ? 'components.securitySchemes' - : 'securityDefinitions'; - result[checkStatus].push({ - path: `${location}.${name}`, - message: `A security scheme is defined but never used: ${name}` - }); - } + const location = isOAS3 + ? 'components.securitySchemes' + : 'securityDefinitions'; + messages.addMessage( + `${location}.${name}`, + `A security scheme is defined but never used: ${name}`, + config.unused_security_schemes + ); } }); each(definedScopes, (info, name) => { if (info.used === false) { - const checkStatus = config.unused_security_scopes; - if (checkStatus !== 'off') { - const path = isOAS3 - ? `components.securitySchemes.${info.scheme}.flows.${ - info.flow - }.scopes.${name}` - : `securityDefinitions.${info.scheme}.scopes.${name}`; - result[checkStatus].push({ - path, - message: `A security scope is defined but never used: ${name}` - }); - } + const path = isOAS3 + ? `components.securitySchemes.${info.scheme}.flows.${ + info.flow + }.scopes.${name}` + : `securityDefinitions.${info.scheme}.scopes.${name}`; + messages.addMessage( + path, + `A security scope is defined but never used: ${name}`, + config.unused_security_scopes + ); } }); - return { errors: result.error, warnings: result.warning }; + return messages; }; diff --git a/src/plugins/validation/2and3/semantic-validators/security-ibm.js b/src/plugins/validation/2and3/semantic-validators/security-ibm.js index 1c53c448a..7f4660b1f 100644 --- a/src/plugins/validation/2and3/semantic-validators/security-ibm.js +++ b/src/plugins/validation/2and3/semantic-validators/security-ibm.js @@ -11,11 +11,10 @@ // Items in `security` must match a `securityDefinition`. const each = require('lodash/each'); +const MessageCarrier = require('../../../utils/messageCarrier'); module.exports.validate = function({ jsSpec, isOAS3 }, config) { - const result = {}; - result.error = []; - result.warning = []; + const messages = new MessageCarrier(); config = config.security; @@ -67,10 +66,11 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) { // ensure the security scheme is defined if (!schemeIsDefined) { - result.error.push({ - path: `${path}.${schemeName}`, - message: 'security requirements must match a security definition' - }); + messages.addMessage( + `${path}.${schemeName}`, + 'security requirements must match a security definition', + 'error' + ); } else { const schemeType = securityDefinitions[schemeName].type; const isNonEmptyArray = schemeObject[schemeName].length > 0; @@ -83,15 +83,13 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) { ); if (isNonEmptyArray && !isSchemeWithNonEmptyArray) { - const checkStatus = config.invalid_non_empty_security_array; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path: `${path}.${schemeName}`, - message: `For security scheme types other than ${schemesWithNonEmptyArrays.join( - ' or ' - )}, the value must be an empty array.` - }); - } + messages.addMessage( + `${path}.${schemeName}`, + `For security scheme types other than ${schemesWithNonEmptyArrays.join( + ' or ' + )}, the value must be an empty array.`, + config.invalid_non_empty_security_array + ); } if (isSchemeWithNonEmptyArray) { @@ -105,10 +103,11 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) { ? checkOAS3Scopes(scope, securityDefinition) : checkSwagger2Scopes(scope, securityDefinition); if (!scopeIsDefined) { - result.error.push({ - message: `Definition could not be resolved for security scope: ${scope}`, - path: `${path}.${schemeName}.${i}` - }); + messages.addMessage( + `${path}.${schemeName}.${i}`, + `Definition could not be resolved for security scope: ${scope}`, + 'error' + ); } }); } @@ -118,7 +117,7 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) { }); } - return { errors: result.error, warnings: result.warning }; + return messages; }; // return true if scope is defined diff --git a/src/plugins/validation/2and3/semantic-validators/walker-ibm.js b/src/plugins/validation/2and3/semantic-validators/walker-ibm.js index e8f0f65e1..2e2a17694 100644 --- a/src/plugins/validation/2and3/semantic-validators/walker-ibm.js +++ b/src/plugins/validation/2and3/semantic-validators/walker-ibm.js @@ -6,12 +6,11 @@ const at = require('lodash/at'); const { walk } = require('../../../utils'); +const MessageCarrier = require('../../../utils/messageCarrier'); // Walks an entire spec. module.exports.validate = function({ jsSpec, resolvedSpec }, config) { - const result = {}; - result.error = []; - result.warning = []; + const messages = new MessageCarrier(); config = config.walker; @@ -22,13 +21,11 @@ module.exports.validate = function({ jsSpec, resolvedSpec }, config) { // verify description is not empty if (description.length === 0 || !description.trim()) { - const checkStatus = config.no_empty_descriptions; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path: [...path, 'description'], - message: 'Items with a description must have content in it.' - }); - } + messages.addMessage( + [...path, 'description'], + 'Items with a description must have content in it.', + config.no_empty_descriptions + ); } // check description siblings to $refs @@ -41,14 +38,11 @@ module.exports.validate = function({ jsSpec, resolvedSpec }, config) { referencedSchema.description && referencedSchema.description === description ) { - const checkStatus = config.duplicate_sibling_description; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path: [...path, 'description'], - message: - 'Description sibling to $ref matches that of the referenced schema. This is redundant and should be removed.' - }); - } + messages.addMessage( + [...path, 'description'], + 'Description sibling to $ref matches that of the referenced schema. This is redundant and should be removed.', + config.duplicate_sibling_description + ); } } } @@ -56,13 +50,14 @@ module.exports.validate = function({ jsSpec, resolvedSpec }, config) { // check for and flag null values - they are not allowed by the spec and are likely mistakes Object.keys(obj).forEach(key => { if (obj[key] === null) { - result.error.push({ - path: [...path, key], - message: 'Null values are not allowed for any property.' - }); + messages.addMessage( + [...path, key], + 'Null values are not allowed for any property.', + 'error' + ); } }); }); - return { errors: result.error, warnings: result.warning }; + return messages; }; diff --git a/src/plugins/validation/2and3/semantic-validators/walker.js b/src/plugins/validation/2and3/semantic-validators/walker.js index b74bec8a9..b859a47b9 100644 --- a/src/plugins/validation/2and3/semantic-validators/walker.js +++ b/src/plugins/validation/2and3/semantic-validators/walker.js @@ -13,11 +13,10 @@ const match = require('matcher'); const { walk } = require('../../../utils'); +const MessageCarrier = require('../../../utils/messageCarrier'); module.exports.validate = function({ jsSpec, isOAS3 }, config) { - const result = {}; - result.error = []; - result.warning = []; + const messages = new MessageCarrier(); config = config.walker; @@ -45,10 +44,11 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) { ///// "type" should always have a string-type value, everywhere. if (obj.type && allowedParents.indexOf(path[path.length - 1]) === -1) { if (typeof obj.type !== 'string') { - result.error.push({ - path: [...path, 'type'], - message: '"type" should be a string' - }); + messages.addMessage( + [...path, 'type'], + '"type" should be a string', + 'error' + ); } } @@ -56,28 +56,31 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) { if (obj.maximum && obj.minimum) { if (greater(obj.minimum, obj.maximum)) { - result.error.push({ - path: path.concat(['minimum']), - message: 'Minimum cannot be more than maximum' - }); + messages.addMessage( + path.concat(['minimum']), + 'Minimum cannot be more than maximum', + 'error' + ); } } if (obj.maxProperties && obj.minProperties) { if (greater(obj.minProperties, obj.maxProperties)) { - result.error.push({ - path: path.concat(['minProperties']), - message: 'minProperties cannot be more than maxProperties' - }); + messages.addMessage( + path.concat(['minProperties']), + 'minProperties cannot be more than maxProperties', + 'error' + ); } } if (obj.maxLength && obj.minLength) { if (greater(obj.minLength, obj.maxLength)) { - result.error.push({ - path: path.concat(['minLength']), - message: 'minLength cannot be more than maxLength' - }); + messages.addMessage( + path.concat(['minLength']), + 'minLength cannot be more than maxLength', + 'error' + ); } } @@ -90,33 +93,29 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) { if (refBlacklist && refBlacklist.length && matches.length) { // Assertation 2 // use the slice(1) to remove the `!` negator from the string - const checkStatus = config.incorrect_ref_pattern; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path: [...path, '$ref'], - message: `${ - blacklistPayload.location - } $refs must follow this format: ${refBlacklist[0].slice(1)}` - }); - } + messages.addMessage( + [...path, '$ref'], + `${ + blacklistPayload.location + } $refs must follow this format: ${refBlacklist[0].slice(1)}`, + config.incorrect_ref_pattern + ); } } const keys = Object.keys(obj); keys.forEach(k => { if (keys.indexOf('$ref') > -1 && k !== '$ref') { - const checkStatus = config.$ref_siblings; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path: path.concat([k]), - message: 'Values alongside a $ref will be ignored.' - }); - } + messages.addMessage( + path.concat([k]), + 'Values alongside a $ref will be ignored.', + config.$ref_siblings + ); } }); }); - return { errors: result.error, warnings: result.warning }; + return messages; }; // values are globs! diff --git a/src/plugins/validation/oas3/semantic-validators/discriminator.js b/src/plugins/validation/oas3/semantic-validators/discriminator.js index dbed0ac7f..3abb38955 100644 --- a/src/plugins/validation/oas3/semantic-validators/discriminator.js +++ b/src/plugins/validation/oas3/semantic-validators/discriminator.js @@ -13,10 +13,10 @@ const each = require('lodash/each'); const has = require('lodash/has'); const get = require('lodash/get'); +const MessageCarrier = require('../../../utils/messageCarrier'); module.exports.validate = function({ jsSpec }) { - const errors = []; - const warnings = []; + const messages = new MessageCarrier(); const schemas = get(jsSpec, ['components', 'schemas'], []); @@ -29,47 +29,48 @@ module.exports.validate = function({ jsSpec }) { // If discriminator is not an object, error out and return if (typeof discriminator === 'object') { if (!has(discriminator, 'propertyName')) { - errors.push({ - path: basePath.concat([schemaName, 'discriminator']).join('.'), - message: - 'Discriminator must be of type object with field name propertyName' - }); + messages.addMessage( + basePath.concat([schemaName, 'discriminator']).join('.'), + 'Discriminator must be of type object with field name propertyName', + 'error' + ); return; } } else { - errors.push({ - path: basePath.concat([schemaName, 'discriminator']).join('.'), - message: 'Discriminator must be of type object' - }); + messages.addMessage( + basePath.concat([schemaName, 'discriminator']).join('.'), + 'Discriminator must be of type object', + 'error' + ); return; } // If discriminator propertyName is not a string, error out and return const { propertyName } = discriminator; if (typeof propertyName !== 'string') { - errors.push({ - path: basePath + messages.addMessage( + basePath .concat([schemaName, 'discriminator', 'propertyName']) .join('.'), - message: - '`propertyName` inside discriminator object must be of type string' - }); + '`propertyName` inside discriminator object must be of type string', + 'error' + ); return; } // If the schema's property doesn't include propertyName defined in discriminator, error out and return const { properties } = schema; if (!has(properties, propertyName)) { - errors.push({ - path: basePath + messages.addMessage( + basePath .concat([schemaName, 'discriminator', 'propertyName']) .join('.'), - message: - 'The discriminator property name used must be defined in this schema' - }); + 'The discriminator property name used must be defined in this schema', + 'error' + ); return; } } }); - return { errors, warnings }; + return messages; }; diff --git a/src/plugins/validation/oas3/semantic-validators/dummy-ibm.js b/src/plugins/validation/oas3/semantic-validators/dummy-ibm.js index 9dfc1dcda..0fb39a870 100644 --- a/src/plugins/validation/oas3/semantic-validators/dummy-ibm.js +++ b/src/plugins/validation/oas3/semantic-validators/dummy-ibm.js @@ -1,10 +1,10 @@ /* eslint-disable */ +const MessageCarrier = require('../../../utils/messageCarrier'); + module.exports.validate = function({ jsSpec }, config) { - let result = {} - result.error = [] - result.warning = [] + const messages = new MessageCarrier(); // use the appropriate validation category object // ex) `config = config.operations` for the operations validator @@ -22,14 +22,12 @@ module.exports.validate = function({ jsSpec }, config) { // error pushing format: /* - let checkStatus = config.custom_rule_name - if (checkStatus !== "off") { - result[checkStatus].push({ - path: "path to error, either as an array or a string", - message: "message about the error/warning" - }) - } + messages.addMessage( + path to error either as an array or string, + message about the error/warning, + config.custom_rule_name OR 'error' OR 'warning + ) */ - return { errors: result.error, warnings: result.warning } + return messages; } diff --git a/src/plugins/validation/oas3/semantic-validators/openapi.js b/src/plugins/validation/oas3/semantic-validators/openapi.js index 6ddb3bb09..3f064ef12 100644 --- a/src/plugins/validation/oas3/semantic-validators/openapi.js +++ b/src/plugins/validation/oas3/semantic-validators/openapi.js @@ -7,9 +7,10 @@ // Assertation 3: // make sure the string follows semantic versioning 2.0.0 +const MessageCarrier = require('../../../utils/messageCarrier'); + module.exports.validate = function({ jsSpec }) { - const errors = []; - const warnings = []; + const messages = new MessageCarrier(); // Regex taken from Semantic Versioning 2.0.0 documentation to check if string follows Semantic Versioning // https://semver.org/ @@ -22,20 +23,23 @@ module.exports.validate = function({ jsSpec }) { const openapi = jsSpec.openapi; if (!openapi) { - errors.push({ - path: ['openapi'], - message: 'API definition must have an `openapi` field' - }); + messages.addMessage( + ['openapi'], + 'API definition must have an `openapi` field', + 'error' + ); } else if (typeof openapi !== 'string') { - errors.push({ - path: ['openapi'], - message: 'API definition must have an `openapi` field as type string' - }); + messages.addMessage( + ['openapi'], + 'API definition must have an `openapi` field as type string', + 'error' + ); } else if (!openapi.match(semverRegex)) { - errors.push({ - path: ['openapi'], - message: '`openapi` string must follow Semantic Versioning 2.0.0' - }); + messages.addMessage( + ['openapi'], + '`openapi` string must follow Semantic Versioning 2.0.0', + 'error' + ); } - return { errors, warnings }; + return messages; }; diff --git a/src/plugins/validation/oas3/semantic-validators/operations.js b/src/plugins/validation/oas3/semantic-validators/operations.js index 33aaac200..78c57825d 100644 --- a/src/plugins/validation/oas3/semantic-validators/operations.js +++ b/src/plugins/validation/oas3/semantic-validators/operations.js @@ -10,13 +10,12 @@ const pick = require('lodash/pick'); const each = require('lodash/each'); const { hasRefProperty } = require('../../../utils'); +const MessageCarrier = require('../../../utils/messageCarrier'); const findOctetSequencePaths = require('../../../utils/findOctetSequencePaths') .findOctetSequencePaths; module.exports.validate = function({ resolvedSpec, jsSpec }, config) { - const result = {}; - result.error = []; - result.warning = []; + const messages = new MessageCarrier(); const configSchemas = config.schemas; config = config.operations; @@ -39,13 +38,11 @@ module.exports.validate = function({ resolvedSpec, jsSpec }, config) { const requestBodyMimeTypes = op.requestBody.content && Object.keys(requestBodyContent); if (!requestBodyContent || !requestBodyMimeTypes.length) { - const checkStatus = config.no_request_body_content; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path: `paths.${pathName}.${opName}.requestBody`, - message: 'Request bodies MUST specify a `content` property' - }); - } + messages.addMessage( + `paths.${pathName}.${opName}.requestBody`, + 'Request bodies MUST specify a `content` property', + config.no_request_body_content + ); } else { // request body has content const firstMimeType = requestBodyMimeTypes[0]; // code generation uses the first mime type @@ -78,15 +75,11 @@ module.exports.validate = function({ resolvedSpec, jsSpec }, config) { !hasReferencedRequestBody && !hasRequestBodyName ) { - const checkStatus = config.no_request_body_name; - if (checkStatus != 'off') { - const message = - 'Operations with non-form request bodies should set a name with the x-codegen-request-body-name annotation.'; - result[checkStatus].push({ - path: `paths.${pathName}.${opName}`, - message - }); - } + messages.addMessage( + `paths.${pathName}.${opName}`, + 'Operations with non-form request bodies should set a name with the x-codegen-request-body-name annotation.', + config.no_request_body_name + ); } // Assertation 3 @@ -99,13 +92,12 @@ module.exports.validate = function({ resolvedSpec, jsSpec }, config) { requestBodyContent[mimeType].schema, schemaPath ); - const message = - 'JSON request/response bodies should not contain binary (type: string, format: binary) values.'; for (const p of octetSequencePaths) { - result[binaryStringStatus].push({ - path: p, - message - }); + messages.addMessage( + p, + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.', + binaryStringStatus + ); } } } @@ -115,7 +107,7 @@ module.exports.validate = function({ resolvedSpec, jsSpec }, config) { }); }); - return { errors: result.error, warnings: result.warning }; + return messages; }; function isFormParameter(mimeType) { diff --git a/src/plugins/validation/oas3/semantic-validators/pagination-ibm.js b/src/plugins/validation/oas3/semantic-validators/pagination-ibm.js index 9fdabc762..68fb442f4 100644 --- a/src/plugins/validation/oas3/semantic-validators/pagination-ibm.js +++ b/src/plugins/validation/oas3/semantic-validators/pagination-ibm.js @@ -16,15 +16,16 @@ // - If the operation has an `offset` query parameter, the response body must contain an `offset` property this is type integer and required // - The response body must contain an array property with the same plural resource name appearing in the collection’s URL. +const MessageCarrier = require('../../../utils/messageCarrier'); + module.exports.validate = function({ resolvedSpec }, config) { - const result = {}; - result.error = []; - result.warning = []; + const messages = new MessageCarrier(); + const checkStatus = config.pagination.pagination_style; //when pagnation is turned off, skip all of the pagination checks if (checkStatus == 'off') { - return { errors: [], warnings: [] }; + return messages; } // Loop through all paths looking for "list" operations. @@ -87,11 +88,11 @@ module.exports.validate = function({ resolvedSpec }, config) { !limitParam.schema.default || !limitParam.schema.maximum ) { - result[checkStatus].push({ - path: ['paths', path, 'get', 'parameters', limitParamIndex], - message: - 'The limit parameter must be of type integer and optional with default and maximum values.' - }); + messages.addMessage( + ['paths', path, 'get', 'parameters', limitParamIndex], + 'The limit parameter must be of type integer and optional with default and maximum values.', + checkStatus + ); } // - If the operation has an `offset` query parameter, it must be type integer and optional @@ -106,10 +107,11 @@ module.exports.validate = function({ resolvedSpec }, config) { offsetParam.schema.type !== 'integer' || !!offsetParam.required ) { - result[checkStatus].push({ - path: ['paths', path, 'get', 'parameters', offsetParamIndex], - message: 'The offset parameter must be of type integer and optional.' - }); + messages.addMessage( + ['paths', path, 'get', 'parameters', offsetParamIndex], + 'The offset parameter must be of type integer and optional.', + checkStatus + ); } } @@ -127,12 +129,13 @@ module.exports.validate = function({ resolvedSpec }, config) { startParam.schema.type !== 'string' || !!startParam.required ) { - result[checkStatus].push({ - path: ['paths', path, 'get', 'parameters', startParamIndex], - message: `The ${ + messages.addMessage( + ['paths', path, 'get', 'parameters', startParamIndex], + `The ${ startParam.name - } parameter must be of type string and optional.` - }); + } parameter must be of type string and optional.`, + checkStatus + ); } } @@ -152,19 +155,21 @@ module.exports.validate = function({ resolvedSpec }, config) { const limitProp = jsonResponse.schema.properties.limit; if (!limitProp) { - result[checkStatus].push({ - path: propertiesPath, - message: `A paginated list operation must include a "limit" property in the response body schema.` - }); + messages.addMessage( + propertiesPath, + `A paginated list operation must include a "limit" property in the response body schema.`, + checkStatus + ); } else if ( limitProp.type !== 'integer' || !jsonResponse.schema.required || jsonResponse.schema.required.indexOf('limit') === -1 ) { - result[checkStatus].push({ - path: [...propertiesPath, 'limit'], - message: `The "limit" property in the response body of a paginated list operation must be of type integer and required.` - }); + messages.addMessage( + [...propertiesPath, 'limit'], + `The "limit" property in the response body of a paginated list operation must be of type integer and required.`, + checkStatus + ); } // - If the operation has an `offset` query parameter, the response body must contain an `offset` property this is type integer and required @@ -172,19 +177,21 @@ module.exports.validate = function({ resolvedSpec }, config) { if (offsetParamIndex !== -1) { const offsetProp = jsonResponse.schema.properties.offset; if (!offsetProp) { - result[checkStatus].push({ - path: propertiesPath, - message: `A paginated list operation with an "offset" parameter must include an "offset" property in the response body schema.` - }); + messages.addMessage( + propertiesPath, + `A paginated list operation with an "offset" parameter must include an "offset" property in the response body schema.`, + checkStatus + ); } else if ( offsetProp.type !== 'integer' || !jsonResponse.schema.required || jsonResponse.schema.required.indexOf('offset') === -1 ) { - result[checkStatus].push({ - path: [...propertiesPath, 'offset'], - message: `The "offset" property in the response body of a paginated list operation must be of type integer and required.` - }); + messages.addMessage( + [...propertiesPath, 'offset'], + `The "offset" property in the response body of a paginated list operation must be of type integer and required.`, + checkStatus + ); } } @@ -193,12 +200,13 @@ module.exports.validate = function({ resolvedSpec }, config) { const pluralResourceName = path.split('/').pop(); const resourcesProp = jsonResponse.schema.properties[pluralResourceName]; if (!resourcesProp || resourcesProp.type !== 'array') { - result[checkStatus].push({ - path: propertiesPath, - message: `A paginated list operation must include an array property whose name matches the final segment of the path.` - }); + messages.addMessage( + propertiesPath, + `A paginated list operation must include an array property whose name matches the final segment of the path.`, + checkStatus + ); } } - return { errors: result.error, warnings: result.warning }; + return messages; }; diff --git a/src/plugins/validation/oas3/semantic-validators/parameters.js b/src/plugins/validation/oas3/semantic-validators/parameters.js index c5919bddf..7d42f3094 100644 --- a/src/plugins/validation/oas3/semantic-validators/parameters.js +++ b/src/plugins/validation/oas3/semantic-validators/parameters.js @@ -12,13 +12,12 @@ // defined way to encode an octet sequence in a URL. const { isParameterObject, walk } = require('../../../utils'); +const MessageCarrier = require('../../../utils/messageCarrier'); const findOctetSequencePaths = require('../../../utils/findOctetSequencePaths') .findOctetSequencePaths; module.exports.validate = function({ jsSpec }, config) { - const result = {}; - result.error = []; - result.warning = []; + const messages = new MessageCarrier(); const configSchemas = config.schemas; config = config.parameters; @@ -32,46 +31,36 @@ module.exports.validate = function({ jsSpec }, config) { const allowedInValues = ['query', 'header', 'path', 'cookie']; if (!obj.in) { // bad because in is required - const checkStatus = config.no_in_property; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path, - message: 'Parameters MUST have an `in` property.' - }); - } + messages.addMessage( + path, + 'Parameters MUST have an `in` property.', + config.no_in_property + ); } else if (!allowedInValues.includes(obj.in)) { // bad because `in` must be one of a few values - const checkStatus = config.invalid_in_property; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path: path.concat('in'), - message: `Unsupported value for \`in\`: '${ - obj.in - }'. Allowed values are ${allowedInValues.join(', ')}` - }); - } + messages.addMessage( + path.concat('in'), + `Unsupported value for \`in\`: '${ + obj.in + }'. Allowed values are ${allowedInValues.join(', ')}`, + config.invalid_in_property + ); } if (!obj.schema && !obj.content) { // bad because at least one is needed - const checkStatus = config.missing_schema_or_content; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path, - message: - 'Parameters MUST have their data described by either `schema` or `content`.' - }); - } + messages.addMessage( + path, + 'Parameters MUST have their data described by either `schema` or `content`.', + config.missing_schema_or_content + ); } else if (obj.schema && obj.content) { // bad because only one is allowed to be used at a time - const checkStatus = config.has_schema_and_content; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path, - message: - 'Parameters MUST NOT have both a `schema` and `content` property.' - }); - } + messages.addMessage( + path, + 'Parameters MUST NOT have both a `schema` and `content` property.', + config.has_schema_and_content + ); } const binaryStringStatus = configSchemas.json_or_param_binary_string; @@ -99,16 +88,14 @@ module.exports.validate = function({ jsSpec }, config) { } for (const p of octetSequencePaths) { - const message = - 'Parameters should not contain binary (type: string, format: binary) values.'; - result[binaryStringStatus].push({ - path: p, - message - }); + messages.addMessage( + p, + 'Parameters should not contain binary (type: string, format: binary) values.', + binaryStringStatus + ); } } } }); - - return { errors: result.error, warnings: result.warning }; + return messages; }; diff --git a/src/plugins/validation/oas3/semantic-validators/responses.js b/src/plugins/validation/oas3/semantic-validators/responses.js index 5f8de7f42..459ef0fec 100644 --- a/src/plugins/validation/oas3/semantic-validators/responses.js +++ b/src/plugins/validation/oas3/semantic-validators/responses.js @@ -16,13 +16,12 @@ // type: string, format: binary. const { walk } = require('../../../utils'); +const MessageCarrier = require('../../../utils/messageCarrier'); const findOctetSequencePaths = require('../../../utils/findOctetSequencePaths') .findOctetSequencePaths; module.exports.validate = function({ resolvedSpec }, config) { - const result = {}; - result.error = []; - result.warning = []; + const messages = new MessageCarrier(); const configSchemas = config.schemas; config = config.responses; @@ -38,56 +37,46 @@ module.exports.validate = function({ resolvedSpec }, config) { if (binaryStringStatus !== 'off') { validateNoBinaryStringsInResponse( obj, - result, + messages, path, binaryStringStatus ); } if (!statusCodes.length) { - const message = - 'Each `responses` object MUST have at least one response code.'; - const checkStatus = config.no_response_codes; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path, - message - }); - } + messages.addMessage( + path, + 'Each `responses` object MUST have at least one response code.', + config.no_response_codes + ); } else if (!successCodes.length) { - const message = - 'Each `responses` object SHOULD have at least one code for a successful response.'; - const checkStatus = config.no_success_response_codes; - if (checkStatus !== 'off') { - result[checkStatus].push({ - path, - message - }); - } + messages.addMessage( + path, + 'Each `responses` object SHOULD have at least one code for a successful response.', + config.no_success_response_codes + ); } else { // validate success codes for (const successCode of successCodes) { if (successCode !== '204' && !obj[successCode].content) { - const checkStatus = config.no_response_body; - if (checkStatus !== 'off') { - const message = `A ${successCode} response should include a response body. Use 204 for responses without content.`; - result[checkStatus].push({ - path: path.concat([successCode]), - message - }); - } + messages.addMessage( + path.concat([successCode]), + `A ${successCode} response should include a response body. Use 204 for responses without content.`, + config.no_response_body + ); } else if (successCode === '204' && obj[successCode].content) { - result.error.push({ - path: path.concat(['204', 'content']), - message: `A 204 response MUST NOT include a message-body.` - }); + messages.addMessage( + path.concat(['204', 'content']), + `A 204 response MUST NOT include a message-body.`, + 'error' + ); } } } } }); - return { errors: result.error, warnings: result.warning }; + return messages; }; function getResponseCodes(responseObj) { @@ -105,7 +94,7 @@ function isStatusCode(code) { function validateNoBinaryStringsInResponse( responseObj, - result, + messages, path, binaryStringStatus ) { @@ -124,13 +113,12 @@ function validateNoBinaryStringsInResponse( responseBodyContent[mimeType].schema, schemaPath ); - const message = - 'JSON request/response bodies should not contain binary (type: string, format: binary) values.'; for (const p of octetSequencePaths) { - result[binaryStringStatus].push({ - path: p, - message - }); + messages.addMessage( + p, + 'JSON request/response bodies should not contain binary (type: string, format: binary) values.', + binaryStringStatus + ); } } }); diff --git a/src/plugins/validation/oas3/semantic-validators/security-definitions-ibm.js b/src/plugins/validation/oas3/semantic-validators/security-definitions-ibm.js index 91fdf3897..a3728b0c7 100644 --- a/src/plugins/validation/oas3/semantic-validators/security-definitions-ibm.js +++ b/src/plugins/validation/oas3/semantic-validators/security-definitions-ibm.js @@ -21,15 +21,16 @@ // Assertation 6: `opedIdConnectUrl` property is required for `openIdConnect` and must be a valid url const stringValidator = require('validator'); +const MessageCarrier = require('../../../utils/messageCarrier'); module.exports.validate = function({ resolvedSpec }) { + const messages = new MessageCarrier(); + const API_KEY = 'apiKey'; const OAUTH2 = 'oauth2'; const HTTP = 'http'; const OPENID_CONNECT = 'openIdConnect'; const authTypes = [API_KEY, HTTP, OAUTH2, OPENID_CONNECT]; - const errors = []; - const warnings = []; const securitySchemes = resolvedSpec.components && resolvedSpec.components.securitySchemes; @@ -39,32 +40,33 @@ module.exports.validate = function({ resolvedSpec }) { const type = security.type; if (!type) { - errors.push({ - message: 'security scheme is missing required field `type`', - path - }); + messages.addMessage( + path, + 'security scheme is missing required field `type`', + 'error' + ); } else if (authTypes.indexOf(type) === -1) { - errors.push({ - message: - '`type` must have one of the following types: `apiKey`, `oauth2`, `http`, `openIdConnect`', - path: path + '.type' - }); + messages.addMessage( + path + '.type', + '`type` must have one of the following types: `apiKey`, `oauth2`, `http`, `openIdConnect`', + 'error' + ); } else if (type === API_KEY) { //apiKey validation const authIn = security.in; if (!authIn || !['query', 'header', 'cookie'].includes(authIn)) { - errors.push({ - message: - "apiKey authorization must have required 'in' property, valid values are 'query' or 'header' or 'cookie'.", - path: path + '.in' - }); + messages.addMessage( + path + '.in', + "apiKey authorization must have required 'in' property, valid values are 'query' or 'header' or 'cookie'.", + 'error' + ); } if (!security.name) { - errors.push({ - message: - "apiKey authorization must have required 'name' string property. The name of the header or query property to be used.", - path - }); + messages.addMessage( + path, + "apiKey authorization must have required 'name' string property. The name of the header or query property to be used.", + 'error' + ); } } // oauth2 validation @@ -72,89 +74,91 @@ module.exports.validate = function({ resolvedSpec }) { const flows = security.flows; if (!flows) { - errors.push({ - message: "oauth2 authorization must have required 'flows' property", - path - }); + messages.addMessage( + path, + "oauth2 authorization must have required 'flows' property", + 'error' + ); } else if (flows.authorizationCode && !flows.authorizationCode.tokenUrl) { - errors.push({ - message: - "flow must have required 'tokenUrl' property if type is `authorizationCode`", - path: path + '.flows.authorizationCode' - }); + messages.addMessage( + path + '.flows.authorizationCode', + "flow must have required 'tokenUrl' property if type is `authorizationCode`", + 'error' + ); } else if (flows.password && !flows.password.tokenUrl) { - errors.push({ - message: - "flow must have required 'tokenUrl' property if type is `password`", - path: path + '.flows.password' - }); + messages.addMessage( + path + '.flows.password', + "flow must have required 'tokenUrl' property if type is `password`", + 'error' + ); } else if (flows.clientCredentials && !flows.clientCredentials.tokenUrl) { - errors.push({ - message: - "flow must have required 'tokenUrl' property if type is `clientCredentials`", - path: path + '.flows.clientCredentials' - }); + messages.addMessage( + path + '.flows.clientCredentials', + "flow must have required 'tokenUrl' property if type is `clientCredentials`", + 'error' + ); } else if ( !flows.implicit && !flows.authorizationCode && !flows.password && !flows.clientCredentials ) { - errors.push({ - message: - "oauth2 authorization `flows` must have one of the following properties: 'implicit', 'password', 'clientCredentials' or 'authorizationCode'", - path: path + '.flows' - }); + messages.addMessage( + path + '.flows', + "oauth2 authorization `flows` must have one of the following properties: 'implicit', 'password', 'clientCredentials' or 'authorizationCode'", + 'error' + ); } else if (flows.implicit) { const authorizationUrl = flows.implicit.authorizationUrl; if (!authorizationUrl) { - errors.push({ - message: - "oauth2 implicit flow must have required 'authorizationUrl' property", - path: path + '.flows.implicit' - }); + messages.addMessage( + path + '.flows.implicit', + "oauth2 implicit flow must have required 'authorizationUrl' property", + 'error' + ); } if (!flows.implicit.scopes) { - errors.push({ - message: - "oauth2 authorization implicit flow must have required 'scopes' property.", - path: path + '.flows.implicit' - }); + messages.addMessage( + path + '.flows.implicit', + "oauth2 authorization implicit flow must have required 'scopes' property.", + 'error' + ); } } else if (flows.authorizationCode) { const authorizationUrl = flows.authorizationCode.authorizationUrl; if (!authorizationUrl) { - errors.push({ - message: - "oauth2 authorizationCode flow must have required 'authorizationUrl' property.", - path: path + 'flows.authorizationCode' - }); + messages.addMessage( + path + 'flows.authorizationCode', + "oauth2 authorizationCode flow must have required 'authorizationUrl' property.", + 'error' + ); } } else if (flows.password) { const tokenUrl = flows.password.tokenUrl; if (!tokenUrl) { - errors.push({ - message: - "oauth2 authorization password flow must have required 'tokenUrl' property.", - path: path + '.flows.password' - }); + messages.addMessage( + path + '.flows.password', + "oauth2 authorization password flow must have required 'tokenUrl' property.", + 'error' + ); } } else if (flows.clientCredentials) { if (!flows.clientCredentials.tokenUrl) { - errors.push({ - message: - "oauth2 authorization clientCredentials flow must have required 'tokenUrl' property.", - path: path + '.flows.clientCredentials' - }); + messages.addMessage( + path + '.flows.clientCredentials', + "oauth2 authorization clientCredentials flow must have required 'tokenUrl' property.", + 'error' + ); } } } else if (type === HTTP) { //scheme is required if (!security.scheme) { - errors.push({ - message: 'scheme must be defined for type `http`', - path - }); + messages.addMessage( + path, + 'scheme must be defined for type `http`', + 'error' + ); } } else if (type == OPENID_CONNECT) { const openIdConnectURL = security.openIdConnectUrl; @@ -163,14 +167,14 @@ module.exports.validate = function({ resolvedSpec }) { typeof openIdConnectURL !== 'string' || !stringValidator.isurl(openIdConnectURL) ) { - errors.push({ - message: - 'openIdConnectUrl must be defined for openIdConnect property and must be a valid URL', - path - }); + messages.addMessage( + path, + 'openIdConnectUrl must be defined for openIdConnect property and must be a valid URL', + 'error' + ); } } } - return { errors, warnings }; + return messages; }; diff --git a/src/plugins/validation/swagger2/semantic-validators/discriminator.js b/src/plugins/validation/swagger2/semantic-validators/discriminator.js index bd4fe9be0..aad4ee36b 100644 --- a/src/plugins/validation/swagger2/semantic-validators/discriminator.js +++ b/src/plugins/validation/swagger2/semantic-validators/discriminator.js @@ -14,10 +14,10 @@ const each = require('lodash/each'); const has = require('lodash/has'); const get = require('lodash/get'); const includes = require('lodash/includes'); +const MessageCarrier = require('../../../utils/messageCarrier'); module.exports.validate = function({ jsSpec }) { - const errors = []; - const warnings = []; + const messages = new MessageCarrier(); const schemas = get(jsSpec, ['definitions'], []); @@ -29,21 +29,22 @@ module.exports.validate = function({ jsSpec }) { // If discriminator is not an string, error out and return if (typeof discriminator !== 'string') { - errors.push({ - path: basePath.concat([schemaName, 'discriminator']).join('.'), - message: 'Discriminator must be of type string' - }); + messages.addMessage( + basePath.concat([schemaName, 'discriminator']).join('.'), + 'Discriminator must be of type string', + 'error' + ); return; } // If the schema's property doesn't include property defined in discriminator, error out and return const { properties } = schema; if (!has(properties, discriminator)) { - errors.push({ - path: basePath.concat([schemaName, 'discriminator']).join('.'), - message: - 'The discriminator defined must also be defined as a property in this schema' - }); + messages.addMessage( + basePath.concat([schemaName, 'discriminator']).join('.'), + 'The discriminator defined must also be defined as a property in this schema', + 'error' + ); return; } @@ -51,31 +52,33 @@ module.exports.validate = function({ jsSpec }) { const { required } = schema; if (!required) { - errors.push({ - path: basePath.concat([schemaName]).join('.'), - message: - 'Required array not found. The discriminator defined must also be part of required properties' - }); + messages.addMessage( + basePath.concat([schemaName]).join('.'), + 'Required array not found. The discriminator defined must also be part of required properties', + 'error' + ); return; } // required must be an array if (!(required instanceof Array)) { - errors.push({ - path: basePath.concat([schemaName, 'required']).join('.'), - message: 'Required must be an array' - }); + messages.addMessage( + basePath.concat([schemaName, 'required']).join('.'), + 'Required must be an array', + 'error' + ); return; } // discriminator must be in required if (!includes(required, discriminator)) { - errors.push({ - path: basePath.concat([schemaName, 'required']).join('.'), - message: 'Discriminator is not listed as part of required' - }); + messages.addMessage( + basePath.concat([schemaName, 'required']).join('.'), + 'Discriminator is not listed as part of required', + 'error' + ); } } }); - return { errors, warnings }; + return messages; }; diff --git a/src/plugins/validation/swagger2/semantic-validators/dummy-ibm.js b/src/plugins/validation/swagger2/semantic-validators/dummy-ibm.js index 9dfc1dcda..bcbf4f53b 100644 --- a/src/plugins/validation/swagger2/semantic-validators/dummy-ibm.js +++ b/src/plugins/validation/swagger2/semantic-validators/dummy-ibm.js @@ -1,10 +1,10 @@ /* eslint-disable */ +const MessageCarrier = require('../../../utils/messageCarrier'); + module.exports.validate = function({ jsSpec }, config) { - let result = {} - result.error = [] - result.warning = [] + const messages = new MessageCarrier(); // use the appropriate validation category object // ex) `config = config.operations` for the operations validator @@ -31,5 +31,5 @@ module.exports.validate = function({ jsSpec }, config) { } */ - return { errors: result.error, warnings: result.warning } + return messages; } diff --git a/src/plugins/validation/swagger2/semantic-validators/dummy.js b/src/plugins/validation/swagger2/semantic-validators/dummy.js index fbacf033c..6d69bc9e3 100644 --- a/src/plugins/validation/swagger2/semantic-validators/dummy.js +++ b/src/plugins/validation/swagger2/semantic-validators/dummy.js @@ -1,9 +1,11 @@ /* eslint-disable */ + +const MessageCarrier = require('../../../utils/messageCarrier'); + module.exports.validate = function({ jsSpec }) { - let errors = [] - let warnings = [] + const messages = new MessageCarrier(); // do stuff - return { errors, warnings } + return messages; } diff --git a/src/plugins/validation/swagger2/semantic-validators/form-data.js b/src/plugins/validation/swagger2/semantic-validators/form-data.js index 1edb927b8..d87d14a56 100644 --- a/src/plugins/validation/swagger2/semantic-validators/form-data.js +++ b/src/plugins/validation/swagger2/semantic-validators/form-data.js @@ -17,10 +17,11 @@ const isObject = require('lodash/isObject'); const getIn = require('lodash/get'); +const MessageCarrier = require('../../../utils/messageCarrier'); module.exports.validate = function({ resolvedSpec }) { - let errors = []; - const warnings = []; + const messages = new MessageCarrier(); + if (!isObject(resolvedSpec)) { return; } @@ -86,18 +87,17 @@ module.exports.validate = function({ resolvedSpec }) { ); if (formDataWithTypos.length) { - return (errors = errors.concat( - params.map((param, i) => { - if (param['in'] !== 'formdata') { - return; - } - const pathStr = `${path.join('.')}.${i}`; - return { - path: pathStr, - message: 'The form data value for `in` must be camelCase (formData)' - }; - }) - )); + params.forEach((param, i) => { + if (param['in'] !== 'formdata') { + return; + } + messages.addMessage( + `${path.join('.')}.${i}`, + 'The form data value for `in` must be camelCase (formData)', + 'error' + ); + }); + return; } } @@ -114,11 +114,12 @@ module.exports.validate = function({ resolvedSpec }) { if (~inBodyIndex && hasFormData) { // We"ll blame the `in: body` parameter const pathStr = `${path.join('.')}.${inBodyIndex}`; - return errors.push({ - path: pathStr, - message: - 'Parameters cannot have `in` values of both "body" and "formData", as "formData" _will_ be the body' - }); + messages.addMessage( + pathStr, + 'Parameters cannot have `in` values of both "body" and "formData", as "formData" _will_ be the body', + 'error' + ); + return; } } @@ -139,20 +140,21 @@ module.exports.validate = function({ resolvedSpec }) { const pathStr = [...path, typeFileIndex].join('.'); // a - must have formData if (param['in'] !== 'formData') { - errors.push({ - path: pathStr, - message: 'Parameters with `type` "file" must have `in` be "formData"' - }); + messages.addMessage( + pathStr, + 'Parameters with `type` "file" must have `in` be "formData"', + 'error' + ); hasErrors = true; } // - b. The consumes property must have `multipart/form-data` if (!hasConsumes(operation, 'multipart/form-data')) { - errors.push({ - path: pathStr, - message: - 'Operations with Parameters of `type` "file" must include "multipart/form-data" in their "consumes" property' - }); + messages.addMessage( + pathStr, + 'Operations with Parameters of `type` "file" must include "multipart/form-data" in their "consumes" property', + 'error' + ); hasErrors = true; } @@ -175,13 +177,14 @@ module.exports.validate = function({ resolvedSpec }) { } const pathStr = path.slice(0, -1).join('.'); // Blame the operation, not the parameter0 - return errors.push({ - path: pathStr, - message: - 'Operations with Parameters of `in` "formData" must include "application/x-www-form-urlencoded" or "multipart/form-data" in their "consumes" property' - }); + messages.addMessage( + pathStr, + 'Operations with Parameters of `in` "formData" must include "application/x-www-form-urlencoded" or "multipart/form-data" in their "consumes" property', + 'error' + ); + return; } walk(resolvedSpec, []); - return { errors, warnings }; + return messages; }; diff --git a/src/plugins/validation/swagger2/semantic-validators/operations-ibm.js b/src/plugins/validation/swagger2/semantic-validators/operations-ibm.js index a3adfc6f1..2e6433abe 100644 --- a/src/plugins/validation/swagger2/semantic-validators/operations-ibm.js +++ b/src/plugins/validation/swagger2/semantic-validators/operations-ibm.js @@ -11,11 +11,10 @@ const each = require('lodash/each'); const includes = require('lodash/includes'); const map = require('lodash/map'); const pick = require('lodash/pick'); +const MessageCarrier = require('../../../utils/messageCarrier'); module.exports.validate = function({ jsSpec }, config) { - const result = {}; - result.error = []; - result.warning = []; + const messages = new MessageCarrier(); config = config.operations; @@ -71,15 +70,11 @@ module.exports.validate = function({ jsSpec }, config) { !hasGlobalConsumes && (hasBodyParamInOps || hasBodyParamInPath) ) { - const checkStatus = config.no_consumes_for_put_or_post; - - if (checkStatus !== 'off') { - result[checkStatus].push({ - path: `paths.${pathKey}.${opKey}.consumes`, - message: - 'PUT and POST operations with a body parameter must have a non-empty `consumes` field.' - }); - } + messages.addMessage( + `paths.${pathKey}.${opKey}.consumes`, + 'PUT and POST operations with a body parameter must have a non-empty `consumes` field.', + config.no_consumes_for_put_or_post + ); } } @@ -101,14 +96,11 @@ module.exports.validate = function({ jsSpec }, config) { successResponses.length === 1 && successResponses[0] === '204'; if (!hasLocalProduces && !hasGlobalProduces && !onlyHas204) { - const checkStatus = config.no_produces; - - if (checkStatus !== 'off') { - result[checkStatus].push({ - path: `paths.${pathKey}.${opKey}.produces`, - message: 'Operations must have a non-empty `produces` field.' - }); - } + messages.addMessage( + `paths.${pathKey}.${opKey}.produces`, + 'Operations must have a non-empty `produces` field.', + config.no_produces + ); } } @@ -116,18 +108,15 @@ module.exports.validate = function({ jsSpec }, config) { if (isGetOperation) { // get operations should not have a consumes property if (op.consumes) { - const checkStatus = config.get_op_has_consumes; - - if (checkStatus !== 'off') { - result[checkStatus].push({ - path: `paths.${pathKey}.${opKey}.consumes`, - message: 'GET operations should not specify a consumes field.' - }); - } + messages.addMessage( + `paths.${pathKey}.${opKey}.consumes`, + 'GET operations should not specify a consumes field.', + config.get_op_has_consumes + ); } } }); }); - return { errors: result.error, warnings: result.warning }; + return messages; }; diff --git a/src/plugins/validation/swagger2/semantic-validators/operations.js b/src/plugins/validation/swagger2/semantic-validators/operations.js index 055ef9e17..c4ec0a1dd 100644 --- a/src/plugins/validation/swagger2/semantic-validators/operations.js +++ b/src/plugins/validation/swagger2/semantic-validators/operations.js @@ -6,10 +6,10 @@ const map = require('lodash/map'); const each = require('lodash/each'); const findIndex = require('lodash/findIndex'); const findLastIndex = require('lodash/findLastIndex'); +const MessageCarrier = require('../../../utils/messageCarrier'); module.exports.validate = function({ resolvedSpec }) { - const errors = []; - const warnings = []; + const messages = new MessageCarrier(); map(resolvedSpec.paths, (path, pathKey) => { const pathOps = pick(path, [ @@ -30,22 +30,23 @@ module.exports.validate = function({ resolvedSpec }) { const bodyParamIndex = findIndex(op.parameters, ['in', 'body']); const formDataParamIndex = findIndex(op.parameters, ['in', 'formData']); if (bodyParamIndex > -1 && formDataParamIndex > -1) { - errors.push({ - path: `paths.${pathKey}.${opKey}.parameters`, - message: - 'Operations cannot have both a "body" parameter and "formData" parameter' - }); + messages.addMessage( + `paths.${pathKey}.${opKey}.parameters`, + 'Operations cannot have both a "body" parameter and "formData" parameter', + 'error' + ); } // Assertation 2 const lastBodyParamIndex = findLastIndex(op.parameters, ['in', 'body']); if (bodyParamIndex !== lastBodyParamIndex) { - errors.push({ - path: `paths.${pathKey}.${opKey}.parameters`, - message: 'Operations must have no more than one body parameter' - }); + messages.addMessage( + `paths.${pathKey}.${opKey}.parameters`, + 'Operations must have no more than one body parameter', + 'error' + ); } }); }); - return { errors, warnings }; + return messages; }; diff --git a/src/plugins/validation/swagger2/semantic-validators/parameters.js b/src/plugins/validation/swagger2/semantic-validators/parameters.js index 6647a426e..acf9dac4c 100644 --- a/src/plugins/validation/swagger2/semantic-validators/parameters.js +++ b/src/plugins/validation/swagger2/semantic-validators/parameters.js @@ -2,10 +2,10 @@ // The items property for a parameter is required when its type is set to array const { isParameterObject, walk } = require('../../../utils'); +const MessageCarrier = require('../../../utils/messageCarrier'); module.exports.validate = function({ resolvedSpec }) { - const errors = []; - const warnings = []; + const messages = new MessageCarrier(); walk(resolvedSpec, [], (obj, path) => { const isContentsOfParameterObject = isParameterObject(path, false); // 2nd arg is isOAS3 @@ -13,13 +13,14 @@ module.exports.validate = function({ resolvedSpec }) { // 1 if (isContentsOfParameterObject) { if (obj.type === 'array' && typeof obj.items !== 'object') { - errors.push({ + messages.addMessage( path, - message: "Parameters with 'array' type require an 'items' property." - }); + "Parameters with 'array' type require an 'items' property.", + 'error' + ); } } }); - return { errors, warnings }; + return messages; }; diff --git a/src/plugins/validation/swagger2/semantic-validators/schema.js b/src/plugins/validation/swagger2/semantic-validators/schema.js index b30b6f20a..d4ed3b3cc 100644 --- a/src/plugins/validation/swagger2/semantic-validators/schema.js +++ b/src/plugins/validation/swagger2/semantic-validators/schema.js @@ -1,8 +1,8 @@ const each = require('lodash/each'); +const MessageCarrier = require('../../../utils/messageCarrier'); module.exports.validate = function({ resolvedSpec }) { - const errors = []; - const warnings = []; + const messages = new MessageCarrier(); const schemas = []; @@ -57,28 +57,26 @@ module.exports.validate = function({ resolvedSpec }) { schemas.forEach(({ schema, path }) => { if (Array.isArray(schema.properties) && Array.isArray(schema.required)) { schema.properties.forEach(() => { - errors.push(...generateReadOnlyErrors(schema, path)); + generateReadOnlyErrors(schema, path, messages); }); } }); - return { errors, warnings }; + return messages; }; -function generateReadOnlyErrors(schema, contextPath) { - const arr = []; - +function generateReadOnlyErrors(schema, contextPath, messages) { schema.properties.forEach((property, i) => { if ( property.name && property.readOnly && schema.required.indexOf(property.name) > -1 ) { - arr.push({ - path: contextPath.concat(['required', i.toString()]), - message: 'Read only properties cannot marked as required by a schema.' - }); + messages.addMessage( + contextPath.concat(['required', i.toString()]), + 'Read only properties cannot marked as required by a schema.', + 'error' + ); } }); - return arr; } diff --git a/src/plugins/validation/swagger2/semantic-validators/security-definitions.js b/src/plugins/validation/swagger2/semantic-validators/security-definitions.js index e4f522a71..7ffaba091 100644 --- a/src/plugins/validation/swagger2/semantic-validators/security-definitions.js +++ b/src/plugins/validation/swagger2/semantic-validators/security-definitions.js @@ -6,7 +6,11 @@ // Assertation 5: "oauth2" security flow "accessCode" must have required string "tokenUrl", string "authorizationUrl" and object "scopes" parameters // Assertation 6: "oauth2" security flow "application" must have required string "tokenUrl", string "authorizationUrl" and object "scopes" parameters +const MessageCarrier = require('../../../utils/messageCarrier'); + module.exports.validate = function({ jsSpec }) { + const messages = new MessageCarrier(); + const API_KEY = 'apiKey'; const OAUTH2 = 'oauth2'; const BASIC = 'basic'; @@ -17,9 +21,6 @@ module.exports.validate = function({ jsSpec }) { const ACCESS_CODE = 'accessCode'; const oauth2Flows = [IMPLICIT, PASSWORD, APPLICATION, ACCESS_CODE]; - const errors = []; - const warnings = []; - const securityDefinitions = jsSpec.securityDefinitions; for (const key in securityDefinitions) { @@ -28,32 +29,33 @@ module.exports.validate = function({ jsSpec }) { const path = `securityDefinitions.${key}`; if (auths.indexOf(type) === -1) { - errors.push({ - message: `string 'type' param required for path: ${path}`, + messages.addMessageWithAuthId( path, - authId: key - }); + `string 'type' param required for path: ${path}`, + key, + 'error' + ); } else { //apiKey validation if (type === API_KEY) { const authIn = security.in; if (authIn !== 'query' && authIn !== 'header') { - errors.push({ - message: - "apiKey authorization must have required 'in' param, valid values are 'query' or 'header'.", + messages.addMessageWithAuthId( path, - authId: key - }); + "apiKey authorization must have required 'in' param, valid values are 'query' or 'header'.", + key, + 'error' + ); } if (!security.name) { - errors.push({ - message: - "apiKey authorization must have required 'name' string param. The name of the header or query parameter to be used.", + messages.addMessageWithAuthId( path, - authId: key - }); + "apiKey authorization must have required 'name' string param. The name of the header or query parameter to be used.", + key, + 'error' + ); } } // oauth2 validation else if (type === OAUTH2) { @@ -63,61 +65,61 @@ module.exports.validate = function({ jsSpec }) { const scopes = security.scopes; if (oauth2Flows.indexOf(flow) === -1) { - errors.push({ - message: - "oauth2 authorization must have required 'flow' string param. Valid values are 'implicit', 'password', 'application' or 'accessCode'", + messages.addMessageWithAuthId( path, - authId: key - }); + "oauth2 authorization must have required 'flow' string param. Valid values are 'implicit', 'password', 'application' or 'accessCode'", + key, + 'error' + ); } else if (flow === IMPLICIT) { if (!authorizationUrl) { - errors.push({ - message: - "oauth2 authorization implicit flow must have required 'authorizationUrl' parameter.", + messages.addMessageWithAuthId( path, - authId: key - }); + "oauth2 authorization implicit flow must have required 'authorizationUrl' parameter.", + key, + 'error' + ); } } else if (flow === ACCESS_CODE) { if (!authorizationUrl || !tokenUrl) { - errors.push({ - message: - "oauth2 authorization accessCode flow must have required 'authorizationUrl' and 'tokenUrl' string parameters.", + messages.addMessageWithAuthId( path, - authId: key - }); + "oauth2 authorization accessCode flow must have required 'authorizationUrl' and 'tokenUrl' string parameters.", + key, + 'error' + ); } } else if (flow === PASSWORD) { if (!tokenUrl) { - errors.push({ - message: - "oauth2 authorization password flow must have required 'tokenUrl' string parameter.", + messages.addMessageWithAuthId( path, - authId: key - }); + "oauth2 authorization password flow must have required 'tokenUrl' string parameter.", + key, + 'error' + ); } } else if (flow === APPLICATION) { if (!tokenUrl) { - errors.push({ - message: - "oauth2 authorization application flow must have required 'tokenUrl' string parameter.", + messages.addMessageWithAuthId( path, - authId: key - }); + "oauth2 authorization application flow must have required 'tokenUrl' string parameter.", + key, + 'error' + ); } } if (typeof scopes !== 'object') { - errors.push({ - message: - "'scopes' is required property type object. The available scopes for the OAuth2 security scheme.", + messages.addMessageWithAuthId( path, - authId: key - }); + "'scopes' is required property type object. The available scopes for the OAuth2 security scheme.", + key, + 'error' + ); } } } } - return { errors, warnings }; + return messages; }; diff --git a/src/plugins/validation/swagger2/semantic-validators/swagger.js b/src/plugins/validation/swagger2/semantic-validators/swagger.js index b9b176113..6c6a94514 100644 --- a/src/plugins/validation/swagger2/semantic-validators/swagger.js +++ b/src/plugins/validation/swagger2/semantic-validators/swagger.js @@ -7,27 +7,31 @@ // Assertation 3: // make sure the value of swagger field must be "2.0" +const MessageCarrier = require('../../../utils/messageCarrier'); + module.exports.validate = function({ jsSpec }) { - const errors = []; - const warnings = []; + const messages = new MessageCarrier(); const swagger = jsSpec.swagger; if (!swagger) { - errors.push({ - path: ['swagger'], - message: 'API definition must have an `swagger` field' - }); + messages.addMessage( + ['swagger'], + 'API definition must have an `swagger` field', + 'error' + ); } else if (typeof swagger !== 'string') { - errors.push({ - path: ['swagger'], - message: 'API definition must have an `swagger` field as type string' - }); + messages.addMessage( + ['swagger'], + 'API definition must have an `swagger` field as type string', + 'error' + ); } else if (swagger !== '2.0') { - errors.push({ - path: ['swagger'], - message: '`swagger` string must have the value `2.0`' - }); + messages.addMessage( + ['swagger'], + '`swagger` string must have the value `2.0`', + 'error' + ); } - return { errors, warnings }; + return messages; }; diff --git a/test/plugins/caseConventionCheck.js b/test/plugins/utils/caseConventionCheck.js similarity index 98% rename from test/plugins/caseConventionCheck.js rename to test/plugins/utils/caseConventionCheck.js index e6f7a537f..25e166831 100644 --- a/test/plugins/caseConventionCheck.js +++ b/test/plugins/utils/caseConventionCheck.js @@ -1,5 +1,5 @@ const expect = require('expect'); -const checkCase = require('../../src/plugins/utils/caseConventionCheck'); +const checkCase = require('../../../src/plugins/utils/caseConventionCheck'); describe('case convention regex tests', function() { describe('lower snake case tests', function() { diff --git a/test/plugins/has-ref-property.js b/test/plugins/utils/has-ref-property.js similarity index 97% rename from test/plugins/has-ref-property.js rename to test/plugins/utils/has-ref-property.js index 27f91f903..05d355f74 100644 --- a/test/plugins/has-ref-property.js +++ b/test/plugins/utils/has-ref-property.js @@ -1,5 +1,5 @@ const expect = require('expect'); -const { hasRefProperty } = require('../../src/plugins/utils'); +const { hasRefProperty } = require('../../../src/plugins/utils'); const spec = { paths: { diff --git a/test/plugins/utils/messageCarrier.js b/test/plugins/utils/messageCarrier.js new file mode 100644 index 000000000..de16239a0 --- /dev/null +++ b/test/plugins/utils/messageCarrier.js @@ -0,0 +1,154 @@ +const expect = require('expect'); +const MessageCarrier = require('../../../src/plugins/utils/messageCarrier'); + +describe('MessageCarrier tests', function() { + it('get errors returns all errors added', function() { + const messages = new MessageCarrier(); + + messages.addMessage(['paths', '/example', 'get'], 'message1', 'error'); + messages.addMessage(['paths', '/example', 'post'], 'message2', 'error'); + + expect(messages.errors.length).toEqual(2); + expect(messages.errors[0]).toEqual({ + path: ['paths', '/example', 'get'], + message: 'message1' + }); + expect(messages.errors[1]).toEqual({ + path: ['paths', '/example', 'post'], + message: 'message2' + }); + }); + + it('get warnings returns all warnings added', function() { + const messages = new MessageCarrier(); + + messages.addMessage('paths./example.get', 'message1', 'warning'); + messages.addMessage('paths./example.post', 'message2', 'warning'); + + expect(messages.warnings.length).toEqual(2); + expect(messages.warnings[0]).toEqual({ + path: 'paths./example.get', + message: 'message1' + }); + expect(messages.warnings[1]).toEqual({ + path: 'paths./example.post', + message: 'message2' + }); + }); + + it('get messages returns a dictionary with the list of errors and warnings', function() { + const messages = new MessageCarrier(); + + messages.addMessage(['paths', '/example', 'get'], 'message1', 'error'); + messages.addMessage(['paths', '/example', 'post'], 'message2', 'error'); + messages.addMessage( + 'paths./example.get.requestBody', + 'message3', + 'warning' + ); + + const messageDict = messages.messages; + expect(messageDict.error.length).toEqual(2); + expect(messageDict.error[0]).toEqual({ + path: ['paths', '/example', 'get'], + message: 'message1' + }); + expect(messageDict.warning.length).toEqual(1); + expect(messageDict.warning[0]).toEqual({ + path: 'paths./example.get.requestBody', + message: 'message3' + }); + }); + + it('addMessage does not add errors and warnings when status is off', function() { + const messages = new MessageCarrier(); + + messages.addMessage(['paths', '/example', 'get'], 'message1', 'off'); + messages.addMessage(['paths', '/example', 'post'], 'message2', 'off'); + messages.addMessage('paths./example.get.requestBody', 'message3', 'off'); + + expect(messages.errors.length).toEqual(0); + expect(messages.warnings.length).toEqual(0); + }); + + it('addMessageWithAuthId does not add errors and warnings when status is off', function() { + const messages = new MessageCarrier(); + + messages.addMessageWithAuthId( + ['paths', '/example', 'get'], + 'message1', + 'off' + ); + messages.addMessageWithAuthId( + ['paths', '/example', 'post'], + 'message2', + 'off' + ); + messages.addMessageWithAuthId( + 'paths./example.get.requestBody', + 'message3', + 'off' + ); + + expect(messages.errors.length).toEqual(0); + expect(messages.warnings.length).toEqual(0); + }); + + it('addMessageWithAuthId adds errors and includes the authId in the error', function() { + const messages = new MessageCarrier(); + + messages.addMessageWithAuthId( + ['paths', '/example', 'get'], + 'message1', + 'authId1', + 'error' + ); + messages.addMessageWithAuthId( + ['paths', '/example', 'post'], + 'message2', + 'authId2', + 'error' + ); + + expect(messages.errors.length).toEqual(2); + expect(messages.errors[0]).toEqual({ + path: ['paths', '/example', 'get'], + message: 'message1', + authId: 'authId1' + }); + expect(messages.errors[1]).toEqual({ + path: ['paths', '/example', 'post'], + message: 'message2', + authId: 'authId2' + }); + }); + + it('addMessageWithAuthId adds warnings and includes the authId in the warning', function() { + const messages = new MessageCarrier(); + + messages.addMessageWithAuthId( + ['paths', '/example', 'get'], + 'message1', + 'authId1', + 'warning' + ); + messages.addMessageWithAuthId( + ['paths', '/example', 'post'], + 'message2', + 'authId2', + 'warning' + ); + + expect(messages.warnings.length).toEqual(2); + expect(messages.warnings[0]).toEqual({ + path: ['paths', '/example', 'get'], + message: 'message1', + authId: 'authId1' + }); + expect(messages.warnings[1]).toEqual({ + path: ['paths', '/example', 'post'], + message: 'message2', + authId: 'authId2' + }); + }); +});