Skip to content

Commit ecde7da

Browse files
committed
feat: added explicit control of case conventions in paths, schema properties and schema enums
This change adds support for a couple of new case conventions to round out case convention support, upper_snake_case and upper_dash_case. Additionally it includes support for explicit and independent control of the case convention for paths (paths_case_convention), schema properties (property_case_convention), and schema enums (enum_case_convention). These options are disabled by default so as not to impact current usage of snake_case_only and thus are backwards compatible. In order to use them, a user must turn off the corresponding 'snake_case_only' option and enable these options. Full automated tests are included. Closes #38, #40
1 parent 6019de9 commit ecde7da

File tree

7 files changed

+656
-3
lines changed

7 files changed

+656
-3
lines changed

src/.defaultsForValidator.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ const defaults = {
3838
},
3939
'paths': {
4040
'missing_path_parameter': 'error',
41-
'snake_case_only': 'warning'
41+
'snake_case_only': 'warning',
42+
'paths_case_convention': ['off', 'lower_snake_case']
4243
},
4344
'responses': {
4445
'inline_response_schema': 'warning'
@@ -56,7 +57,9 @@ const defaults = {
5657
'no_schema_description': 'warning',
5758
'no_property_description': 'warning',
5859
'description_mentions_json': 'warning',
59-
'array_of_arrays': 'warning'
60+
'array_of_arrays': 'warning',
61+
'property_case_convention': [ 'off', 'lower_snake_case'],
62+
'enum_case_convention': [ 'off', 'lower_snake_case']
6063
},
6164
'walker': {
6265
'no_empty_descriptions': 'error',
@@ -107,9 +110,11 @@ const deprecated = {
107110
const configOptions = {
108111
'case_conventions': [
109112
'lower_snake_case',
113+
'upper_snake_case',
110114
'upper_camel_case',
111115
'lower_camel_case',
112116
'lower_dash_case',
117+
'upper_dash_case',
113118
'operation_id_case'
114119
]
115120
};

src/plugins/utils/caseConventionCheck.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,20 @@
66
77
*/
88
const lowerSnakeCase = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/;
9+
const upperSnakeCase = /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$/;
910
const upperCamelCase = /^[A-Z][a-z0-9]+([A-Z][a-z0-9]+)*$/;
1011
const lowerCamelCase = /^[a-z][a-z0-9]*([A-Z][a-z0-9]+)*$/;
1112
const lowerDashCase = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
13+
const upperDashCase = /^[A-Z][A-Z0-9]*(-[A-Z0-9]+)*$/;
1214

1315
module.exports = (string, convention) => {
1416
switch (convention) {
1517
case 'lower_snake_case':
1618
return lowerSnakeCase.test(string);
1719

20+
case 'upper_snake_case':
21+
return upperSnakeCase.test(string);
22+
1823
case 'upper_camel_case':
1924
return upperCamelCase.test(string);
2025

@@ -24,6 +29,9 @@ module.exports = (string, convention) => {
2429
case 'lower_dash_case':
2530
return lowerDashCase.test(string);
2631

32+
case 'upper_dash_case':
33+
return upperDashCase.test(string);
34+
2735
default:
2836
// this should never happen, the convention is validated in the config processor
2937
console.log(`Unsupported case: ${convention}`);

src/plugins/validation/2and3/semantic-validators/paths-ibm.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
// Assertation 3. All path segments are lower snake case
77

88
const isSnakecase = require('../../../utils/checkSnakeCase');
9+
const checkCase = require('../../../utils/caseConventionCheck');
910

1011
module.exports.validate = function({ resolvedSpec }, config) {
1112
const result = {};
@@ -135,6 +136,29 @@ module.exports.validate = function({ resolvedSpec }, config) {
135136
}
136137
});
137138
}
139+
140+
// enforce path segments follow path_case_convention if provided
141+
if (config.paths_case_convention) {
142+
const checkStatusPath = config.paths_case_convention[0];
143+
if (checkStatusPath !== 'off') {
144+
const caseConvention = config.paths_case_convention[1];
145+
const segments = pathName.split('/');
146+
segments.forEach(segment => {
147+
// the first element will be "" since pathName starts with "/"
148+
// also, ignore validating the path parameters
149+
if (segment === '' || segment[0] === '{') {
150+
return;
151+
}
152+
const isCorrectCase = checkCase(segment, caseConvention);
153+
if (!isCorrectCase) {
154+
result[checkStatusPath].push({
155+
path: `paths.${pathName}`,
156+
message: `Path segments must follow case convention: ${caseConvention}`
157+
});
158+
}
159+
});
160+
}
161+
}
138162
});
139163

140164
return { errors: result.error, warnings: result.warning };

src/plugins/validation/2and3/semantic-validators/schema-ibm.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
const forIn = require('lodash/forIn');
2121
const includes = require('lodash/includes');
2222
const isSnakecase = require('../../../utils/checkSnakeCase');
23+
const checkCase = require('../../../utils/caseConventionCheck');
2324
const walk = require('../../../utils/walk');
2425

2526
module.exports.validate = function({ jsSpec, isOAS3 }, config) {
@@ -97,6 +98,33 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) {
9798
errors.push(...res.error);
9899
warnings.push(...res.warning);
99100
}
101+
102+
// optional support for property_case_convention and enum_case_convention
103+
// in config. Should be mutually exclusive with usage of config.snake_case_only
104+
if (config.property_case_convention) {
105+
const checkCaseStatus = config.property_case_convention[0];
106+
if (checkCaseStatus !== 'off') {
107+
res = checkPropNamesCaseConvention(
108+
schema,
109+
path,
110+
config.property_case_convention
111+
);
112+
errors.push(...res.error);
113+
warnings.push(...res.warning);
114+
}
115+
}
116+
if (config.enum_case_convention) {
117+
const checkCaseStatus = config.enum_case_convention[0];
118+
if (checkCaseStatus !== 'off') {
119+
res = checkEnumCaseConvention(
120+
schema,
121+
path,
122+
config.enum_case_convention
123+
);
124+
errors.push(...res.error);
125+
warnings.push(...res.warning);
126+
}
127+
}
100128
});
101129

