Skip to content

Commit e8e0077

Browse files
fix(ipa): Handle abbreviations/numbers in op ID rules (#927)
1 parent 628cae6 commit e8e0077

File tree

7 files changed

+84
-16
lines changed

7 files changed

+84
-16
lines changed

tools/spectral/ipa/__tests__/IPA104ValidOperationID.test.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,15 @@ testRule('xgen-IPA-104-valid-operation-id', [
6767
name: 'valid methods with valid overrides',
6868
document: {
6969
paths: {
70+
'/api/atlas/v2': {
71+
get: {
72+
'x-xgen-method-verb-override': {
73+
verb: 'getSystemStatus',
74+
customMethod: false,
75+
},
76+
operationId: 'getSystemStatus',
77+
},
78+
},
7079
'/api/atlas/v2/federationSettings/{federationSettingsId}/connectedOrgConfigs/{orgId}/roleMappings/{id}': {
7180
get: {
7281
operationId: 'getFederationSettingConnectedOrgConfigRoleMapping',
@@ -122,7 +131,10 @@ testRule('xgen-IPA-104-valid-operation-id', [
122131
'/api/atlas/v2/groups/{groupId}/streams/{tenantName}': {
123132
get: {
124133
operationId: 'getGroupStreamWorkspace',
125-
'x-xgen-method-verb-override': { verb: 'getWorkspace', customMethod: false },
134+
'x-xgen-method-verb-override': {
135+
verb: 'getWorkspace',
136+
customMethod: false,
137+
},
126138
},
127139
},
128140
},

tools/spectral/ipa/__tests__/IPA105ValidOperationID.test.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ testRule('xgen-IPA-105-valid-operation-id', [
5050
{
5151
code: 'xgen-IPA-105-valid-operation-id',
5252
message:
53-
"Invalid OperationID. Found 'returnAllControlPlaneIpAddresses', expected 'listControlPlaneIPAddresses'. ",
53+
"Invalid OperationID. Found 'returnAllControlPlaneIpAddresses', expected 'listControlPlaneIpAddresses'. ",
5454
path: ['paths', '/api/atlas/v2/unauth/controlPlaneIPAddresses', 'get', 'operationId'],
5555
severity: DiagnosticSeverity.Warning,
5656
},
@@ -73,6 +73,12 @@ testRule('xgen-IPA-105-valid-operation-id', [
7373
'x-xgen-operation-id-override': 'listExportBuckets',
7474
},
7575
},
76+
'/api/atlas/v2/unauth/controlPlaneIPAddresses': {
77+
get: {
78+
operationId: 'listControlPlaneIpAddresses',
79+
'x-xgen-operation-id-override': 'listControlPlaneAddresses',
80+
},
81+
},
7682
},
7783
},
7884
errors: [],
@@ -112,7 +118,10 @@ testRule('xgen-IPA-105-valid-operation-id', [
112118
'/api/atlas/v2/groups/{groupId}/serverless': {
113119
get: {
114120
operationId: 'listGroupServerlessInstances',
115-
'x-xgen-method-verb-override': { verb: 'listInstances', customMethod: false },
121+
'x-xgen-method-verb-override': {
122+
verb: 'listInstances',
123+
customMethod: false,
124+
},
116125
'x-xgen-operation-id-override': 'listServerlessInstances',
117126
},
118127
},

tools/spectral/ipa/__tests__/IPA109ValidOperationID.test.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,19 @@ testRule('xgen-IPA-109-valid-operation-id', [
7474
'/api/atlas/v2/federationSettings/{federationSettingsId}/connectedOrgConfigs/{orgId}': {
7575
delete: {
7676
operationId: 'removeFederationSettingConnectedOrgConfig',
77-
'x-xgen-method-verb-override': { verb: 'remove', customMethod: true },
77+
'x-xgen-method-verb-override': {
78+
verb: 'remove',
79+
customMethod: true,
80+
},
7881
'x-xgen-operation-id-override': 'removeConnectedOrgConfig',
7982
},
8083
},
84+
'/api/atlas/v2/groups/{groupId}/clusters/{clusterName}:revokeMongoDBEmployeeAccess': {
85+
delete: {
86+
operationId: 'revokeGroupClusterMongoDbEmployeeAccess',
87+
'x-xgen-operation-id-override': 'revokeEmployeeAccess',
88+
},
89+
},
8190
},
8291
},
8392
errors: [],
@@ -89,7 +98,10 @@ testRule('xgen-IPA-109-valid-operation-id', [
8998
'/api/atlas/v2/federationSettings/{federationSettingsId}/connectedOrgConfigs/{orgId}': {
9099
delete: {
91100
operationId: 'removeFederationSettingConnectedOrgConfig',
92-
'x-xgen-method-verb-override': { verb: 'remove', customMethod: true },
101+
'x-xgen-method-verb-override': {
102+
verb: 'remove',
103+
customMethod: true,
104+
},
93105
'x-xgen-operation-id-override': 'removeOrgConfigTest',
94106
},
95107
},

tools/spectral/ipa/__tests__/utils/operationIdGeneration.test.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,19 @@ describe('tools/spectral/ipa/utils/operationIdGeneration.js', () => {
5050
expect(generateOperationID('get', '')).toEqual('get');
5151
expect(generateOperationID('getInfo', '')).toEqual('getInfo');
5252
});
53+
54+
it('should transform uppercase abbreviations and numbers to camel case correctly', () => {
55+
expect(generateOperationID('get', '/api/atlas/v2/groups/{groupId}/openAPI')).toEqual('getGroupOpenApi');
56+
expect(generateOperationID('list', '/api/atlas/v2/unauth/controlPlaneIPAddresses')).toEqual(
57+
'listControlPlaneIpAddresses'
58+
);
59+
expect(generateOperationID('delete', '/api/atlas/v2/groups/{groupId}/userSecurity/ldap/userToDNMapping')).toEqual(
60+
'deleteGroupUserSecurityLdapUserToDnMapping'
61+
);
62+
expect(generateOperationID('get', '/api/atlas/v2/groups/{groupId}/userSecurity/customerX509')).toEqual(
63+
'getGroupUserSecurityCustomerX509'
64+
);
65+
});
5366
});
5467

5568
describe('numberOfWords', () => {
@@ -60,6 +73,8 @@ describe('tools/spectral/ipa/utils/operationIdGeneration.js', () => {
6073
expect(numberOfWords('createGroupClusterIndex')).toEqual(4);
6174
expect(numberOfWords('getOpenAPIInfo')).toEqual(4);
6275
expect(numberOfWords('getCustomDNS')).toEqual(3);
76+
expect(numberOfWords('getX509Certificate')).toEqual(3);
77+
expect(numberOfWords('X509Certificate')).toEqual(2);
6378
expect(numberOfWords('')).toEqual(0);
6479
});
6580
});
@@ -72,6 +87,7 @@ describe('tools/spectral/ipa/utils/operationIdGeneration.js', () => {
7287
expect(shortenOperationId('getFederationSettingConnectedOrgConfigRoleMapping')).toEqual('getConfigRoleMapping');
7388
expect(shortenOperationId('getGroupAwsCustomDNS')).toEqual('getAwsCustomDNS');
7489
expect(shortenOperationId('getExampleOpenAPIInfo')).toEqual('getOpenAPIInfo');
90+
expect(shortenOperationId('getGroupUserX509Certificate')).toEqual('getUserX509Certificate');
7591
});
7692

7793
it('should make no change if the operation ID is <= 4 words long or undefined', () => {

tools/spectral/ipa/rulesets/IPA-109.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ functions:
77
- IPA109CustomMethodIdentifierFormat
88
- IPA109ValidOperationID
99

10+
aliases:
11+
OperationObject:
12+
- '$.paths[*][get,put,post,delete,options,head,patch,trace]'
13+
1014
rules:
1115
xgen-IPA-109-custom-method-must-be-GET-or-POST:
1216
description: |
@@ -76,7 +80,7 @@ rules:
7680
- `ignorePluralizationList`: Words that are allowed to maintain their assumed plurality (e.g., "Fts")
7781
message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-109-valid-operation-id'
7882
severity: warn
79-
given: '$.paths[*][*]'
83+
given: '#OperationObject'
8084
then:
8185
function: 'IPA109ValidOperationID'
8286
functionOptions:

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const inflection = require('inflection');
22
import { isPathParam, removePrefix, isSingleResourceIdentifier } from './resourceEvaluation.js';
33

44
const CAMEL_CASE = /[A-Z]?[a-z]+/g;
5-
const CAMEL_CASE_WITH_ABBREVIATIONS = /[A-Z]+(?![a-z])|[A-Z]*[a-z]+/g;
5+
export const CAMEL_CASE_WITH_ABBREVIATIONS = /[A-Z]+(?![a-z0-9])|[A-Z]*[a-z0-9]+/g;
66

77
/**
88
* Returns IPA Compliant Operation ID.
@@ -40,7 +40,7 @@ export function generateOperationID(method, path, ignorePluralizationList = [])
4040

4141
let opID = verb;
4242
for (let i = 0; i < nouns.length - 1; i++) {
43-
opID += singularize(nouns[i], ignorePluralizationList);
43+
opID += upperCamelCase(singularize(nouns[i], ignorePluralizationList));
4444
}
4545

4646
// singularize final noun, dependent on resource identifier - leave custom nouns alone
@@ -52,13 +52,13 @@ export function generateOperationID(method, path, ignorePluralizationList = [])
5252
nouns[nouns.length - 1] = singularize(nouns[nouns.length - 1], ignorePluralizationList);
5353
}
5454

55-
opID += nouns.pop();
55+
opID += upperCamelCase(nouns.pop());
5656

5757
return opID;
5858
}
5959

6060
/**
61-
* Counts the number of words in a camelCase string. Allows for abbreviations (e.g. 'getOpenAPI').
61+
* Counts the number of words in a camelCase string. Allows for abbreviations (e.g. 'getOpenAPI') and numbers (e.g. 'X509').
6262
* @param operationId
6363
* @returns {number}
6464
*/
@@ -99,3 +99,13 @@ function singularize(noun, ignorePluralizationList = []) {
9999
}
100100
return noun;
101101
}
102+
103+
function upperCamelCase(input) {
104+
if (input) {
105+
return input
106+
.match(CAMEL_CASE_WITH_ABBREVIATIONS)
107+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
108+
.join('');
109+
}
110+
return input;
111+
}

tools/spectral/ipa/rulesets/functions/utils/validations/validateOperationIdAndReturnErrors.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import { generateOperationID, numberOfWords, shortenOperationId } from '../operationIdGeneration.js';
1+
import {
2+
CAMEL_CASE_WITH_ABBREVIATIONS,
3+
generateOperationID,
4+
numberOfWords,
5+
shortenOperationId,
6+
} from '../operationIdGeneration.js';
27
import { getOperationIdOverride, hasOperationIdOverride, OPERATION_ID_OVERRIDE_EXTENSION } from '../extensions.js';
38

4-
const CAMEL_CASE = /[A-Z]?[a-z]+/g;
5-
69
const INVALID_OP_ID_ERROR_MESSAGE = 'Invalid OperationID.';
710
const TOO_LONG_OP_ID_ERROR_MESSAGE =
811
"The Operation ID is longer than 4 words. Please add an '" +
@@ -16,6 +19,7 @@ const TOO_LONG_OP_ID_ERROR_MESSAGE =
1619
* @param resourcePath the resource path for the endpoint (e.g. '/users', '/users/{userId}', etc.). For custom methods, this is the path without the custom method name.
1720
* @param operationObject the operation object to validate, which should contain the operationId and optionally the x-xgen-operation-id-override extension.
1821
* @param path the path to the operation object being evaluated, used for error reporting with Spectral.
22+
* @param ignorePluralizationList an array of nouns to ignore when singularizing resource names.
1923
* @returns {[{path: string[], message: string}]} an array of error objects, each containing a path and a message, or an empty array if no errors are found.
2024
*/
2125
export function validateOperationIdAndReturnErrors(
@@ -60,7 +64,7 @@ export function validateOperationIdAndReturnErrors(
6064
}
6165

6266
function validateOperationIdOverride(operationIdOverridePath, override, expectedOperationId) {
63-
const expectedVerb = expectedOperationId.match(CAMEL_CASE)[0];
67+
const expectedVerb = expectedOperationId.match(CAMEL_CASE_WITH_ABBREVIATIONS)[0];
6468
const errors = [];
6569
if (!override.startsWith(expectedVerb)) {
6670
errors.push({
@@ -76,15 +80,16 @@ function validateOperationIdOverride(operationIdOverridePath, override, expected
7680
});
7781
}
7882

79-
const overrideWords = override.match(CAMEL_CASE).slice(1);
83+
const overrideWords = override.match(CAMEL_CASE_WITH_ABBREVIATIONS).slice(1);
8084
if (overrideWords.some((word) => !expectedOperationId.includes(word))) {
8185
errors.push({
8286
path: operationIdOverridePath,
8387
message: `The operation ID override must only contain nouns from the operation ID '${expectedOperationId}'.`,
8488
});
8589
}
8690

87-
const expectedLastNoun = expectedOperationId.match(CAMEL_CASE)[numberOfWords(expectedOperationId) - 1];
91+
const expectedLastNoun =
92+
expectedOperationId.match(CAMEL_CASE_WITH_ABBREVIATIONS)[numberOfWords(expectedOperationId) - 1];
8893
if (!override.endsWith(expectedLastNoun)) {
8994
errors.push({
9095
path: operationIdOverridePath,

0 commit comments

Comments
 (0)