Skip to content

Commit 8f58503

Browse files
authored
Merge pull request #41 from aspear/aspear/explicit-case-convention-support
feat: added explicit control of case conventions in paths, properties and enums
2 parents 8152d06 + 23cf8e5 commit 8f58503

File tree

8 files changed

+670
-6
lines changed

8 files changed

+670
-6
lines changed

README.md

+11-3
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,12 @@ console.log(JSON.stringify(validationResults, null, 2));
9595
Returns a `Promise` with the validation results.
9696

9797
###### openApiDoc
98-
Type: `Object`
98+
Type: `Object`
9999
An object that represents an OpenAPI document.
100100

101101
###### defaultMode
102-
Type: `boolean`
103-
Default: `false`
102+
Type: `boolean`
103+
Default: `false`
104104
If set to true, the validator will ignore the `.validaterc` file and will use the [configuration defaults](#default-values).
105105

106106
#### Validation results
@@ -205,6 +205,7 @@ The supported rules are described below:
205205
| --------------------------- | ------------------------------------------------------------------------------------------------------------ | ------ |
206206
| missing_path_parameter | For a path that contains path parameters, flag any operations that do not correctly define those parameters. | shared |
207207
| snake_case_only | Flag any path segment that does not use snake case. | shared |
208+
| paths_case_convention | Flag any path segment that does not follow a given case convention. snake_case_only must be 'off' to use. | shared |
208209

209210
##### [responses][4]
210211
| Rule | Description | Spec |
@@ -222,6 +223,8 @@ The supported rules are described below:
222223
| no_property_description | Flag any schema that contains a 'property' without a `description` field. | shared |
223224
| description_mentions_json | Flag any schema with a 'property' description that mentions the word 'JSON'. | shared |
224225
| array_of_arrays | Flag any schema with a 'property' of type `array` with items of type `array`. | shared |
226+
| property_case_convention | Flag any property with a `name` that does not follow a given case convention. snake_case_only must be 'off' to use. | shared |
227+
| enum_case_convention | Flag any enum with a `value` that does not follow a given case convention. snake_case_only must be 'off' to use. | shared |
225228

226229
##### security_definitions
227230
| Rule | Description | Spec |
@@ -265,9 +268,11 @@ For rules that accept additional configuration, there will be a limited set of a
265268
| Option | Description | Example |
266269
| ---------------- | -------------------------------------------------------- | ---------------- |
267270
| lower_snake_case | Words must follow standard lower snake case conventions. | learning_opt_out |
271+
| upper_snake_case | Words must follow standard upper snake case conventions. | LEARNING_OPT_OUT |
268272
| upper_camel_case | Words must follow standard upper camel case conventions. | LearningOptOut |
269273
| lower_camel_case | Words must follow standard lower camel case conventions. | learningOptOut |
270274
| lower_dash_case | Words must follow standard lower dash case conventions. | learning-opt-out |
275+
| upper_dash_case | Words must follow standard upper dash case conventions. | LEARNING-OPT-OUT |
271276

272277
### Configuration file
273278

@@ -353,6 +358,7 @@ The default values for each rule are described below.
353358
| --------------------------- | ------- |
354359
| missing_path_parameter | error |
355360
| snake_case_only | warning |
361+
| paths_case_convention | off, lower_snake_case |
356362

357363
##### responses
358364
| Rule | Default |
@@ -379,6 +385,8 @@ The default values for each rule are described below.
379385
| no_property_description | warning |
380386
| description_mentions_json | warning |
381387
| array_of_arrays | warning |
388+
| property_case_convention | off, lower_snake_case |
389+
| enum_case_convention | off, lower_snake_case |
382390

383391
###### walker
384392
| Rule | Default |

src/.defaultsForValidator.js

+7-2
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

+8
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

+26
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 = {};
@@ -134,6 +135,31 @@ module.exports.validate = function({ resolvedSpec }, config) {
134135
});
135136
}
136137
});
138+
} else {
139+
// in the else block because usage of paths_case_convention is mutually
140+
// exclusive with usage of config.snake_case_only since it is overlapping
141+
// functionality
142+
if (config.paths_case_convention) {
143+
const checkStatusPath = config.paths_case_convention[0];
144+
if (checkStatusPath !== 'off') {
145+
const caseConvention = config.paths_case_convention[1];
146+
const segments = pathName.split('/');
147+
segments.forEach(segment => {
148+
// the first element will be "" since pathName starts with "/"
149+
// also, ignore validating the path parameters
150+
if (segment === '' || segment[0] === '{') {
151+
return;
152+
}
153+
const isCorrectCase = checkCase(segment, caseConvention);
154+
if (!isCorrectCase) {
155+
result[checkStatusPath].push({
156+
path: `paths.${pathName}`,
157+
message: `Path segments must follow case convention: ${caseConvention}`
158+
});
159+
}
160+
});
161+
}
162+
}
137163
}
138164
});
139165

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

+105
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) {
@@ -96,6 +97,34 @@ module.exports.validate = function({ jsSpec, isOAS3 }, config) {
9697
res = checkEnumValues(schema, path, config);
9798
errors.push(...res.error);
9899
warnings.push(...res.warning);
100+
} else {
101+
// optional support for property_case_convention and enum_case_convention
102+
// in config. In the else block because support should be mutually exclusive
103+
// with config.snake_case_only since it is overlapping functionality
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+
}
99128
}
100129
});
101130

@@ -284,6 +313,45 @@ function checkPropNames(schema, contextPath, config) {
284313
return result;
285314
}
286315

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

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

test/plugins/caseConventionCheck.js

+58
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
});

0 commit comments

Comments
 (0)