102130
return { errors, warnings };
@@ -284,6 +312,45 @@ function checkPropNames(schema, contextPath, config) {
284312
return result;
285313
}
286314

315+
/**
316+
* Check that property names follow the specified case convention
317+
* @param schema
318+
* @param contextPath
319+
* @param caseConvention an array, [0]='off' | 'warning' | 'error'. [1]='lower_snake_case' etc.
320+
*/
321+
function checkPropNamesCaseConvention(schema, contextPath, caseConvention) {
322+
const result = {};
323+
result.error = [];
324+
result.warning = [];
325+
326+
if (!schema.properties) {
327+
return result;
328+
}
329+
if (!caseConvention) {
330+
return result;
331+
}
332+
333+
// flag any property whose name does not follow the case convention
334+
forIn(schema.properties, (property, propName) => {
335+
if (propName.slice(0, 2) === 'x-') return;
336+
337+
const checkStatus = caseConvention[0] || 'off';
338+
if (checkStatus.match('error|warning')) {
339+
const caseConventionValue = caseConvention[1];
340+
341+
const isCorrectCase = checkCase(propName, caseConventionValue);
342+
if (!isCorrectCase) {
343+
result[checkStatus].push({
344+
path: contextPath.concat(['properties', propName]),
345+
message: `Property names must follow case convention: ${caseConventionValue}`
346+
});
347+
}
348+
}
349+
});
350+
351+
return result;
352+
}
353+
287354
function checkEnumValues(schema, contextPath, config) {
288355
const result = {};
289356
result.error = [];
@@ -310,6 +377,43 @@ function checkEnumValues(schema, contextPath, config) {
310377
return result;
311378
}
312379

380+
/**
381+
* Check that enum values follow the specified case convention
382+
* @param schema
383+
* @param contextPath
384+
* @param caseConvention an array, [0]='off' | 'warning' | 'error'. [1]='lower_snake_case' etc.
385+
*/
386+
function checkEnumCaseConvention(schema, contextPath, caseConvention) {
387+
const result = {};
388+
result.error = [];
389+
result.warning = [];
390+
391+
if (!schema.enum) {
392+
return result;
393+
}
394+
if (!caseConvention) {
395+
return result;
396+
}
397+
398+
for (let i = 0; i < schema.enum.length; i++) {
399+
const enumValue = schema.enum[i];
400+
401+
const checkStatus = caseConvention[0] || 'off';
402+
if (checkStatus.match('error|warning')) {
403+
const caseConventionValue = caseConvention[1];
404+
const isCorrectCase = checkCase(enumValue, caseConventionValue);
405+
if (!isCorrectCase) {
406+
result[checkStatus].push({
407+
path: contextPath.concat(['enum', i.toString()]),
408+
message: `Enum values must follow case convention: ${caseConventionValue}`
409+
});
410+
}
411+
}
412+
}
413+
414+
return result;
415+
}
416+
313417
// NOTE: this function is Swagger 2 specific and would need to be adapted to be used with OAS
314418
function isRootSchema(path) {
315419
const current = path[path.length - 1];

test/plugins/caseConventionCheck.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,34 @@ describe('case convention regex tests', function() {
2121
});
2222
});
2323

