Skip to content

Commit a8ccb4b

Browse files
CLOUDP-287249: IPA-123: Validate enums must be UPPER_SNAKE_CASE (#332)
1 parent b7dc4f2 commit a8ccb4b

File tree

7 files changed

+271
-15
lines changed

7 files changed

+271
-15
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import testRule from './__helpers__/testRule';
2+
import { DiagnosticSeverity } from '@stoplight/types';
3+
4+
testRule('xgen-IPA-123-enum-values-must-be-upper-snake-case', [
5+
{
6+
name: 'valid schema - components.schemas',
7+
document: {
8+
components: {
9+
schemas: {
10+
SchemaName: {
11+
properties: {
12+
exampleProperty: {
13+
enum: ['EXAMPLE_A', 'EXAMPLE_B'],
14+
type: 'string',
15+
},
16+
},
17+
},
18+
},
19+
},
20+
},
21+
errors: [],
22+
},
23+
{
24+
name: 'invalid schema with exception - components.schemas',
25+
document: {
26+
components: {
27+
schemas: {
28+
SchemaName: {
29+
'x-xgen-IPA-exception': {
30+
'xgen-IPA-123-enum-values-must-be-upper-snake-case': 'reason',
31+
},
32+
properties: {
33+
exampleProperty: {
34+
enum: ['exampleA', 'exampleB'],
35+
type: 'string',
36+
},
37+
},
38+
},
39+
},
40+
},
41+
},
42+
errors: [],
43+
},
44+
{
45+
name: 'invalid schema - components.schemas',
46+
document: {
47+
components: {
48+
schemas: {
49+
SchemaName: {
50+
properties: {
51+
exampleProperty: {
52+
enum: ['exampleA', 'exampleB'],
53+
type: 'string',
54+
},
55+
},
56+
},
57+
},
58+
},
59+
},
60+
errors: [
61+
{
62+
code: 'xgen-IPA-123-enum-values-must-be-upper-snake-case',
63+
message: 'exampleA enum value must be UPPER_SNAKE_CASE. http://go/ipa/123',
64+
path: ['components', 'schemas', 'SchemaName', 'properties', 'exampleProperty', 'enum', '0'],
65+
severity: DiagnosticSeverity.Warning,
66+
},
67+
{
68+
code: 'xgen-IPA-123-enum-values-must-be-upper-snake-case',
69+
message: 'exampleB enum value must be UPPER_SNAKE_CASE. http://go/ipa/123',
70+
path: ['components', 'schemas', 'SchemaName', 'properties', 'exampleProperty', 'enum', '1'],
71+
severity: DiagnosticSeverity.Warning,
72+
},
73+
],
74+
},
75+
{
76+
name: 'valid schema - paths.*',
77+
document: {
78+
paths: {
79+
'/a/{exampleId}': {
80+
get: {
81+
parameters: [
82+
{
83+
schema: {
84+
type: 'string',
85+
enum: ['EXAMPLE_A', 'EXAMPLE_B'],
86+
},
87+
},
88+
],
89+
},
90+
},
91+
},
92+
},
93+
errors: [],
94+
},
95+
{
96+
name: 'invalid schema with exception - paths.*',
97+
document: {
98+
paths: {
99+
'/a/{exampleId}': {
100+
get: {
101+
parameters: [
102+
{
103+
schema: {
104+
'x-xgen-IPA-exception': {
105+
'xgen-IPA-123-enum-values-must-be-upper-snake-case': 'reason',
106+
},
107+
type: 'string',
108+
enum: ['exampleA', 'exampleB'],
109+
},
110+
},
111+
],
112+
},
113+
},
114+
},
115+
},
116+
errors: [],
117+
},
118+
{
119+
name: 'invalid schema - paths.*',
120+
document: {
121+
paths: {
122+
'/a/{exampleId}': {
123+
get: {
124+
parameters: [
125+
{
126+
schema: {
127+
type: 'string',
128+
enum: ['exampleA', 'exampleB'],
129+
},
130+
},
131+
],
132+
},
133+
},
134+
},
135+
},
136+
errors: [
137+
{
138+
code: 'xgen-IPA-123-enum-values-must-be-upper-snake-case',
139+
message: 'exampleA enum value must be UPPER_SNAKE_CASE. http://go/ipa/123',
140+
path: ['paths', '/a/{exampleId}', 'get', 'parameters', '0', 'schema', 'enum', '0'],
141+
severity: DiagnosticSeverity.Warning,
142+
},
143+
{
144+
code: 'xgen-IPA-123-enum-values-must-be-upper-snake-case',
145+
message: 'exampleB enum value must be UPPER_SNAKE_CASE. http://go/ipa/123',
146+
path: ['paths', '/a/{exampleId}', 'get', 'parameters', '0', 'schema', 'enum', '1'],
147+
severity: DiagnosticSeverity.Warning,
148+
},
149+
],
150+
},
151+
]);

tools/spectral/ipa/ipa-spectral.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ extends:
33
- ./rulesets/IPA-102.yaml
44
- ./rulesets/IPA-104.yaml
55
- ./rulesets/IPA-109.yaml
6+
- ./rulesets/IPA-123.yaml
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# IPA-123: Enums
2+
# http://go/ipa/123
3+
4+
functions:
5+
- eachEnumValueMustBeUpperSnakeCase
6+
7+
rules:
8+
xgen-IPA-123-enum-values-must-be-upper-snake-case:
9+
description: 'Enum values must be UPPER_SNAKE_CASE. http://go/ipa/123'
10+
message: '{{error}} http://go/ipa/123'
11+
severity: warn
12+
given: '$..enum'
13+
then:
14+
function: 'eachEnumValueMustBeUpperSnakeCase'
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { hasException } from './utils/exceptions.js';
2+
import { getSchemaPath, resolveObject } from './utils/componentUtils.js';
3+
import { casing } from '@stoplight/spectral-functions';
4+
5+
const RULE_NAME = 'xgen-IPA-123-enum-values-must-be-upper-snake-case';
6+
const ERROR_MESSAGE = 'enum value must be UPPER_SNAKE_CASE.';
7+
8+
export default (input, _, { path, documentInventory }) => {
9+
const oas = documentInventory.resolved;
10+
const schemaPath = getSchemaPath(path);
11+
const schemaObject = resolveObject(oas, schemaPath);
12+
if (hasException(schemaObject, RULE_NAME)) {
13+
return;
14+
}
15+
16+
const errors = [];
17+
input.forEach((enumValue, index) => {
18+
const isUpperSnakeCase = casing(enumValue, { type: 'macro' });
19+
20+
if (isUpperSnakeCase) {
21+
errors.push({
22+
path: [...path, index],
23+
message: `${enumValue} ${ERROR_MESSAGE} `,
24+
});
25+
}
26+
});
27+
28+
return errors;
29+
};

tools/spectral/ipa/rulesets/functions/eachPathAlternatesBetweenResourceNameAndPathParam.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isPathParam } from './utils/pathUtils.js';
1+
import { isPathParam } from './utils/componentUtils.js';
22
import { hasException } from './utils/exceptions.js';
33

