diff --git a/src/.defaultsForValidator.js b/src/.defaultsForValidator.js index 543d26ad3..31a712fdf 100644 --- a/src/.defaultsForValidator.js +++ b/src/.defaultsForValidator.js @@ -38,7 +38,8 @@ const defaults = { }, 'paths': { 'missing_path_parameter': 'error', - 'snake_case_only': 'warning' + 'snake_case_only': 'warning', + 'paths_case_convention': ['off', 'lower_snake_case'] }, 'responses': { 'inline_response_schema': 'warning' @@ -56,7 +57,9 @@ const defaults = { 'no_schema_description': 'warning', 'no_property_description': 'warning', 'description_mentions_json': 'warning', - 'array_of_arrays': 'warning' + 'array_of_arrays': 'warning', + 'property_case_convention': [ 'off', 'lower_snake_case'], + 'enum_case_convention': [ 'off', 'lower_snake_case'] }, 'walker': { 'no_empty_descriptions': 'error', @@ -107,9 +110,11 @@ const deprecated = { const configOptions = { 'case_conventions': [ 'lower_snake_case', + 'upper_snake_case', 'upper_camel_case', 'lower_camel_case', 'lower_dash_case', + 'upper_dash_case', 'operation_id_case' ] }; diff --git a/src/plugins/utils/caseConventionCheck.js b/src/plugins/utils/caseConventionCheck.js index 2229deb96..b490aae6f 100644 --- a/src/plugins/utils/caseConventionCheck.js +++ b/src/plugins/utils/caseConventionCheck.js @@ -6,15 +6,20 @@ */ const lowerSnakeCase = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/; +const upperSnakeCase = /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/; const upperCamelCase = /^[A-Z][a-z0-9]+([A-Z][a-z0-9]+)*$/; const lowerCamelCase = /^[a-z][a-z0-9]*([A-Z][a-z0-9]+)*$/; const lowerDashCase = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/; +const upperDashCase = /^[A-Z][A-Z0-9]*(-[A-Z0-9]+)*$/; module.exports = (string, convention) => { switch (convention) { case 'lower_snake_case': return lowerSnakeCase.test(string); + case 'upper_snake_case': + return upperSnakeCase.test(string); + case 'upper_camel_case': return upperCamelCase.test(string); @@ -24,6 +29,9 @@ module.exports = (string, convention) => { case 'lower_dash_case': return lowerDashCase.test(string); + case 'upper_dash_case': + return upperDashCase.test(string); + default: // this should never happen, the convention is validated in the config processor console.log(`Unsupported case: ${convention}`); diff --git a/src/plugins/validation/2and3/semantic-validators/paths-ibm.js b/src/plugins/validation/2and3/semantic-validators/paths-ibm.js index 418729de6..a314670a3 100644 --- a/src/plugins/validation/2and3/semantic-validators/paths-ibm.js +++ b/src/plugins/validation/2and3/semantic-validators/paths-ibm.js @@ -6,6 +6,7 @@ // Assertation 3. All path segments are lower snake case const isSnakecase = require('../../../utils/checkSnakeCase'); +const checkCase = require('../../../utils/caseConventionCheck'); module.exports.validate = function({ resolvedSpec }, config) { const result = {}; @@ -135,6 +136,29 @@ module.exports.validate = function({ resolvedSpec }, config) { } }); } + + // enforce path segments follow path_case_convention if provided + if (config.paths_case_convention) { + const checkStatusPath = config.paths_case_convention[0]; + if (checkStatusPath !== 'off') { + const caseConvention = config.paths_case_convention[1]; + const segments = pathName.split('/'); + segments.forEach(segment => { + // the first element will be "" since pathName starts with "/" + // also, ignore validating the path parameters + if (segment === '' || segment[0] === '{') { + return; + } + const isCorrectCase = checkCase(segment, caseConvention); + if (!isCorrectCase) { + result[checkStatusPath].push({ + path: `paths.${pathName}`, + message: `Path segments must follow case convention: ${caseConvention}` + }); + } + }); + } + } }); return { errors: result.error, warnings: result.warning }; diff --git a/src/plugins/validation/2and3/semantic-validators/schema-ibm.js b/src/plugins/validation/2and3/semantic-validators/schema-ibm.js index 4621955eb..9eff071eb 100644 --- a/src/plugins/validation/2and3/semantic-validators/schema-ibm.js +++ b/src/plugins/validation/2and3/semantic-validators/schema-ibm.js @@ -20,6 +20,7 @@ const forIn = require('lodash/forIn'); const includes = require('lodash/includes'); const isSnakecase = require('../../../utils/checkSnakeCase'); +const checkCase = require('../../../utils/caseConventionCheck'); const walk = require('../../../utils/walk'); module.exports.validate = function({ jsSpec, isOAS3 }, config) { @@ -97,6 +98,33 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) { errors.push(...res.error); warnings.push(...res.warning); } + + // optional support for property_case_convention and enum_case_convention + // in config. Should be mutually exclusive with usage of config.snake_case_only + if (config.property_case_convention) { + const checkCaseStatus = config.property_case_convention[0]; + if (checkCaseStatus !== 'off') { + res = checkPropNamesCaseConvention( + schema, + path, + config.property_case_convention + ); + 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( + schema, + path, + config.enum_case_convention + ); + errors.push(...res.error); + warnings.push(...res.warning); + } + } }); return { errors, warnings }; @@ -284,6 +312,45 @@ function checkPropNames(schema, contextPath, config) { return result; } +/** + * Check that property names follow the specified case convention + * @param schema + * @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; + } + + // flag any property whose name does not follow the case convention + forIn(schema.properties, (property, propName) => { + if (propName.slice(0, 2) === 'x-') return; + + const checkStatus = caseConvention[0] || 'off'; + if (checkStatus.match('error|warning')) { + const caseConventionValue = caseConvention[1]; + + const isCorrectCase = checkCase(propName, caseConventionValue); + if (!isCorrectCase) { + result[checkStatus].push({ + path: contextPath.concat(['properties', propName]), + message: `Property names must follow case convention: ${caseConventionValue}` + }); + } + } + }); + + return result; +} + function checkEnumValues(schema, contextPath, config) { const result = {}; result.error = []; @@ -310,6 +377,43 @@ function checkEnumValues(schema, contextPath, config) { return result; } +/** + * Check that enum values follow the specified case convention + * @param schema + * @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; + } + + for (let i = 0; i < schema.enum.length; i++) { + const enumValue = schema.enum[i]; + + const checkStatus = caseConvention[0] || 'off'; + if (checkStatus.match('error|warning')) { + 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}` + }); + } + } + } + + return result; +} + // NOTE: this function is Swagger 2 specific and would need to be adapted to be used with OAS function isRootSchema(path) { const current = path[path.length - 1]; diff --git a/test/plugins/caseConventionCheck.js b/test/plugins/caseConventionCheck.js index f54197dfc..392751559 100644 --- a/test/plugins/caseConventionCheck.js +++ b/test/plugins/caseConventionCheck.js @@ -21,6 +21,34 @@ describe('case convention regex tests', function() { }); }); + describe('upper snake case tests', function() { + const convention = 'upper_snake_case'; + + it('SHA1 is upper snake case', function() { + const string = 'SHA1'; + expect(checkCase(string, convention)).toEqual(true); + }); + it('sha1 is NOT upper snake case', function() { + const string = 'sha1'; + expect(checkCase(string, convention)).toEqual(false); + }); + + it('good_case_string is NOT upper_snake_case', function() { + const string = 'good_case_string'; + expect(checkCase(string, convention)).toEqual(false); + }); + + it('GOOD_CASE_STRING is upper_snake_case', function() { + const string = 'GOOD_CASE_STRING'; + expect(checkCase(string, convention)).toEqual(true); + }); + + it('badCaseString is NOT upper_snake_case', function() { + const string = 'badCaseString'; + expect(checkCase(string, convention)).toEqual(false); + }); + }); + describe('upper camel case tests', function() { const convention = 'upper_camel_case'; it('Sha1 is upper camel case', function() { @@ -84,4 +112,34 @@ describe('case convention regex tests', function() { expect(checkCase(string, convention)).toEqual(false); }); }); + describe('upper dash case tests', function() { + const convention = 'upper_dash_case'; + it('sha1 is NOT upper_dash_case', function() { + const string = 'sha1'; + expect(checkCase(string, convention)).toEqual(false); + }); + + it('SHA1 is upper_dash_case', function() { + const string = 'SHA1'; + expect(checkCase(string, convention)).toEqual(true); + }); + + it('bad-case-string is NOT upper_dash_case', function() { + const string = 'bad-case-string'; + expect(checkCase(string, convention)).toEqual(false); + }); + it('GOOD-CASE-STRING is upper_dash_case', function() { + const string = 'GOOD-CASE-STRING'; + expect(checkCase(string, convention)).toEqual(true); + }); + + it('Bad-Case-String is NOT upper_dash_case', function() { + const string = 'Bad-Case-String'; + expect(checkCase(string, convention)).toEqual(false); + }); + it('badCaseString is NOT upper_dash_case', function() { + const string = 'badCaseString'; + expect(checkCase(string, convention)).toEqual(false); + }); + }); }); diff --git a/test/plugins/validation/2and3/paths-ibm.js b/test/plugins/validation/2and3/paths-ibm.js index 97233cf08..a9f4976ac 100644 --- a/test/plugins/validation/2and3/paths-ibm.js +++ b/test/plugins/validation/2and3/paths-ibm.js @@ -271,7 +271,7 @@ describe('validation plugin - semantic - paths-ibm', function() { expect(res.warnings).toEqual([]); }); - it('shoud flag a path segment that is not snake_case but should ignore path parameter', function() { + it('should flag a path segment that is not snake_case but should ignore path parameter', function() { const config = { paths: { snake_case_only: 'warning' @@ -304,4 +304,68 @@ describe('validation plugin - semantic - paths-ibm', function() { 'Path segments must be lower snake case.' ); }); + + it('should flag a path segment that does not follow paths_case_convention but should ignore path parameter', function() { + const config = { + paths: { + snake_case_only: 'off', + paths_case_convention: ['warning', 'lower_camel_case'] + } + }; + + const badSpec = { + paths: { + '/v1/api/NotGoodSegment/{shouldntMatter}/resource': { + parameters: [ + { + in: 'path', + name: 'shouldntMatter', + description: + 'bad parameter but should be caught by another validator, not here', + type: 'string' + } + ] + } + } + }; + + const res = validate({ resolvedSpec: badSpec }, config); + expect(res.errors.length).toEqual(0); + expect(res.warnings.length).toEqual(1); + expect(res.warnings[0].path).toEqual( + 'paths./v1/api/NotGoodSegment/{shouldntMatter}/resource' + ); + expect(res.warnings[0].message).toEqual( + 'Path segments must follow case convention: lower_camel_case' + ); + }); + + it('should not flag a path segment that follows paths_case_convention and should ignore path parameter', function() { + const config = { + paths: { + snake_case_only: 'off', + paths_case_convention: ['warning', 'lower_dash_case'] + } + }; + + const goodSpec = { + paths: { + '/v1/api/good-segment/{shouldntMatter}/the-resource': { + parameters: [ + { + in: 'path', + name: 'shouldntMatter', + description: + 'bad parameter but should be caught by another validator, not here', + type: 'string' + } + ] + } + } + }; + + const res = validate({ resolvedSpec: goodSpec }, config); + expect(res.errors.length).toEqual(0); + expect(res.warnings.length).toEqual(0); + }); }); diff --git a/test/plugins/validation/2and3/schema-ibm.js b/test/plugins/validation/2and3/schema-ibm.js index a2bb3d69b..8eb414710 100644 --- a/test/plugins/validation/2and3/schema-ibm.js +++ b/test/plugins/validation/2and3/schema-ibm.js @@ -368,6 +368,130 @@ describe('validation plugin - semantic - schema-ibm - Swagger 2', () => { ); }); + // tests for explicit property case convention + it('should return a warning when a property name does not follow property_case_convention[1]=lower_snake_case', () => { + const config = { + schemas: { + snake_case_only: 'off', + property_case_convention: ['warning', 'lower_snake_case'] + } + }; + + const spec = { + definitions: { + Thing: { + type: 'object', + description: 'thing', + properties: { + thingString: { + type: 'string', + description: 'thing string' + } + } + } + } + }; + + const res = validate({ jsSpec: spec }, config); + expect(res.errors.length).toEqual(0); + expect(res.warnings.length).toEqual(1); + expect(res.warnings[0].path).toEqual([ + 'definitions', + 'Thing', + 'properties', + 'thingString' + ]); + expect(res.warnings[0].message).toEqual( + 'Property names must follow case convention: lower_snake_case' + ); + }); + + it('should return a warning when a property name does not follow property_case_convention[1]=lower_snake_case', () => { + const config = { + schemas: { + snake_case_only: 'off', + property_case_convention: ['warning', 'lower_snake_case'] + } + }; + + const spec = { + definitions: { + Thing: { + type: 'object', + description: 'thing', + properties: { + thing: { + type: 'array', + description: 'thing array', + items: { + type: 'object', + properties: { + thingString: { + type: 'string', + description: 'thing string' + } + } + } + } + } + } + } + }; + + const res = validate({ jsSpec: spec }, config); + expect(res.errors.length).toEqual(0); + expect(res.warnings.length).toEqual(1); + expect(res.warnings[0].path).toEqual([ + 'definitions', + 'Thing', + 'properties', + 'thing', + 'items', + 'properties', + 'thingString' + ]); + expect(res.warnings[0].message).toEqual( + 'Property names must follow case convention: lower_snake_case' + ); + }); + + it('should return no warnings when a property does follow property_case_convention[1]=lower_snake_case', () => { + const config = { + schemas: { + snake_case_only: 'off', + property_case_convention: ['warning', 'lower_snake_case'] + } + }; + + const spec = { + definitions: { + Thing: { + type: 'object', + description: 'thing', + properties: { + thing: { + type: 'array', + description: 'thing array', + items: { + type: 'object', + properties: { + thing_string: { + type: 'string', + description: 'thing string' + } + } + } + } + } + } + } + }; + + const res = validate({ jsSpec: spec }, config); + expect(res.errors.length).toEqual(0); + expect(res.warnings.length).toEqual(0); + }); + it('should return an error when a schema has no description', () => { const config = { schemas: { @@ -938,4 +1062,270 @@ describe('validation plugin - semantic - schema-ibm - OpenAPI 3', () => { 'Enum values must be lower snake case.' ); }); + + it('should return a warning when an enum value is not snake case', () => { + const config = { + schemas: { + snake_case_only: 'warning' + } + }; + + const spec = { + definitions: { + Thing: { + type: 'object', + description: 'thing', + properties: { + color: { + type: 'string', + description: 'some color', + enum: ['blue', 'light_blue', 'darkBlue'] + } + } + } + } + }; + + const res = validate({ jsSpec: spec, isOAS3: true }, config); + expect(res.errors.length).toEqual(0); + expect(res.warnings.length).toEqual(1); + expect(res.warnings[0].path).toEqual([ + 'definitions', + 'Thing', + 'properties', + 'color', + 'enum', + '2' + ]); + expect(res.warnings[0].message).toEqual( + 'Enum values must be lower snake case.' + ); + }); + + it('should return a warning when an enum value is not snake case', () => { + const config = { + schemas: { + snake_case_only: 'warning' + } + }; + + const spec = { + paths: { + '/some/path/{id}': { + get: { + parameters: [ + { + name: 'enum_param', + in: 'query', + description: 'an enum param', + type: 'array', + required: 'true', + items: { + type: 'string', + description: 'the values', + enum: ['all', 'enumValues', 'possible'] + } + } + ] + } + } + } + }; + + const res = validate({ jsSpec: spec, isOAS3: true }, config); + expect(res.errors.length).toEqual(0); + expect(res.warnings.length).toEqual(1); + expect(res.warnings[0].path).toEqual([ + 'paths', + '/some/path/{id}', + 'get', + 'parameters', + '0', + 'items', + 'enum', + '1' + ]); + expect(res.warnings[0].message).toEqual( + 'Enum values must be lower snake case.' + ); + }); + + // Tests for explicit enum_case_convention + it('should return a warning when an enum value does not follow enum_case_convention[1]=lower_snake_case', () => { + const config = { + schemas: { + snake_case_only: 'off', + enum_case_convention: ['warning', 'lower_snake_case'] + } + }; + + const spec = { + definitions: { + Thing: { + type: 'object', + description: 'thing', + properties: { + color: { + type: 'string', + description: 'some color', + enum: ['blue', 'light_blue', 'darkBlue'] + } + } + } + } + }; + + const res = validate({ jsSpec: spec, isOAS3: true }, config); + expect(res.errors.length).toEqual(0); + expect(res.warnings.length).toEqual(1); + expect(res.warnings[0].path).toEqual([ + 'definitions', + 'Thing', + 'properties', + 'color', + 'enum', + '2' + ]); + expect(res.warnings[0].message).toEqual( + 'Enum values must follow case convention: lower_snake_case' + ); + }); + + it('should return a warning when an enum value does not follow enum_case_convention[1]=lower_snake_case', () => { + const config = { + schemas: { + snake_case_only: 'off', + enum_case_convention: ['warning', 'lower_snake_case'] + } + }; + + const spec = { + paths: { + '/some/path/{id}': { + get: { + parameters: [ + { + name: 'enum_param', + in: 'query', + description: 'an enum param', + type: 'array', + required: 'true', + items: { + type: 'string', + description: 'the values', + enum: ['all', 'enumValues', 'possible'] + } + } + ] + } + } + } + }; + + const res = validate({ jsSpec: spec, isOAS3: true }, config); + expect(res.errors.length).toEqual(0); + expect(res.warnings.length).toEqual(1); + expect(res.warnings[0].path).toEqual([ + 'paths', + '/some/path/{id}', + 'get', + 'parameters', + '0', + 'items', + 'enum', + '1' + ]); + expect(res.warnings[0].message).toEqual( + 'Enum values must follow case convention: lower_snake_case' + ); + }); + + it('should return a warning when an enum value does not follow enum_case_convention[1]=lower_snake_case', () => { + const config = { + schemas: { + snake_case_only: 'off', + enum_case_convention: ['warning', 'lower_snake_case'] + } + }; + + const spec = { + definitions: { + Thing: { + type: 'object', + description: 'thing', + properties: { + color: { + type: 'string', + description: 'some color', + enum: ['blue', 'light_blue', 'darkBlue'] + } + } + } + } + }; + + const res = validate({ jsSpec: spec, isOAS3: true }, config); + expect(res.errors.length).toEqual(0); + expect(res.warnings.length).toEqual(1); + expect(res.warnings[0].path).toEqual([ + 'definitions', + 'Thing', + 'properties', + 'color', + 'enum', + '2' + ]); + expect(res.warnings[0].message).toEqual( + 'Enum values must follow case convention: lower_snake_case' + ); + }); + + it('should return a warning when an enum value does not follow enum_case_convention[1]=lower_snake_case', () => { + const config = { + schemas: { + snake_case_only: 'off', + enum_case_convention: ['warning', 'lower_snake_case'] + } + }; + + const spec = { + paths: { + '/some/path/{id}': { + get: { + parameters: [ + { + name: 'enum_param', + in: 'query', + description: 'an enum param', + type: 'array', + required: 'true', + items: { + type: 'string', + description: 'the values', + enum: ['all', 'enumValues', 'possible'] + } + } + ] + } + } + } + }; + + const res = validate({ jsSpec: spec, isOAS3: true }, config); + expect(res.errors.length).toEqual(0); + expect(res.warnings.length).toEqual(1); + expect(res.warnings[0].path).toEqual([ + 'paths', + '/some/path/{id}', + 'get', + 'parameters', + '0', + 'items', + 'enum', + '1' + ]); + expect(res.warnings[0].message).toEqual( + 'Enum values must follow case convention: lower_snake_case' + ); + }); });