Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion tools/spectral/ipa/__tests__/IPA104ValidOperationID.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ testRule('xgen-IPA-104-valid-operation-id', [
name: 'valid methods with valid overrides',
document: {
paths: {
'/api/atlas/v2': {
get: {
'x-xgen-method-verb-override': {
verb: 'getSystemStatus',
customMethod: false,
},
operationId: 'getSystemStatus',
},
},
'/api/atlas/v2/federationSettings/{federationSettingsId}/connectedOrgConfigs/{orgId}/roleMappings/{id}': {
get: {
operationId: 'getFederationSettingConnectedOrgConfigRoleMapping',
Expand Down Expand Up @@ -122,7 +131,10 @@ testRule('xgen-IPA-104-valid-operation-id', [
'/api/atlas/v2/groups/{groupId}/streams/{tenantName}': {
get: {
operationId: 'getGroupStreamWorkspace',
'x-xgen-method-verb-override': { verb: 'getWorkspace', customMethod: false },
'x-xgen-method-verb-override': {
verb: 'getWorkspace',
customMethod: false,
},
},
},
},
Expand Down
13 changes: 11 additions & 2 deletions tools/spectral/ipa/__tests__/IPA105ValidOperationID.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ testRule('xgen-IPA-105-valid-operation-id', [
{
code: 'xgen-IPA-105-valid-operation-id',
message:
"Invalid OperationID. Found 'returnAllControlPlaneIpAddresses', expected 'listControlPlaneIPAddresses'. ",
"Invalid OperationID. Found 'returnAllControlPlaneIpAddresses', expected 'listControlPlaneIpAddresses'. ",
path: ['paths', '/api/atlas/v2/unauth/controlPlaneIPAddresses', 'get', 'operationId'],
severity: DiagnosticSeverity.Warning,
},
Expand All @@ -73,6 +73,12 @@ testRule('xgen-IPA-105-valid-operation-id', [
'x-xgen-operation-id-override': 'listExportBuckets',
},
},
'/api/atlas/v2/unauth/controlPlaneIPAddresses': {
get: {
operationId: 'listControlPlaneIpAddresses',
'x-xgen-operation-id-override': 'listControlPlaneAddresses',
},
},
},
},
errors: [],
Expand Down Expand Up @@ -112,7 +118,10 @@ testRule('xgen-IPA-105-valid-operation-id', [
'/api/atlas/v2/groups/{groupId}/serverless': {
get: {
operationId: 'listGroupServerlessInstances',
'x-xgen-method-verb-override': { verb: 'listInstances', customMethod: false },
'x-xgen-method-verb-override': {
verb: 'listInstances',
customMethod: false,
},
'x-xgen-operation-id-override': 'listServerlessInstances',
},
},
Expand Down
16 changes: 14 additions & 2 deletions tools/spectral/ipa/__tests__/IPA109ValidOperationID.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,19 @@ testRule('xgen-IPA-109-valid-operation-id', [
'/api/atlas/v2/federationSettings/{federationSettingsId}/connectedOrgConfigs/{orgId}': {
delete: {
operationId: 'removeFederationSettingConnectedOrgConfig',
'x-xgen-method-verb-override': { verb: 'remove', customMethod: true },
'x-xgen-method-verb-override': {
verb: 'remove',
customMethod: true,
},
'x-xgen-operation-id-override': 'removeConnectedOrgConfig',
},
},
'/api/atlas/v2/groups/{groupId}/clusters/{clusterName}:revokeMongoDBEmployeeAccess': {
delete: {
operationId: 'revokeGroupClusterMongoDbEmployeeAccess',
'x-xgen-operation-id-override': 'revokeEmployeeAccess',
},
},
},
},
errors: [],
Expand All @@ -89,7 +98,10 @@ testRule('xgen-IPA-109-valid-operation-id', [
'/api/atlas/v2/federationSettings/{federationSettingsId}/connectedOrgConfigs/{orgId}': {
delete: {
operationId: 'removeFederationSettingConnectedOrgConfig',
'x-xgen-method-verb-override': { verb: 'remove', customMethod: true },
'x-xgen-method-verb-override': {
verb: 'remove',
customMethod: true,
},
'x-xgen-operation-id-override': 'removeOrgConfigTest',
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@ describe('tools/spectral/ipa/utils/operationIdGeneration.js', () => {
expect(generateOperationID('get', '')).toEqual('get');
expect(generateOperationID('getInfo', '')).toEqual('getInfo');
});

it('should transform uppercase abbreviations and numbers to camel case correctly', () => {
expect(generateOperationID('get', '/api/atlas/v2/groups/{groupId}/openAPI')).toEqual('getGroupOpenApi');
expect(generateOperationID('list', '/api/atlas/v2/unauth/controlPlaneIPAddresses')).toEqual(
'listControlPlaneIpAddresses'
);
expect(generateOperationID('delete', '/api/atlas/v2/groups/{groupId}/userSecurity/ldap/userToDNMapping')).toEqual(
'deleteGroupUserSecurityLdapUserToDnMapping'
);
expect(generateOperationID('get', '/api/atlas/v2/groups/{groupId}/userSecurity/customerX509')).toEqual(
'getGroupUserSecurityCustomerX509'
);
});
});

describe('numberOfWords', () => {
Expand All @@ -60,6 +73,8 @@ describe('tools/spectral/ipa/utils/operationIdGeneration.js', () => {
expect(numberOfWords('createGroupClusterIndex')).toEqual(4);
expect(numberOfWords('getOpenAPIInfo')).toEqual(4);
expect(numberOfWords('getCustomDNS')).toEqual(3);
expect(numberOfWords('getX509Certificate')).toEqual(3);
expect(numberOfWords('X509Certificate')).toEqual(2);
expect(numberOfWords('')).toEqual(0);
});
});
Expand All @@ -72,6 +87,7 @@ describe('tools/spectral/ipa/utils/operationIdGeneration.js', () => {
expect(shortenOperationId('getFederationSettingConnectedOrgConfigRoleMapping')).toEqual('getConfigRoleMapping');
expect(shortenOperationId('getGroupAwsCustomDNS')).toEqual('getAwsCustomDNS');
expect(shortenOperationId('getExampleOpenAPIInfo')).toEqual('getOpenAPIInfo');
expect(shortenOperationId('getGroupUserX509Certificate')).toEqual('getUserX509Certificate');
});

it('should make no change if the operation ID is <= 4 words long or undefined', () => {
Expand Down
6 changes: 5 additions & 1 deletion tools/spectral/ipa/rulesets/IPA-109.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ functions:
- IPA109CustomMethodIdentifierFormat
- IPA109ValidOperationID

aliases:
OperationObject:
- '$.paths[*][get,put,post,delete,options,head,patch,trace]'

rules:
xgen-IPA-109-custom-method-must-be-GET-or-POST:
description: |
Expand Down Expand Up @@ -76,7 +80,7 @@ rules:
- `ignorePluralizationList`: Words that are allowed to maintain their assumed plurality (e.g., "Fts")
message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-109-valid-operation-id'
severity: warn
given: '$.paths[*][*]'
given: '#OperationObject'
then:
function: 'IPA109ValidOperationID'
functionOptions:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const inflection = require('inflection');
import { isPathParam, removePrefix, isSingleResourceIdentifier } from './resourceEvaluation.js';

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

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

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

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

opID += nouns.pop();
opID += upperCamelCase(nouns.pop());

return opID;
}

/**
* Counts the number of words in a camelCase string. Allows for abbreviations (e.g. 'getOpenAPI').
* Counts the number of words in a camelCase string. Allows for abbreviations (e.g. 'getOpenAPI') and numbers (e.g. 'X509').
* @param operationId
* @returns {number}
*/
Expand Down Expand Up @@ -99,3 +99,13 @@ function singularize(noun, ignorePluralizationList = []) {
}
return noun;
}

function upperCamelCase(input) {
if (input) {
return input
.match(CAMEL_CASE_WITH_ABBREVIATIONS)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join('');
}
return input;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { generateOperationID, numberOfWords, shortenOperationId } from '../operationIdGeneration.js';
import {
CAMEL_CASE_WITH_ABBREVIATIONS,
generateOperationID,
numberOfWords,
shortenOperationId,
} from '../operationIdGeneration.js';
import { getOperationIdOverride, hasOperationIdOverride, OPERATION_ID_OVERRIDE_EXTENSION } from '../extensions.js';

const CAMEL_CASE = /[A-Z]?[a-z]+/g;

const INVALID_OP_ID_ERROR_MESSAGE = 'Invalid OperationID.';
const TOO_LONG_OP_ID_ERROR_MESSAGE =
"The Operation ID is longer than 4 words. Please add an '" +
Expand All @@ -16,6 +19,7 @@ const TOO_LONG_OP_ID_ERROR_MESSAGE =
* @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.
* @param operationObject the operation object to validate, which should contain the operationId and optionally the x-xgen-operation-id-override extension.
* @param path the path to the operation object being evaluated, used for error reporting with Spectral.
* @param ignorePluralizationList an array of nouns to ignore when singularizing resource names.
* @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.
*/
export function validateOperationIdAndReturnErrors(
Expand Down Expand Up @@ -60,7 +64,7 @@ export function validateOperationIdAndReturnErrors(
}

function validateOperationIdOverride(operationIdOverridePath, override, expectedOperationId) {
const expectedVerb = expectedOperationId.match(CAMEL_CASE)[0];
const expectedVerb = expectedOperationId.match(CAMEL_CASE_WITH_ABBREVIATIONS)[0];
const errors = [];
if (!override.startsWith(expectedVerb)) {
errors.push({
Expand All @@ -76,15 +80,16 @@ function validateOperationIdOverride(operationIdOverridePath, override, expected
});
}

const overrideWords = override.match(CAMEL_CASE).slice(1);
const overrideWords = override.match(CAMEL_CASE_WITH_ABBREVIATIONS).slice(1);
if (overrideWords.some((word) => !expectedOperationId.includes(word))) {
errors.push({
path: operationIdOverridePath,
message: `The operation ID override must only contain nouns from the operation ID '${expectedOperationId}'.`,
});
}

const expectedLastNoun = expectedOperationId.match(CAMEL_CASE)[numberOfWords(expectedOperationId) - 1];
const expectedLastNoun =
expectedOperationId.match(CAMEL_CASE_WITH_ABBREVIATIONS)[numberOfWords(expectedOperationId) - 1];
if (!override.endsWith(expectedLastNoun)) {
errors.push({
path: operationIdOverridePath,
Expand Down
Loading