44
const RULE_NAME = 'xgen-IPA-102-path-alternate-resource-name-path-param';
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Checks if a string belongs to a path parameter or a path parameter with a custom method.
3+
*
4+
* A path parameter has the format: `{paramName}`
5+
* A path parameter with a custom method has the format: `{paramName}:customMethod`
6+
*
7+
* @param {string} str - A string extracted from a path split by slashes.
8+
* @returns {boolean} True if the string matches the expected formats, false otherwise.
9+
*/
10+
export function isPathParam(str) {
11+
const pathParamRegEx = new RegExp(`^{[a-z][a-zA-Z0-9]*}$`);
12+
const pathParamWithCustomMethodRegEx = new RegExp(`^{[a-z][a-zA-Z0-9]*}:[a-z][a-zA-Z0-9]*$`);
13+
return pathParamRegEx.test(str) || pathParamWithCustomMethodRegEx.test(str);
14+
}
15+
16+
/**
17+
* Extracts the schema path from the given JSONPath array.
18+
*
19+
* This function is designed to handle two types of paths commonly encountered in OpenAPI definitions:
20+
*
21+
* 1. **Component Schema Paths**:
22+
* - Represented as: `components.schemas.schemaName.*.enum`
23+
* - This path indicates that the enum is defined within a schema under `components.schemas`.
24+
* - The function returns the first three elements (`["components", "schemas", "schemaName"]`).
25+
*
26+
* 2. **Parameter Schema Paths**:
27+
* - Represented as: `paths.*.method.parameters[*].schema.enum`
28+
* - This path indicates that the enum is part of a parameter's schema in an operation.
29+
* - The function identifies the location of `schema` in the path and returns everything up to (and including) it.
30+
*
31+
* @param {string[]} path - An array representing the JSONPath structure of the OpenAPI definition.
32+
* @returns {string[]} The truncated path pointing to the schema object.
33+
*/
34+
export function getSchemaPath(path) {
35+
if (path.includes('components')) {
36+
return path.slice(0, 3);
37+
} else if (path.includes('paths')) {
38+
const index = path.findIndex((item) => item === 'schema');
39+
return path.slice(0, index + 1);
40+
}
41+
}
42+
43+
/**
44+
* Resolves the value of a nested property within an OpenAPI structure using a given path.
45+
*
46+
* This function traverses an OpenAPI object based on a specified path (array of keys)
47+
* and retrieves the value at the end of the path. If any key in the path is not found,
48+
* or the value is undefined at any point, the function will return `undefined`.
49+
*
50+
* @param {Object} oas - The entire OpenAPI Specification object.
51+
* @param {string[]} objectPath - An array of strings representing the path to the desired value.
52+
* For example, `['components', 'schemas', 'MySchema', 'properties']`.
53+
* @returns {*} The value at the specified path within the OpenAPI object, or `undefined` if the path is invalid.
54+
*
55+
* @example
56+
* const oas = {
57+
* components: {
58+
* schemas: {
59+
* MySchema: {
60+
* properties: {
61+
* fieldName: { type: 'string' }
62+
* }
63+
* }
64+
* }
65+
* }
66+
* };
67+
*
68+
* const result = resolveObject(oas, ['components', 'schemas', 'MySchema', 'properties']);
69+
* console.log(result); // Output: { fieldName: { type: 'string' } }
70+
*/
71+
export function resolveObject(oas, objectPath) {
72+
return objectPath.reduce((current, key) => {
73+
return current && current[key] ? current[key] : undefined;
74+
}, oas);
75+
}

tools/spectral/ipa/rulesets/functions/utils/pathUtils.js

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)