24+
describe('upper snake case tests', function() {
25+
const convention = 'upper_snake_case';
26+
27+
it('SHA1 is upper snake case', function() {
28+
const string = 'SHA1';
29+
expect(checkCase(string, convention)).toEqual(true);
30+
});
31+
it('sha1 is NOT upper snake case', function() {
32+
const string = 'sha1';
33+
expect(checkCase(string, convention)).toEqual(false);
34+
});
35+
36+
it('good_case_string is NOT upper_snake_case', function() {
37+
const string = 'good_case_string';
38+
expect(checkCase(string, convention)).toEqual(false);
39+
});
40+
41+
it('GOOD_CASE_STRING is upper_snake_case', function() {
42+
const string = 'GOOD_CASE_STRING';
43+
expect(checkCase(string, convention)).toEqual(true);
44+
});
45+
46+
it('badCaseString is NOT upper_snake_case', function() {
47+
const string = 'badCaseString';
48+
expect(checkCase(string, convention)).toEqual(false);
49+
});
50+
});
51+
2452
describe('upper camel case tests', function() {
2553
const convention = 'upper_camel_case';
2654
it('Sha1 is upper camel case', function() {
@@ -84,4 +112,34 @@ describe('case convention regex tests', function() {
84112
expect(checkCase(string, convention)).toEqual(false);
85113
});
86114
});
115+
describe('upper dash case tests', function() {
116+
const convention = 'upper_dash_case';
117+
it('sha1 is NOT upper_dash_case', function() {
118+
const string = 'sha1';
119+
expect(checkCase(string, convention)).toEqual(false);
120+
});
121+
122+
it('SHA1 is upper_dash_case', function() {
123+
const string = 'SHA1';
124+
expect(checkCase(string, convention)).toEqual(true);
125+
});
126+
127+
it('bad-case-string is NOT upper_dash_case', function() {
128+
const string = 'bad-case-string';
129+
expect(checkCase(string, convention)).toEqual(false);
130+
});
131+
it('GOOD-CASE-STRING is upper_dash_case', function() {
132+
const string = 'GOOD-CASE-STRING';
133+
expect(checkCase(string, convention)).toEqual(true);
134+
});
135+
136+
it('Bad-Case-String is NOT upper_dash_case', function() {
137+
const string = 'Bad-Case-String';
138+
expect(checkCase(string, convention)).toEqual(false);
139+
});
140+
it('badCaseString is NOT upper_dash_case', function() {
141+
const string = 'badCaseString';
142+
expect(checkCase(string, convention)).toEqual(false);
143+
});
144+
});
87145
});

test/plugins/validation/2and3/paths-ibm.js

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ describe('validation plugin - semantic - paths-ibm', function() {
271271
expect(res.warnings).toEqual([]);
272272
});
273273

274-
it('shoud flag a path segment that is not snake_case but should ignore path parameter', function() {
274+
it('should flag a path segment that is not snake_case but should ignore path parameter', function() {
275275
const config = {
276276
paths: {
277277
snake_case_only: 'warning'
@@ -304,4 +304,68 @@ describe('validation plugin - semantic - paths-ibm', function() {
304304
'Path segments must be lower snake case.'
305305
);
306306
});
307+
308+
it('should flag a path segment that does not follow paths_case_convention but should ignore path parameter', function() {
309+
const config = {
310+
paths: {
311+
snake_case_only: 'off',
312+
paths_case_convention: ['warning', 'lower_camel_case']
313+
}
314+
};
315+
316+
const badSpec = {
317+
paths: {
318+
'/v1/api/NotGoodSegment/{shouldntMatter}/resource': {
319+
parameters: [
320+
{
321+
in: 'path',
322+
name: 'shouldntMatter',
323+
description:
324+
'bad parameter but should be caught by another validator, not here',
325+
type: 'string'
326+
}
327+
]
328+
}
329+
}
330+
};
331+
332+
const res = validate({ resolvedSpec: badSpec }, config);
333+
expect(res.errors.length).toEqual(0);
334+
expect(res.warnings.length).toEqual(1);
335+
expect(res.warnings[0].path).toEqual(
336+
'paths./v1/api/NotGoodSegment/{shouldntMatter}/resource'
337+
);
338+
expect(res.warnings[0].message).toEqual(
339+
'Path segments must follow case convention: lower_camel_case'
340+
);
341+
});
342+
343+
it('should not flag a path segment that follows paths_case_convention and should ignore path parameter', function() {
344+
const config = {
345+
paths: {
346+
snake_case_only: 'off',
347+
paths_case_convention: ['warning', 'lower_dash_case']
348+
}
349+
};
350+
351+
const goodSpec = {
352+
paths: {
353+
'/v1/api/good-segment/{shouldntMatter}/the-resource': {
354+
parameters: [
355+
{
356+
in: 'path',
357+
name: 'shouldntMatter',
358+
description:
359+
'bad parameter but should be caught by another validator, not here',
360+
type: 'string'
361+
}
362+
]
363+
}
364+
}
365+
};
366+
367+
const res = validate({ resolvedSpec: goodSpec }, config);
368+
expect(res.errors.length).toEqual(0);
369+
expect(res.warnings.length).toEqual(0);
370+
});
307371
});

0 commit comments

Comments
 (0)